RxAutomaton icon indicating copy to clipboard operation
RxAutomaton copied to clipboard

Pass input parameters to producer in mappings

Open AndrewSB opened this issue 7 years ago • 8 comments

Along the lines of #1, I'd like to be able to pass the parameters from the input into the producer. For example, if I have a producer: discoverCharacteristicsProducer: ([CBService]) -> Observable<Event>, I know I can call it by using a closure as my NextMapping screen shot 2016-09-26 at 5 06 40 pm

in the last element ^ I capture the services from the .discoveredServices event, and I then pass it into the discoverCharacteristicsProducer

Is it possible to do capture the param using the pipe mapping syntax? I'm sure theres some way to compose the operation, I'm not able to think of the type signature for a new | operator that would do this

AndrewSB avatar Sep 27 '16 00:09 AndrewSB

TL;DR: I'm missing an elegant way to pass an associated value from Input into the Producer

AndrewSB avatar Sep 27 '16 05:09 AndrewSB

@AndrewSB Thanks for your suggestions along with #1! 😄

For #2, I was formerly implementing exact the same code in inamiy/ReactiveAutomaton@561b137 with the new type NextProducerGenerator, but reverted afterward to avoid too much complexity.

My personal feeling is that it's not easy to keep using table-like syntax when many different types are involved in [NextMapping] (before reduce), especially when handling multiple input types as discussed in #1.

But this could be a challenging topic to implement it. Let me take some time to think 🤔

inamiy avatar Sep 27 '16 06:09 inamiy

I think all thats needed are two new | operators, that take. The first which captures the variable from the enum associated value, and the second that passes that captured value to the producer.

What do you think?

AndrewSB avatar Sep 29 '16 19:09 AndrewSB

This is an example of one of my nextMappings:

var discoveringServicesToDiscoveringCharacteristics: Automaton<State, Event>.NextMapping { //swiftlint:disable:this variable_name
        return { (state: State, input: Event) in
            guard case .discoveredServices(let services) = input else { return nil }
            guard [.discoveringServices, .waitingToDiscoverCharacteristics].contains(state) else { return nil }

            return (.discoveringCharacteristics, self.discoverCharacteristicsProducer(services))
        }
    }

The significant parts is capturing the services from the .discoveredServices input, and passing it to the discoverCharacteristicsProducer

I think (but Im not sure) that the change is simpler than what you were trying in https://github.com/inamiy/ReactiveAutomaton/commit/561b137, if we make it so this function becomes generic, on T?, then if the enum has an associated value, T? gets filled in, and gets passes into the NextMapping constructor

I'm not sure though. Does that make any sense to you?

AndrewSB avatar Sep 29 '16 19:09 AndrewSB

I think I've come up with a partially complete solution. Very similar to your https://github.com/inamiy/ReactiveAutomaton/commit/561b137, but not as polished.

Basically, I defined a function on genericT that can take an input enum that has one associatedValue, and a fromState to produce a Mapping:

func | <T, State, Input: Equatable & EnumAssociatedValueReadable>(input: @escaping (T) -> Input, transition: Transition<State>) -> (State, Input) -> State? {
    return { fromState, event in
        guard let eventAssociatedValue = event.associated.value as? T else {
            return nil
        }

        // make sure that the event passed in matches the input param with the same associated value
        if input(eventAssociatedValue) == event && transition.fromState(fromState) {
            return transition.toState
        } else {
            return nil
        }
    }
}

It's a combination of the two functions (1 and 2) that work with a generic associatedValue.

It also uses an protocol I found in this stack overflow post called EnumAssociatedValueReadable:

protocol EnumAssociatedValueReadable {
    // implemented for one associated value, probably can be extended to support multiple
    var associated: (label: String, value: Any?) { get }
}
extension EnumAssociatedValueReadable {
    var associated: (label: String, value: Any?) {
        get {
            let mirror = Mirror(reflecting: self)
            if let associated = mirror.children.first {
                return (associated.label!, associated.value)
            }
            return ("\(self)", nil)
        }
    }
}

and I can use it with the first example from my first comment in this issue (discovered(rssi: 0) | .none => .connecting | connectProducer like this Event.discovered | .none => .connecting | connectProducer, where the first pipe operator is the new one I defined above, and the second the same as before, defined in the Mapping+Helper: https://github.com/inamiy/RxAutomaton/blob/swift/3.0/Sources/Mapping%2BHelper.swift#L61

AndrewSB avatar Sep 30 '16 13:09 AndrewSB

So ^ is the first new | operator. I think you were right, and for the second, something like the NextProducerGenerator has to be defined 😁

AndrewSB avatar Sep 30 '16 13:09 AndrewSB

Sorry for late reply (I was on the trip, and still busy for another couple of weeks). Thanks for your idea, I'm still glancing your code though, it looks amazing!

However, I'm not positive with using reflection to extract the associated value. There should be some better way, and I'm thinking of lifting the type from:

typealias NextMapping = (State, Input) -> (State, Producer)

to:

typealias NextMapping2 = T -> (State, Input) -> T -> (State, Producer)
// or, typealias NextMapping2 = (State, T -> Input) -> (State, T -> Producer)

and add more helper functions to transform NextMapping to NextMapping2.

inamiy avatar Oct 12 '16 01:10 inamiy

@inamiy are you against using reflection for the lack of clarity? Or because it seems like the wrong approach/hard result?

Your approach looks good too!

AndrewSB avatar Oct 13 '16 22:10 AndrewSB