7

I have a class, StateMachine, that is generic to allow for different sets of states to be implemented as, for example, an enum. I want to use a StateMachineDelegate protocol to notify a delegate when the state machine enters a new state.

But this doesn't work since the delegate protocol is also generic with type requirements. The error shows where the delegate property is declared.

protocol StateType: Hashable {}

protocol StateMachineDelegate: class {
    typealias S: StateType
    func stateMachine(stateMachine: StateMachine<S>, didEnterState newState: S)
}

class StateMachine<S: StateType> {
    typealias State = S

    weak var delegate: StateMachineDelegate?
    //~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~
    //Protocol 'StateMachineDelegate' can only be used as a generic constraint because it has Self or associated type requirements

    var currentState: State {...}

    init(initialState: State) {...}

    func applyState(toState: State) -> Bool {
        ...
        currentState = toState
        delegate?.stateMachine(self, didEnterState: toState)
        ...
    }
}



I need to somehow associate that StateMachineDelegate.S == S in the StateMachine class, but I'm not sure how to do this, or if it's possible. I tried:

class StateMachine<S: StateType, D: StateMachineDelegate where D.S == S> {
    ...
    weak var delegate: D?
    ...
}

but then I get stuck trying to rework the protocol to properly declare the generic type of StateMachine. And it doesn't seem right to have to declare the type of the delegate up front when creating a StateMachine.

Stuart
  • 36,683
  • 19
  • 101
  • 139

2 Answers2

1

See if this workaround is ok for your needs, it uses @autoclosure to get rid of a problem with recursive generic definitions:

class StateMachine<S: Printable, D: StateMachineDelegate where S == D.StateType> {

    var currentState: S {
        didSet {
            // The observer
            if let delegate = self.delegate {
                delegate.stateMachine(self, didEnterState: self.currentState)
            }
        }
    }

    var delegate: D?

    init(initialState: S) {
        self.currentState = initialState
    }


}


protocol StateMachineDelegate: class {
    typealias StateType: Printable

    // Workaround with autoclosure
    func stateMachine(machine: @autoclosure() -> StateMachine<StateType, Self>, didEnterState newState: StateType)
}

final class ADelegate: StateMachineDelegate {
    typealias StateType = Int
    func stateMachine(machine: @autoclosure  () -> StateMachine<StateType, ADelegate>, didEnterState newState: StateType) {
        // Need to _unbox_ the sander from the closure
        let sender = machine()
        println(newState)
        println("State from sender: \(sender.currentState)")
    }
}

let stateMachine = StateMachine<Int, ADelegate>(initialState: 24)

stateMachine.delegate = ADelegate()
stateMachine.currentState = 50

By the way, consider that if you get the sander, probably you don't need to get the newState passed. I used Printable in place of Hashable for the example.

Matteo Piombo
  • 6,688
  • 2
  • 25
  • 25
0

I think its just a name collision problem ... try this:

protocol StateType: Hashable {}

protocol StateMachineDelegate: class {
    typealias State: StateType
    func stateMachine(stateMachine: StateMachine<State>, didEnterState newState: State)
}

class StateMachine<S: StateType> {
    typealias State = S

    weak var delegate: StateMachineDelegate?


    var currentState: State {...}

    init(initialState: State) {...}

    func applyState(toState: State) -> Bool {
        ...
            currentState = toState
        delegate?.stateMachine(self, didEnterState: toState)
        ...
    }
}

You need to declare what the name of the generic type defined in the protocol should be in the class conforming to it.

Matteo Piombo
  • 6,688
  • 2
  • 25
  • 25
  • 1
    By the way, given that state machine is a good design pattern for a lot of problems, why constraining the state to be hashable? You loose the power of enums with associated values ( or at least you'll have to conform them to hashable). IMHO enums with associated values are great for state machines logics. – Matteo Piombo Jan 29 '15 at 12:55
  • Thanks, but this doesn't work. I think the problem runs a bit deeper. My guess is that the compiler can't (for whatever reason) guarantee that the `StateMachineDelegate`'s type requirements will be satisfied, as `StateMachine` doesn't make any promises in that regard. If `StateMachine` included a _generic_ type parameter constrained to `StateMachineDelegate`, then it's promising that it'll be replaced with a satisfactory class at runtime (but then I end up with an odd parameter of `StateMachine` in the delegate protocol). Perhaps it'll become possible as the language develops. – Stuart Jan 30 '15 at 08:18
  • Regarding the hashable constraint, I did this because the state machine actually stores states & state transitions, and checks for their existence to determine whether a requested state change is valid. The class is a bit more complex than I showed in the question, and will get more complex as I experiment further! If associated enums are handy, it shouldn't be a problem to conform them to `Hashable`, like you suggest. – Stuart Jan 30 '15 at 08:30
  • Ups sorry, playground crashing so frequently that it stopped giving me the error :-/ Isn't it more correct if the delegate defines an alias with regard to the state machine type, instead of the state type? ... Sadly trying this way is a constant crash of playground and editor assistant to me :-S – Matteo Piombo Jan 30 '15 at 09:33
  • SourceKit is crashing constantly for me too... clearly this is giving the compiler as much of a headache as me! You're probably right about defining the state machine as the typealias (although I still need to reference a `State` type in the delegate method, so might use: `typealias StateMachine: Stateful`, then `protocol Stateful { typealias State }` and `class StateMachine: Stateful {...}`... then in the delegate can refer to `StateMachine.State`). But the delegate protocol would still be generic (has an associated type requirement), so doesn't fix the issue. – Stuart Jan 30 '15 at 10:03
  • As a workaround you could explore the possibility of assigning an optional function to the state machine instead of a delegate :-/ – Matteo Piombo Jan 30 '15 at 10:16