メルカリ アッテを支えるオートマトン/monadic-mealy-machine

5b42fb9892c0404457899919d583e087?s=47 Orakaro
October 10, 2017

メルカリ アッテを支えるオートマトン/monadic-mealy-machine

「メルカリアッテ」アプリのリニューアルの一つとして会員登録フローを実装し直しました。Facebook、メルカリ連携など色々な機能が登場し複雑なフローに変更した上で、仕様の調整も頻繁に行われ、柔軟な設計と実装手法が求められていました。

本トークにはその問題を解決するために状態管理技術のステートマシン(オートマトン)を活かし、柔軟性の高い且つテストしやすい実装をどのように実現したかの話を紹介します。

デモ:https://github.com/orakaro/MonadicMealyMachine

5b42fb9892c0404457899919d583e087?s=128

Orakaro

October 10, 2017
Tweet

Transcript

  1. ϝϧΧϦ ΞοςΛࢧ͑Δ ΦʔτϚτϯ Vu Nhat Minh / @orakaro Souzoh iOS

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

  3. Ξος͕औΓ૊ΜͰ͍Δ͜ͱ • ϑϧRxSwift • Driverʹੵۃతʹஔ͖׵͑Δ • ViewModelͷΠϯϓοτɺΞ΢τϓοτԽ • ܧঝΑΓίϯϙδγϣϯ •

    ΦʔτϚτϯ
  4. εςʔτϚγϯͷ͓͞Β͍ • ༗ݶΦʔτϚτϯ • ભҠؔ਺ͷΈ (s, a) -> s •

    Mealy Machine • ભҠؔ਺ (s, a) -> s • ग़ྗؔ਺ (s, a) -> b • Moore Machine • ભҠؔ਺ (s, a) -> s • ग़ྗؔ਺ s -> b
  5. ༗ݶΦʔτϚτϯ • ༗ݶΦʔτϚτϯ • ભҠؔ਺ͷΈ (s, a) -> s •

    Mealy Machine • ભҠؔ਺ (s, a) -> s • ग़ྗؔ਺ (s, a) -> b • Moore Machine • ભҠؔ਺ (s, a) -> s • ग़ྗؔ਺ s -> b ঢ়ଶΛ؅ཧ͍ͨ࣌͠ʹ׆༻͢΂͖
  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. ݈શͳίʔυϕʔεͷͨΊʹ ڊେif elseΛʢͰ͖Ε͹ʣॻ͔ͳ͍ άϩʔόϧม਺ΛʢͰ͖Ε͹ʣ࢖Θͳ͍ ؔ਺ ΛʢͰ͖Δ͚ͩʣ७ਮؔ਺ʹ͍ͨ͠

  11. ձһొ࿥ϑϩʔ ʁ ʁ ʁ ʁ ʁ

  12. ձһొ࿥ϑϩʔ Emailొ࿥͍ͤͨ͞ Facebookೝূ͍ͤͨ͞ ϝϧΧϦొ࿥ࡁΈͳΒϫϯΫϦοΫ ύεϫʔυ࠶ઃఆ ϩάΠϯ͍ͤͨ͞

  13. ձһొ࿥ϑϩʔ

  14. ొ࿥ঢ়ଶ൑ఆ

  15. Facebookೝূ

  16. ϝʔϧϩάΠϯ

  17. ձһొ࿥ʢΠϝʔδʣ 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. ભҠϩδοΫ͸υϝΠϯ஌ࣝ Ϟσϧ૚Ͱ؅ཧ͢΂͖ MVC / MVP / MVVM

  20. ΋͸΍֤ը໘͕ εςʔτΛදݱ͍ͯ͠Δͷ͡Ό

  21. ΠϕϯτʹΑͬͯભҠ 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 { //... } } ΦʔτϚτϯ
  23. TransitionΛఆٛ struct Transition<S, E> { let from: S let to:

    S let by: E } from to by State State Event
  24. 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. 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 } } ࣙॻܕΛ࢖ͬͯભҠΛ؅ཧ͢Δ
  26. εςʔτͱΠϕϯτΛఆٛ 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. ભҠάϥϑΛఆٛ 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. ભҠϩδοΫ͚ͩςετͰ͖Δ 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. ·ͩ໰୊͕͋Δ

  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 { //... } } ͜͜ͰViewControllerΛੜ੒͢Δͷʁ
  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 } ڊେ if elseͱେͯ͠มΘΜͳ͍…
  33. ભҠάϥϑΛఆٛ 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. DefaultEvent ͬͯԿʁ • Swift 3 ͷassociated value enumʹ͸σϑΥϧτ஋ઃఆͰ͖ͳ͍ • Acceptedͷεςʔλε͕ͩSwift4ʹೖ͍ͬͯͳ͍

    Proposal: SE-0155 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. enumͷassociated value͕optionalܕ 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. ΦʔτϚτϯ + FP

  37. ભҠؔ਺ (aka Reducer) • ༗ݶΦʔτϚτϯ • ભҠؔ਺ͷΈ (s, a) ->

    s • Mealy Machine • ભҠؔ਺ (s, a) -> s • ग़ྗؔ਺ (s, a) -> b • Moore Machine • ભҠؔ਺ (s, a) -> s • ग़ྗؔ਺ s -> b
  38. ભҠؔ਺ (s, a) -> s ग़ྗؔ਺ (s, a) -> b

    Mealy Machine / Moore Machine ભҠؔ਺ (s, a) -> (s, b) ભҠؔ਺ (s, a) -> s ग़ྗؔ਺ s -> b ભҠؔ਺ s -> (a -> s, b)
  39. ભҠؔ਺ (s, a) -> s ग़ྗؔ਺ (s, a) -> b

    Mealy Machine vs Elm ભҠؔ਺ (s, a) -> (s, b) Elm Architecture fun update(state: State, action: Action): Pair<State, Command>
  40. ભҠؔ਺ (s, a) -> s ग़ྗؔ਺ (s, a) -> b

    Mealy Machine ભҠؔ਺ (s, a) -> (s, b) ભҠؔ਺ a -> s -> (s, b)
  41. ભҠؔ਺ (s, a) -> s ग़ྗؔ਺ (s, a) -> b

    Mealy Machine ભҠؔ਺ (s, a) -> (s, b) ભҠؔ਺ a -> s -> (s, b) Stateful Computation !!
  42. ঢ়ଶ෇͖ͷܭࢉʢStateful Computationʣ ঢ়ଶ෇͖ͷܭࢉͱ͸ɺ͋Δঢ়ଶΛऔͬͯɺߋ৽͞Εͨঢ়ଶͱҰॹʹܭࢉ݁ՌΛ ฦؔ͢਺ͱͯ͠දݱͰ͖Δɿ 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. εςʔτϞφυ ঢ়ଶ෇͖ͷܭࢉͱ͸ɺ͋Δঢ়ଶΛऔͬͯɺߋ৽͞Εͨঢ়ଶͱҰॹʹܭࢉ݁ՌΛ ฦؔ͢਺ͱͯ͠දݱͰ͖Δɿ s -> (s, a) 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) } } }
  44. Ϟφυ = flatmappable ܕ Image credit: Functors, Applicatives, And Monads

    In Pictures
  45. Swift ͷϞφυ 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. ભҠؔ਺ (s, a) -> s ग़ྗؔ਺ (s, a) -> b

    Mealy Machine ભҠؔ਺ (s, a) -> (s, b) ભҠؔ਺ a -> s -> (s, b) ભҠؔ਺ a -> State[s, b] εςʔτϞφυ
  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) } } ɹભҠؔ਺ A -> State[S, B] A:ΠϯϓοτܕɺB: Ξ΢τϓοτܕ A = ΠϕϯτenumɺB = UIViewController
  48. εςʔτͱΠϕϯτΛఆٛ 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) } OptionalͰ͸ͳ͍ʂʂ
  49. ભҠάϥϑΛఆٛ 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. ΦʔτϚτϯ + FRP

  51. ΦʔτϚτϯ + FRP • ը໘಺ͷUIมԽ΋εςʔτʹදݱ͢Δ • ΠϕϯτΛετϦʔϜʹද͢ • ReactiveSwift •

    inamiy/ReactiveAutomaton • RxSwift • kzaher/RxFeedback
  52. ·ͱΊ • ΦʔτϚτϯͷԠ༻ • ༗ݶΦʔτϚτϯ • Mealy Machine • Moore

    Machine • ঢ়ଶ؅ཧʹ༏ΕΔ • ؆୯ͳભҠॲཧͳΒFSM • Mealy Machine = State Monad
  53. https://github.com/orakaro/MonadicMealyMachine Twitter / Github: @orakaro Q&A