Mercari Atte and Automaton

5b42fb9892c0404457899919d583e087?s=47 Orakaro
October 12, 2017

Mercari Atte and Automaton

Talk in Souzoh iOS 2017, English version.

Demo: https://github.com/orakaro/MonadicMealyMachine

5b42fb9892c0404457899919d583e087?s=128

Orakaro

October 12, 2017
Tweet

Transcript

  1. Mercari Atte and Automaton Vu Nhat Minh / @orakaro Souzoh

    iOS Talk
  2. Mercari Atte Scala / Swift Engineer / @orakaro

  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
  11. Signup Flow ʁ ʁ ʁ ʁ ʁ

  12. Signup Flow Signup by email Facebook login One click signup

    by second party app Reset Password Login by Email
  13. Signup Flow

  14. Signup condition

  15. Facebook Login

  16. Login by email

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

    Mercari AtteLogin Facebook Email Login Register With Mercari Confirm SMS Home PassCode Email SignUp Reset Password
  18. Root Register Root Profile Register Start With Mercari AtteLogin Facebook

    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

  21. Transition by event Root Register Root Atte Login Start With

    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
  26. Define State and Event enum RegistrationState { case root case

    registerRoot case profileRegister case startWithMercari case emailLogin case atteLogin case home } enum RegistrationEvent: Hashable { case showRegisterRoot(loginMode: RootViewController.LoginMode?) case registerWithFacebook(profile: FacebookProfile?) case startWithMercari(resource: MercariIdResource?, tokenPair: TokenPair?, showsCloseButton: Bool) case loginWithEmail(showsCancelButton: Bool) case loginWithAtte(resource: MercariIdResource?, tokenPair: TokenPair?) case loginAndShowHome(tokenPair: TokenPair?, userId: Int64, hasMercariItem: Bool) }
  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
  30. still have problems

  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) }
  35. Optional type associated value enum RegistrationEvent: Hashable { case showRegisterRoot(loginMode:

    RootViewController.LoginMode?) case registerWithFacebook(profile: FacebookProfile?) case startWithMercari(resource: MercariIdResource?, tokenPair: TokenPair?, showsCloseButton: Bool) case loginWithEmail(showsCancelButton: Bool) case loginWithAtte(resource: MercariIdResource?, tokenPair: TokenPair?) case loginAndShowHome(tokenPair: TokenPair?, userId: Int64, hasMercariItem: Bool) } Optional
  36. Automaton + FP

  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
  45. Swift Monads Maybe Monad Either Monad Reader Monad Observable Monad

    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
  50. Automaton + FRP

  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
  53. https://github.com/orakaro/MonadicMealyMachine Twitter / Github: @orakaro Q&A