Orakaro
October 12, 2017
2.3k

# Mercari Atte and Automaton

Talk in Souzoh iOS 2017, English version.

October 12, 2017

## Transcript

iOS Talk

3. ### Mercari Atte iOS development • Full RxSwift • Aggressive use

of Driver • Define input/output type for ViewModel • Composition over Inheritance • Automaton
4. ### State Machine variations • Finite state machine • Transition function

(s, a) -> s • Mealy Machine • Transition function (s, a) -> s • Output function (s, a) -> b • Moore Machine • Transition function (s, a) -> s • Output function s -> b
5. ### Finite State Machine • Finite state machine • Transition function

(s, a) -> s • Mealy Machine • Transition function (s, a) -> s • Output function (s, a) -> b • Moore Machine • Transition function (s, a) -> s • Output function s -> b design pattern for state machines
6. ### Binary Gap A binary gap within a positive integer N

is any maximal sequence of consecutive zeros that is surrounded by ones at both ends in the binary representation of N. Ex: 9 = 1001 → 2
7. ### A binary gap within a positive integer N is any

maximal sequence of consecutive zeros that is surrounded by ones at both ends in the binary representation of N. Ex: 9 = 1001 → 2 Started Zero One Binary Gap
8. ### Visual Format Language class func constraints(withVisualFormat format: String, options opts:

NSLayoutFormatOptions = [], metrics: [String : Any]?, views: [String : Any]) -> [NSLayoutConstraint] "H:|-15-[iconImageView(30)]-[appNameLabel]-[skipButton]-15-|"
9. ### Visual Format Language class func constraints(withVisualFormat format: String, options opts:

NSLayoutFormatOptions = [], metrics: [String : Any]?, views: [String : Any]) -> [NSLayoutConstraint] "H:|-15-[iconImageView(30)]-[appNameLabel]-[skipButton]-15-|" Direction Start Line Number Label [ ] ( ) End
10. ### For a “healthy” codebase Try to avoid large if-else block

Try to avoid global variables Try to write pure functions

17. ### Signup Flow Graph Root Register Root Profile Register Start With

Email Login Register With Mercari Confirm SMS Home PassCode Email SignUp Reset Password
19. ### Transitions is Domain knowledge and should be placed at Model

layer MVC / MVP / MVVM
20. ### Each screen represents State

Mercari Atte Resource Mercari Resource
22. ### RegistrationStateViewController protocol RegistrationStateViewController { var state: RegistrationState { get }

func nextViewController(event: RegistrationEvent) -> UIViewController? } extension RegistrationStateViewController { func nextViewController(event: RegistrationEvent) -> UIViewController? { let machine = AutomatonManager.registrationMachine guard let state = machine.transition(from: state, by: event) else { return nil } switch state { //... } } Automaton
23. ### Define Transition struct Transition<S, E> { let from: S let

to: S let by: E } from to by State State Event
24. ### Implement State Machine class Automaton<S: Hashable, E: Hashable> { var

routes: [S: [E: S]] = [:] init(initialState: S, transitions: [Transition<S, E>]) { for t in transitions { addRoute(t) } } private func addRoute(_ t: Transition<S, E>) { var dict = routes[t.from] ?? [:] dict[t.by] = t.to routes[t.from] = dict } func transition(from: S, by: E) -> S? { guard let next = routes[from].flatMap({ \$0[by] }) else { return nil } return next } }
25. ### Implement State Machine class Automaton<S: Hashable, E: Hashable> { var

routes: [S: [E: S]] = [:] init(initialState: S, transitions: [Transition<S, E>]) { for t in transitions { addRoute(t) } } private func addRoute(_ t: Transition<S, E>) { var dict = routes[t.from] ?? [:] dict[t.by] = t.to routes[t.from] = dict } func transition(from: S, by: E) -> S? { guard let next = routes[from].flatMap({ \$0[by] }) else { return nil } return next } } use dictionary for storing transitions

27. ### Define transition graph let registrationGraph: [Transition<RegistrationState, RegistrationEvent>] = [ Transition(from:

.root, to: .registerRoot, by: DefaultEvent.showRegisterRoot), Transition(from: .registerRoot, to: .profileRegister, by: DefaultEvent.registerWithFacebook), Transition(from: .registerRoot, to: .startWithMercari, by: DefaultEvent.startWithMercari), Transition(from: .registerRoot, to: .emailLogin, by: DefaultEvent.loginWithEmail), Transition(from: .registerRoot, to: .atteLogin, by: DefaultEvent.loginWithAtte), Transition(from: .registerRoot, to: .home, by: DefaultEvent.loginAndShowHome), ] let registrationMachine = Automaton<RegistrationState, RegistrationEvent>( initialState: .root, transitions: registrationGraph )
28. ### Test for transition logic only describe("ΞϓϦىಈ࣌τʔΫϯ͕ଘࡏ͠ͳ͍") { it("ϝʔϧͰ৽نొ࿥͕Ͱ͖Δ") { self.tryRoute(events:

[ DefaultEvent.showRegisterRoot, DefaultEvent.registerWithEmail, DefaultEvent.showProfileRegister, DefaultEvent.confirmSMS, DefaultEvent.loginAndShowHome ]) XCTAssert(self.currentState == .home) } } //... private func tryRoute(events: [RegistrationEvent]) { for event in events { self.currentState.map { state in self.currentState = self.machine.transition(from: state, by: event) } } }
29. None

31. ### RegistrationStateViewController protocol RegistrationStateViewController { var state: RegistrationState { get }

func nextViewController(event: RegistrationEvent) -> UIViewController? } extension RegistrationStateViewController { func nextViewController(event: RegistrationEvent) -> UIViewController? { let machine = AutomatonManager.registrationMachine guard let state = machine.transition(from: state, by: event) else { return nil } switch state { //... } } Should I create ViewController here?
32. ### RegistrationStateViewController switch state { case .root: return RootViewController.make(withDependency: ()) case

.registerRoot: return RegisterRootViewController.make(withDependency: ()) case .profileRegister : switch event { case .registerWithFacebook(.some(let profile)): return ProfileRegisterViewController.make(withDependency: .init(facebookProfile: profile)) default: return nil } case .startWithMercari: switch event { case .startWithMercari(.some(let resource), .some(let token)): return StartWithMercariViewController.make(withDependency: .init(resource: resource, token: token)) default: return nil } case .emailLogin: return EmailLoginViewController.make(withDependency: ()) case .atteLogin: switch event { case .loginWithAtte(.some(let resource), .some(let token)): return AtteLoginViewController.make(withDependency: .init(resource: resource, token: token)) default: return nil } case .home: switch event { case .loginAndShowHome(.some(let token), let userId): return HomeViewController.make(withDependency: .init(token: token, userId: userId)) default: return nil } default: return nil } Not so much different from large if-else block
33. ### Define transition graph let registrationGraph: [Transition<RegistrationState, RegistrationEvent>] = [ Transition(from:

.root, to: .registerRoot, by: DefaultEvent.showRegisterRoot), Transition(from: .registerRoot, to: .profileRegister, by: DefaultEvent.registerWithFacebook), Transition(from: .registerRoot, to: .startWithMercari, by: DefaultEvent.startWithMercari), Transition(from: .registerRoot, to: .emailLogin, by: DefaultEvent.loginWithEmail), Transition(from: .registerRoot, to: .atteLogin, by: DefaultEvent.loginWithAtte), Transition(from: .registerRoot, to: .home, by: DefaultEvent.loginAndShowHome), ] let registrationMachine = Automaton<RegistrationState, RegistrationEvent>( initialState: .root, transitions: registrationGraph ) DefaultEvent !?
34. ### What is DefaultEvent? • In Swift 3 we still can

not use default value for associated value in enum • Proposal: SE-0155 Accepted status but not included in Swift 4 struct DefaultEvent { static let showRegisterRoot: RegistrationEvent = .showRegisterRoot() static let registerWithFacebook: RegistrationEvent = .registerWithFacebook(profile: nil) static let startWithMercari: RegistrationEvent = .startWithMercari(resource: nil, token: nil) static let loginWithEmail: RegistrationEvent = .loginWithEmail() static let loginWithAtte: RegistrationEvent = .loginWithAtte(resource: nil, token: nil) static let loginAndShowHome: RegistrationEvent = .loginAndShowHome(token: nil, userId: 0) }

37. ### Transition function (aka Reducer) • Finite State Machine • Transition

function (s, a) -> s • Mealy Machine • Transition function (s, a) -> s • Output function (s, a) -> b • Moore Machine • Transition function (s, a) -> s • Output function s -> b
38. ### Transition (s, a) -> s Output (s, a) -> b

Mealy Machine / Moore Machine Reducer (s, a) -> (s, b) Transition (s, a) -> s Output s -> b Reducer s -> (a -> s, b)
39. ### Mealy Machine vs Elm Elm Architecture fun update(state: State, action:

Action): Pair<State, Command> Transition (s, a) -> s Output (s, a) -> b Reducer (s, a) -> (s, b)
40. ### Mealy Machine Reducer a -> s -> (s, b) Transition

(s, a) -> s Output (s, a) -> b Reducer (s, a) -> (s, b)
41. ### Mealy Machine Reducer a -> s -> (s, b) Transition

(s, a) -> s Output (s, a) -> b Reducer (s, a) -> (s, b) Stateful Computation !!
42. ### Stateful Computation A stateful computation is a function that takes

some state and returns a value along with some new state: s -> (s, a) class State<S, A> { private let run: (S) -> (S, A) init(f: @escaping (S) -> (S, A)) { self.run = f } func run(s: S) -> (S, A) { return self.run(s) } }
43. ### State Monad extension State { func map<B>(g: @escaping (A) ->

B) -> State<S, B> { return State<S, B> { s in let (s1, val) = self.run(s: s) return (s1, g(val)) } } func flatMap<B>(g: @escaping (A) -> State<S, B>) -> State<S, B> { return State<S, B> { s in let (s1, val) = self.run(s: s) return g(val).run(s: s1) } } } A stateful computation is a function that takes some state and returns a value along with some new state: s -> (s, a)
44. ### Monad = flatmappable type Image credit: Functors, Applicatives, And Monads

In Pictures

State Monad I/O Monad … Swift Optional antitypical/Result Statically typed Dependency Injection Signal / Observable Mealy Machine
46. ### Mealy Machine Reducer a -> s -> (s, b) Transition

(s, a) -> s Output (s, a) -> b Reducer (s, a) -> (s, b) Reducer a -> State[s, b] State Monad
47. ### MonadicAutomaton class MonadicAutomaton<S, A, B> { typealias T = (A)

-> State<S, B> private var f : T init(f: @escaping T) { self.f = f } func transition(from: S, by: A) -> (S, B) { return f(by).run(s: from) } } Reducer A -> State[S, B] A:InputɺB: Output A = Event enumɺB = UIViewController
48. ### Define States and Events enum RState { case any case

root case registerRoot case profileRegister case startWithMercari case emailLogin case atteLogin case home } enum REvent { case showRegisterRoot case registerWithFacebook(profile: FacebookProfile) case startWithMercari(resource: MercariIdResource, token: String) case loginWithEmail case loginWithAtte(resource: MercariIdResource, token: String) case loginAndShowHome(token: String, userId: Int64) } custom type
49. ### Define transition graph let transitionFunc: (REvent) -> State<RState, UIViewController> =

{ event in switch event { case .registerWithFacebook(let profile): let vc = ProfileRegisterViewController.make(withDependency: .init(facebookProfile: profile)) return State<RState, UIViewController> { s in let s1: RState = s == .registerRoot ? .profileRegister : .any return (s1, vc) //... } } let registrationMachine = MonadicAutomaton<RState, REvent, UIViewController>(f : transitionFunc) A S B A S B

51. ### Automaton + FRP • State Machine for UI inside one

screen • Use stream to represent events • ReactiveSwift • inamiy/ReactiveAutomaton • RxSwift • kzaher/RxFeedback
52. ### Recap • State Machine • Finite State Machine • Mealy

Machine • Moore Machine • Design pattern for state management • Can use FSM for simple transitions • Mealy Machine = State Monad