Last active
July 14, 2018 19:25
-
-
Save paulcadman/f211e98f84ea3c60aba022eb44d724ea to your computer and use it in GitHub Desktop.
FSM in swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
struct BasketItem { | |
var name: String | |
var price: Price | |
} | |
extension BasketItem: CustomStringConvertible { | |
var description: String { | |
return "Item \(self.name) \(self.price)" | |
} | |
} | |
typealias Card = String | |
typealias Price = Decimal | |
typealias OrderId = String | |
struct NonEmpty<C: Collection> { | |
var head: C.Element | |
var tail: C | |
init(_ head: C.Element, _ tail: C) { | |
self.head = head | |
self.tail = tail | |
} | |
} | |
extension NonEmpty: CustomStringConvertible { | |
var description: String { | |
return "\(self.head)\(self.tail)" | |
} | |
} | |
extension NonEmpty where C: RangeReplaceableCollection { | |
init(_ head: C.Element, _ tail: C.Element...) { | |
self.head = head | |
self.tail = C(tail) | |
} | |
} | |
extension NonEmpty: Collection { | |
enum Index: Comparable { | |
case head | |
case tail(C.Index) | |
static func < (lhs: Index, rhs: Index) -> Bool { | |
switch (lhs, rhs) { | |
case (.head, .tail): | |
return true | |
case (.tail, .head): | |
return false | |
case (.head, .head): | |
return false | |
case let (.tail(l), .tail(r)): | |
return l < r | |
} | |
} | |
} | |
var startIndex: Index { | |
return .head | |
} | |
var endIndex: Index { | |
return .tail(self.tail.endIndex) | |
} | |
subscript(position: Index) -> C.Element { | |
switch position { | |
case .head: | |
return self.head | |
case let .tail(index): | |
return self.tail[index] | |
} | |
} | |
func index(after i: Index) -> Index { | |
switch i { | |
case .head: | |
return .tail(self.tail.startIndex) | |
case let .tail(index): | |
return .tail(self.tail.index(after: index)) | |
} | |
} | |
} | |
extension NonEmpty where C: RangeReplaceableCollection { | |
mutating func append(_ newElement: C.Element) { | |
self.tail.append(newElement) | |
} | |
} | |
typealias NonEmptySet<A> = NonEmpty<Set<A>> where A: Hashable | |
typealias NonEmptyArray<A> = NonEmpty<[A]> | |
enum Pair<T, WORLD> { | |
case cons (T, WORLD) // (value, outside) | |
} | |
// Outside the computer | |
typealias WORLD = Any | |
// IO Monad Instance (a.k.a IO Action) | |
typealias IO<T> = (WORLD) -> Pair<T, WORLD> | |
// MARK: Basic monad functions | |
func unit<T>(_ value: T) -> IO<T> { | |
return { world in | |
return .cons(value, world) | |
} | |
} | |
func flatMap<A, B>(_ monadInstance: @escaping IO<A>) -> (@escaping (A) -> IO<B>) -> IO<B> { | |
return { (actionAB: @escaping (A) -> IO<B>) in | |
return { (world) in | |
let newPair = monadInstance(world) | |
switch newPair { | |
case .cons(let value, let newWorld): | |
return actionAB(value)(newWorld) //Pair<B, WORLD> | |
} | |
}// as (WORLD) -> Pair<B, WORLD> | |
} | |
} | |
protocol FSM { | |
associatedtype State | |
associatedtype Event | |
func reduce(state: State, event: Event) -> IO<State> | |
} | |
extension FSM { | |
func run(with events: [Event], startingWith initialState: State) -> IO<State> { | |
return events.reduce(unit(initialState)) { acc, nextEvent in | |
return flatMap(acc)( { prevState in | |
self.reduce(state: prevState, event: nextEvent) | |
}) | |
} | |
} | |
} | |
struct FSMWithLogging<T: FSM>: FSM { | |
var baseFSM: T | |
func reduce(state: T.State, event: T.Event) -> IO<T.State> { | |
return flatMap(baseFSM.reduce(state: state, event: event))({ newState in | |
print(" \(state) * \(event) --> \(newState) ") | |
return unit(newState) | |
}) | |
} | |
} | |
enum CheckoutState { | |
case noItems | |
case hasItems(NonEmptyArray<BasketItem>) | |
case noCard(NonEmptyArray<BasketItem>) | |
case cardSelected(NonEmptyArray<BasketItem>, Card) | |
case cardConfirmed(NonEmptyArray<BasketItem>, Card) | |
case orderPlaced | |
} | |
enum CheckoutEvent { | |
case select(BasketItem) | |
case checkout | |
case selectCard(Card) | |
case confirm | |
case placeOrder | |
case cancel | |
} | |
func charge(card: Card, by price: Price) -> IO<Void> { | |
print("Charging card \(card) with $ \(price)") | |
return unit(Void()) | |
} | |
func calcaulatePrice(items: NonEmptyArray<BasketItem>) -> Price { | |
return items.map { $0.price }.reduce(0, +) | |
} | |
struct Checkout: FSM { | |
typealias State = CheckoutState | |
typealias Event = CheckoutEvent | |
func reduce(state: CheckoutState, event: CheckoutEvent) -> IO<CheckoutState> { | |
switch (state, event) { | |
case (.noItems, .select(let item)): | |
return unit(.hasItems(NonEmptyArray(item))) | |
case (.hasItems(let items), .select(let item)): | |
var newItems = items | |
newItems.append(item) | |
return unit(.hasItems(newItems)) | |
case (.hasItems(let items), .checkout): | |
return unit(.noCard(items)) | |
case (.noCard(let items), .selectCard(let card)): | |
return unit(.cardSelected(items, card)) | |
case (.cardSelected(let items, let card), .confirm): | |
return unit(.cardConfirmed(items, card)) | |
case (.noCard(let items), .cancel): | |
return unit(.hasItems(items)) | |
case (.cardSelected(let items, _), .cancel): | |
return unit(.hasItems(items)) | |
case (.cardConfirmed(let items, _), .cancel): | |
return unit(.hasItems(items)) | |
case (_, .cancel): | |
return unit(state) | |
case (.cardConfirmed(let items, let card), .placeOrder): | |
return flatMap(charge(card: card, by: calcaulatePrice(items: items)))( { _ in return unit(.orderPlaced) }) | |
case (_, _): | |
return unit(state) | |
} | |
} | |
} | |
let fsmWithLogging = FSMWithLogging(baseFSM: Checkout()) | |
fsmWithLogging.run( | |
with: [.select(BasketItem(name: "potatoes", price: 1.12)), | |
.select(BasketItem(name: "fish", price: 7.51)), | |
.checkout, | |
.selectCard("0000-0000-0000-0000"), | |
.confirm, | |
.placeOrder], | |
startingWith: .noItems)(Void()) | |
//noItems * select(Item potatoes 1.1200000000000002048) --> hasItems(Item potatoes 1.1200000000000002048[]) | |
//hasItems(Item potatoes 1.1200000000000002048[]) * select(Item fish 7.51) --> hasItems(Item potatoes 1.1200000000000002048[Item fish 7.51]) | |
//hasItems(Item potatoes 1.1200000000000002048[Item fish 7.51]) * checkout --> noCard(Item potatoes 1.1200000000000002048[Item fish 7.51]) | |
//noCard(Item potatoes 1.1200000000000002048[Item fish 7.51]) * selectCard("0000-0000-0000-0000") --> cardSelected(Item potatoes 1.1200000000000002048[Item fish 7.51], "0000-0000-0000-0000") | |
//cardSelected(Item potatoes 1.1200000000000002048[Item fish 7.51], "0000-0000-0000-0000") * confirm --> cardConfirmed(Item potatoes 1.1200000000000002048[Item fish 7.51], "0000-0000-0000-0000") | |
//Charging card 0000-0000-0000-0000 with $ 8.6300000000000002048 | |
//cardConfirmed(Item potatoes 1.1200000000000002048[Item fish 7.51], "0000-0000-0000-0000") * placeOrder --> orderPlaced |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment