Git Product home page Git Product logo

swiftstate's Introduction

SwiftState

Elegant state machine for Swift.

SwiftState

Example

enum MyState: StateType {
    case state0, state1, state2
}
// setup state machine
let machine = StateMachine<MyState, NoEvent>(state: .state0) { machine in
    
    machine.addRoute(.state0 => .state1)
    machine.addRoute(.any => .state2) { context in print("Any => 2, msg=\(context.userInfo)") }
    machine.addRoute(.state2 => .any) { context in print("2 => Any, msg=\(context.userInfo)") }
    
    // add handler (`context = (event, fromState, toState, userInfo)`)
    machine.addHandler(.state0 => .state1) { context in
        print("0 => 1")
    }
    
    // add errorHandler
    machine.addErrorHandler { event, fromState, toState, userInfo in
        print("[ERROR] \(fromState) => \(toState)")
    }
}

// initial
XCTAssertEqual(machine.state, MyState.state0)

// tryState 0 => 1 => 2 => 1 => 0

machine <- .state1
XCTAssertEqual(machine.state, MyState.state1)

machine <- (.state2, "Hello")
XCTAssertEqual(machine.state, MyState.state2)

machine <- (.state1, "Bye")
XCTAssertEqual(machine.state, MyState.state1)

machine <- .state0  // fail: no 1 => 0
XCTAssertEqual(machine.state, MyState.state1)

This will print:

0 => 1
Any => 2, msg=Optional("Hello")
2 => Any, msg=Optional("Bye")
[ERROR] state1 => state0

Transition by Event

Use <-! operator to try transition by Event rather than specifying target State.

enum MyEvent: EventType {
    case event0, event1
}
let machine = StateMachine<MyState, MyEvent>(state: .state0) { machine in
    
    // add 0 => 1 => 2
    machine.addRoutes(event: .event0, transitions: [
        .state0 => .state1,
        .state1 => .state2,
    ])
    
    // add event handler
    machine.addHandler(event: .event0) { context in
        print(".event0 triggered!")
    }
}

// initial
XCTAssertEqual(machine.state, MyState.state0)

// tryEvent
machine <-! .event0
XCTAssertEqual(machine.state, MyState.state1)

// tryEvent
machine <-! .event0
XCTAssertEqual(machine.state, MyState.state2)

// tryEvent (fails)
machine <-! .event0
XCTAssertEqual(machine.state, MyState.state2, "event0 doesn't have 2 => Any")

If there is no Event-based transition, use built-in NoEvent instead.

State & Event enums with associated values

Above examples use arrow-style routing which are easy to understand, but it lacks in ability to handle state & event enums with associated values. In such cases, use either of the following functions to apply closure-style routing:

  • machine.addRouteMapping(routeMapping)
    • RouteMapping: (_ event: E?, _ fromState: S, _ userInfo: Any?) -> S?
  • machine.addStateRouteMapping(stateRouteMapping)
    • StateRouteMapping: (_ fromState: S, _ userInfo: Any?) -> [S]?

For example:

enum StrState: StateType {
    case str(String) ...
}
enum StrEvent: EventType {
    case str(String) ...
}

let machine = Machine<StrState, StrEvent>(state: .str("initial")) { machine in
    
    machine.addRouteMapping { event, fromState, userInfo -> StrState? in
        // no route for no-event
        guard let event = event else { return nil }
        
        switch (event, fromState) {
            case (.str("gogogo"), .str("initial")):
                return .str("Phase 1")
            case (.str("gogogo"), .str("Phase 1")):
                return .str("Phase 2")
            case (.str("finish"), .str("Phase 2")):
                return .str("end")
            default:
                return nil
        }
    }
    
}

// initial
XCTAssertEqual(machine.state, StrState.str("initial"))

// tryEvent (fails)
machine <-! .str("go?")
XCTAssertEqual(machine.state, StrState.str("initial"), "No change.")

// tryEvent
machine <-! .str("gogogo")
XCTAssertEqual(machine.state, StrState.str("Phase 1"))

// tryEvent (fails)
machine <-! .str("finish")
XCTAssertEqual(machine.state, StrState.str("Phase 1"), "No change.")

// tryEvent
machine <-! .str("gogogo")
XCTAssertEqual(machine.state, StrState.str("Phase 2"))

// tryEvent (fails)
machine <-! .str("gogogo")
XCTAssertEqual(machine.state, StrState.str("Phase 2"), "No change.")

// tryEvent
machine <-! .str("finish")
XCTAssertEqual(machine.state, StrState.str("end"))

This behaves very similar to JavaScript's safe state-container rackt/Redux, where RouteMapping can be interpretted as Redux.Reducer.

For more examples, please see XCTest cases.

Features

  • Easy Swift syntax
    • Transition: .state0 => .state1, [.state0, .state1] => .state2
    • Try state: machine <- .state1
    • Try state + messaging: machine <- (.state1, "GoGoGo")
    • Try event: machine <-! .event1
  • Highly flexible transition routing
    • Using Condition

    • Using .any state

      • Entry handling: .any => .someState
      • Exit handling: .someState => .any
      • Blacklisting: .any => .any + Condition
    • Using .any event

    • Route Mapping (closure-based routing): #36

  • Success/Error handlers with order: UInt8 (more flexible than before/after handlers)
  • Removable routes and handlers using Disposable
  • Route Chaining: .state0 => .state1 => .state2
  • Hierarchical State Machine: #10

Terms

Term Type Description
State StateType (protocol) Mostly enum, describing each state e.g. .state0.
Event EventType (protocol) Name for route-group. Transition can be fired via Event instead of explicitly targeting next State.
State Machine Machine State transition manager which can register Route/RouteMapping and Handler separately for variety of transitions.
Transition Transition From- and to- states represented as .state1 => .state2. Also, .any can be used to represent any state.
Route Route Transition + Condition.
Condition Context -> Bool Closure for validating transition. If condition returns false, transition will fail and associated handlers will not be invoked.
Route Mapping (event: E?, fromState: S, userInfo: Any?) -> S? Another way of defining routes using closure instead of transition arrows (=>). This is useful when state & event are enum with associated values. Return value (S?) means preferred-toState, where passing nil means no routes available. See #36 for more info.
State Route Mapping (fromState: S, userInfo: Any?) -> [S]? Another way of defining routes using closure instead of transition arrows (=>). This is useful when state is enum with associated values. Return value ([S]?) means multiple toStates from single fromState (synonym for multiple routing e.g. .state0 => [.state1, .state2]). See #36 for more info.
Handler Context -> Void Transition callback invoked when state has been changed successfully.
Context (event: E?, fromState: S, toState: S, userInfo: Any?) Closure argument for Condition & Handler.
Chain TransitionChain / RouteChain Group of continuous routes represented as .state1 => .state2 => .state3

Related Articles

  1. Swiftで有限オートマトン(ステートマシン)を作る - Qiita (Japanese)
  2. Swift+有限オートマトンでPromiseを拡張する - Qiita (Japanese)

Licence

MIT

swiftstate's People

Contributors

adamyanalunas avatar ajandrade avatar erudel avatar foulkesjohn avatar georgemp avatar humblehacker avatar ileitch avatar inamiy avatar kkirsten avatar mattprowse avatar nwest avatar readmecritic avatar rhummelmose avatar s2ler avatar siyusong avatar srinivas-codezyng avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

swiftstate's Issues

Build errors on Xcode 6.1 Beta 3

Building only SwiftState.xcodeproj:

String+SwiftState.swift:10:1: Type 'String' does not conform to protocol 'NilLiteralConvertible'

Building within another project (as pod):

StateTransitionChain.swift:80:41: Cannot invoke '=>' with an argument list of type '(StateTransitionChain<S>, S)'
StateTransitionChain.swift:91:17: Cannot invoke '=>' with an argument list of type '(S, StateTransitionChain<S>)'

Any idea how to solve these build errors?

How to use with a Vapor3 project

Hi

I'm a Swift/Vapor newbie and need some help configuring my Vapor3 app to use SwiftState.

I support I should first ask if this library can even be used with Swift4/Vapor3 - can it ?

If it can - how do I configure my app to use it ?

Thanks
Dave

Carthage Build failed with exit code 65

On carhage update, build fail:

carthage version: 0.27.0
xcode version: 9.2

Build Failed
Task failed with exit code 65:
/usr/bin/xcrun xcodebuild -project /Users/vml933/Documents/XCodeProject/PalLinkTest3/Carthage/Checkouts/SwiftState/SwiftState.xcodeproj -scheme SwiftState -configuration Release -derivedDataPath /Users/vml933/Library/Caches/org.carthage.CarthageKit/DerivedData/9.2_9C40b/SwiftState/4.1.0 -sdk iphoneos ONLY_ACTIVE_ARCH=NO BITCODE_GENERATION_MODE=bitcode CODE_SIGNING_REQUIRED=NO CODE_SIGN_IDENTITY= CARTHAGE=YES archive -archivePath /var/folders/c6/cmhy9djd5y52994tjg3lsq7c0000gn/T/SwiftState SKIP_INSTALL=YES GCC_INSTRUMENT_PROGRAM_FLOW_ARCS=NO CLANG_ENABLE_CODE_COVERAGE=NO (launched in /Users/vml933/Documents/XCodeProject/PalLinkTest3/Carthage/Checkouts/SwiftState)

This usually indicates that project itself failed to compile. Please check the xcodebuild log for more details: /var/folders/c6/cmhy9djd5y52994tjg3lsq7c0000gn/T/carthage-xcodebuild.WbBQpz.log

Can't use event handler by itself

Why can't I use the event handler by itself without adding a route? Shouldn't this be a design decision up to the client programmer?

machine.addEventHandler(.GotoPreviousCard, handler: { (event, transition, order, userInfo) -> Void in
  println("FeedView ! GotoPreviousCard")
})

Instead, I have to add a route that messes everything up. Sure, I can make it work but it's pretty pointless when I don't want to change states and instead I have code firing twice I would rather not be.

I would like my view to react to events without making state changes. If anything, I would rather filter events for certain states. For example, if someone "likes" a picture, you might send a Like event during the ShowingPhoto state. Your library could make the validation and what not.

In lieu of this functionality, I made transitions with nil => nil

Whats meaning of nil => .State2 ? Got an error

素晴らしいライブラリです
I tried from 0=>2, I got a error, whats meaning of nil => .State2, I think state2 can accept a transform from any other state, isn't it?

    // tryState 0 => 2 => 1 => 2 => 1 => 0
    machine <- (.State2, "Ping") //Any => 2, msg=Ping fatal error: unexpectedly found nil while unwrapping an Optional value
    machine <- .State1
    machine <- (.State2, "Hello")
    machine <- (.State1, "Bye")
    machine <- .State0  // fail: no 1 => 0

Type 'String' does not conform to protocol 'NilLiteralConvertible'

After fiddling around a bit more, I got the following compiler error:

String+SwiftState.swift:10:1: Type 'String' does not conform to protocol 'NilLiteralConvertible'

Changing to the following solved my error:

extension String: StateType, StateEventType
{
    public init(nilLiteral: ()) {
        self = ""
    }
}

(Xcode 6.1 Beta 3)

State & Event enums with associated values complete example

Hi

I'm having some trouble understanding the 'State & Event enums with associated values' example on the main project page. I'm specifically confused by incomplete enum definitions ...

enum StrState: StateType {
case str(String) ...
}
enum StrEvent: EventType {
case str(String) ...
}

Can someone please respond to this question with a complete example or update the project README ?

Thanks
Dave

Swift 1.2 support

There are currently several issues with the build after the latest Xcode update (6D520o). We are looking at these locally and probably have a patch during today.

How are you handling backwards incompatible changes at the moment? After the updates the code will only compile with Swift 1.2 as there is no proper support for conditional compiling in the language.

RouteEvent from .AnyState

Hi,
is it a bug the that last line of the code below returns false or I misunderstood something?

enum State : StateType {
    case A, B, C
    case AnyState

    init(nilLiteral: Void) {
        self = AnyState
    }
}

enum Event: StateEventType {
    case Event1
    case AnyEvent

    init(nilLiteral: Void) {
        self = AnyEvent
    }
}

let machine = StateMachine<State, Event>(state: State.A) { machine in
    machine.addRoute(.A => .B)
    machine.addRoute(.AnyState => .C)
    machine.addRouteEvent(.Event1, transitions: [.AnyState => .A])
}

machine <- .C       // Returns true
machine <-! .Event1 // Returns false ... ?bug?

suggestion about state and context

Suggestions and comments about the design (not a bug.)

Should state be an attribute of the machine, or the object which is going through the machine?

My use case is a game having many tokens (AKA sprites) each having state and a state machine. I don't mind each token owning an instance of StateMachine, but then I must configure each instance the same way (easy enough to workaround.) But possibly a StateMachine should have a Design which is configured and passed to a StateMachine instance.

In other implementations of FSM 'context' seems to mean: the object which has state, which the state machine sets the state of, and which is passed to all actions (what you call Handlers) for them to act on. Your context is not the same thing? Maybe a solution is a delegate for the state machine?

On another topic, I don't understand the use case for AnyState. Is one use case adding transitions from any state to a reset state, so that you don't have to specify a transition from every state to the reset state? Maybe you could explain in your document. But I should read the code, and for example study what a transition from AnyState to AnyState would mean.

Thanks, SwiftState is rather elegant.

suggestion on transition and condition, which should include Event

There is a case that the event has associate values which cannot be enumerated all the possible values, so that it is not possible to add all Event Route into State Machine.

In this case, the only way to do it is to use condition.

But unfortunately, condition only includes transition which has no information about the event.

So I suggest to add Event information into transition.

Arrow pointing the wrong direction for sending events?

This issue is a very non-issue but... shouldn't machine <- event be event -> machine? Code is naturally read from left to right (at least, in english code).

The only reason I can think of to not use -> is because it is a reserved Swift operator. Instead, why not reuse the => operator that is already declared and used in the project to describe state transitions?

onEntry and onExit handlers?

From the README, I can see that you could construct state entry and exit handlers likes so:

// onExit handler
machine.addHandler(.State0 => .Any) { context in
   print("0 => 1")
}
// onEntry handler
machine.addHandler(.Any => .State0) { context in
   print("0 => 1")
}

But will these handlers be executed in the expected order? I don't really want to have to manage order through the int myself...

Add addChainErrorHandler method.

Psudocode

machine.addRoute(nil=>nil) // connect all states
machine.addRouteChain(1=>2=>3) { ctx in println("SUCCEEDED") }
machine.addChainErrorHandler(1=>2=>3) { ctx in println("FAILED") }

// current state = 0
machine <- 1 // start chaining
machine <- 3 // chaining failed (println FAILED)
machine <- 2 // already failed (no println)
machine <- 1 // reset & start chaining again
machine <- 2 // succeeding...
machine <- 3 // chaining succeeded (println SUCCEEDED)

Allow Swift 2.0 code to be used by CocoaPods

Since #25 got merged in (thanks!) it'd be nice to have an updated podspec so I can use it in my apps. However, as Swift 2.0 isn't yet in production a branch would suffice so I can do something like:

pod 'SwiftState', :git => '[email protected]:ReactKit/SwiftState.git', :branch => 'swift-2.0'

What do you think, @inamiy?

Support for Hierarchical State Machine?

Hi @inamiy, I really like your take on state machines in Swift. I'm now running into an issue in which support for a Hierarchical State Machine would benefit the architectural design. For example, I have three states: A, B and C. A and B are two variants; and it is possible to go from either A or B to C. However, from C it is only possible to go to A. So I would need a handler (A, B) => C and C => A. However SwiftState doesn't allow me to create a handler (A, B) => C, so either I duplicate the handler (no good), or disregard the current state and add nil => C (but that also allows C => C, and a future D => C). As the states grow complexer, this would result in headaches; as adding yet another state would require re-evaluating all the current handlers.

iOS 7 support

I've got a question regarding iOS support.

Do you know if SwiftState will work if I include the code in an iOS project that has min deployment target of iOS 7?

Thanks

Can't build with Swift 2.0 on XCode7 beta5

When I try to build the swift/2.0 branch on XCode7, I'm getting nearly a hundred errors about private class names. I think what's happening is that the private type aliases are being misinterpreted by the swift compiler. It's difficult to test this assumption because I'm getting so many build errors that I can't find the root of the type/alias dependencies. Have you seen anything like this?

Example in Readme lacks MyEvent.

I cut and pasted the example in the Readme into a project that references SwiftState framework. It would not compile until 1) I switched to building for "iOS Device" and 2) I cut and pasted your example definition of MyEvent into the project.

Now I understand that the caller must define the MyEvent type. But the example could be better.

Also, I don't understand how some of the StateMachine tests work because they also reference MyEvent type. It is not clear to me how the tests found that type. But I know little about XCTest.

Machine.addHandler(transition: order: handler:) -> Disposable not working ?

I modified the basic test case as following and it's failing.
Any ideas to get rid of ?

    func testREADME_tryEvent()
    {
        var handlerCalled = false
        let machine = StateMachine<MyState, MyEvent>(state: .State0) { machine in

            // add 0 => 1 => 2
            machine.addRoutes(event: .Event0, transitions: [
                .State0 => .State1,
                .State1 => .State2,
            ])
            machine.addHandler(.Any => .Any) { _ in
                handlerCalled = true
            }
        }

        // initial
        XCTAssertEqual(machine.state, MyState.State0)

        // tryEvent
        machine <-! .Event0
        XCTAssertEqual(machine.state, MyState.State1)

        // tryEvent
        machine <-! .Event0
        XCTAssertEqual(machine.state, MyState.State2)

        // tryEvent (fails)
        machine <-! .Event0
        XCTAssertEqual(machine.state, MyState.State2, "Event0 doesn't have 2 => Any")

        XCTAssertTrue(handlerCalled) // Failure
    }

SwiftLint config error on carthage build

On carthage build or carthage update , this library currently emits swiftlint config errors.

$ carthage build --platform tvos SwiftState
*** xcodebuild output can be found in /var/folders/tr/3lftss053tqdh731cgt0w1p00000gn/T/carthage-xcodebuild.eJzwNs.log
*** Building scheme "SwiftState" in SwiftState.xcodeproj
2016-03-05 01:41:43.498 xcodebuild[19404:463121] ### Failed to load Addressbook class CNContactNameFormatter
2016-03-05 01:41:48.926 xcodebuild[19466:463272] ### Failed to load Addressbook class CNContactNameFormatter
configuration error: 'variable_name_min_length' is not a valid rule identifier
configuration error: 'variable_name_min_length' is not a valid rule identifier

IMO, a library should not be running SwiftLint in its Build Phase.

Request for improvement

Currently, there are 2 types of handlers in StateMachine:

  • based on event transition
  • based on state transition

Suppose I want to have a handler that does not care how I got to from one state to another - it could be via state -> state transition or based on some event.

It would be nice if there was a way to handle this scenario with a single handler.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.