Skip to content

Instantly share code, notes, and snippets.

@paulcadman
Last active July 14, 2018 19:25
Show Gist options
  • Save paulcadman/f211e98f84ea3c60aba022eb44d724ea to your computer and use it in GitHub Desktop.
Save paulcadman/f211e98f84ea3c60aba022eb44d724ea to your computer and use it in GitHub Desktop.
FSM in swift
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