Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

Mercari Atte Scala / Swift Engineer / @orakaro

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

εςʔτϚγϯͷ͓͞Β͍ • ༗ݶΦʔτϚτϯ • ભҠؔ਺ͷΈ (s, a) -> s • Mealy Machine • ભҠؔ਺ (s, a) -> s • ग़ྗؔ਺ (s, a) -> b • Moore Machine • ભҠؔ਺ (s, a) -> s • ग़ྗؔ਺ s -> b

Slide 5

Slide 5 text

༗ݶΦʔτϚτϯ • ༗ݶΦʔτϚτϯ • ભҠؔ਺ͷΈ (s, a) -> s • Mealy Machine • ભҠؔ਺ (s, a) -> s • ग़ྗؔ਺ (s, a) -> b • Moore Machine • ભҠؔ਺ (s, a) -> s • ग़ྗؔ਺ s -> b ঢ়ଶΛ؅ཧ͍ͨ࣌͠ʹ׆༻͢΂͖

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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-|"

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

݈શͳίʔυϕʔεͷͨΊʹ ڊେif elseΛʢͰ͖Ε͹ʣॻ͔ͳ͍ άϩʔόϧม਺ΛʢͰ͖Ε͹ʣ࢖Θͳ͍ ؔ਺ ΛʢͰ͖Δ͚ͩʣ७ਮؔ਺ʹ͍ͨ͠

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

ձһొ࿥ϑϩʔ

Slide 14

Slide 14 text

ొ࿥ঢ়ଶ൑ఆ

Slide 15

Slide 15 text

Facebookೝূ

Slide 16

Slide 16 text

ϝʔϧϩάΠϯ

Slide 17

Slide 17 text

ձһొ࿥ʢΠϝʔδʣ Root Register Root Profile Register Start With Mercari AtteLogin Facebook Email Login Register With Mercari Confirm SMS Home PassCode Email SignUp Reset Password

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

ભҠϩδοΫ͸υϝΠϯ஌ࣝ Ϟσϧ૚Ͱ؅ཧ͢΂͖ MVC / MVP / MVVM

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

ΠϕϯτʹΑͬͯભҠ Root Register Root Atte Login Start With Mercari Atte Resource Mercari Resource

Slide 22

Slide 22 text

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 { //... } } ΦʔτϚτϯ

Slide 23

Slide 23 text

TransitionΛఆٛ struct Transition { let from: S let to: S let by: E } from to by State State Event

Slide 24

Slide 24 text

State MachineΛ࣮૷ class Automaton { var routes: [S: [E: S]] = [:] init(initialState: S, transitions: [Transition]) { for t in transitions { addRoute(t) } } private func addRoute(_ t: Transition) { 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 } }

Slide 25

Slide 25 text

State MachineΛ࣮૷ class Automaton { var routes: [S: [E: S]] = [:] init(initialState: S, transitions: [Transition]) { for t in transitions { addRoute(t) } } private func addRoute(_ t: Transition) { 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 } } ࣙॻܕΛ࢖ͬͯભҠΛ؅ཧ͢Δ

Slide 26

Slide 26 text

εςʔτͱΠϕϯτΛఆٛ 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) }

Slide 27

Slide 27 text

ભҠάϥϑΛఆٛ let registrationGraph: [Transition] = [ 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( initialState: .root, transitions: registrationGraph )

Slide 28

Slide 28 text

ભҠϩδοΫ͚ͩςετͰ͖Δ 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) } } }

Slide 29

Slide 29 text

No content

Slide 30

Slide 30 text

·ͩ໰୊͕͋Δ

Slide 31

Slide 31 text

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Λੜ੒͢Δͷʁ

Slide 32

Slide 32 text

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ͱେͯ͠มΘΜͳ͍…

Slide 33

Slide 33 text

ભҠάϥϑΛఆٛ let registrationGraph: [Transition] = [ 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( initialState: .root, transitions: registrationGraph ) DefaultEvent !?

Slide 34

Slide 34 text

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) }

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

ΦʔτϚτϯ + FP

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

ભҠؔ਺ (s, a) -> s ग़ྗؔ਺ (s, a) -> b Mealy Machine ભҠؔ਺ (s, a) -> (s, b) ભҠؔ਺ a -> s -> (s, b) Stateful Computation !!

Slide 42

Slide 42 text

ঢ়ଶ෇͖ͷܭࢉʢStateful Computationʣ ঢ়ଶ෇͖ͷܭࢉͱ͸ɺ͋Δঢ়ଶΛऔͬͯɺߋ৽͞Εͨঢ়ଶͱҰॹʹܭࢉ݁ՌΛ ฦؔ͢਺ͱͯ͠දݱͰ͖Δɿ s -> (s, a) class State { 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) } }

Slide 43

Slide 43 text

εςʔτϞφυ ঢ়ଶ෇͖ͷܭࢉͱ͸ɺ͋Δঢ়ଶΛऔͬͯɺߋ৽͞Εͨঢ়ଶͱҰॹʹܭࢉ݁ՌΛ ฦؔ͢਺ͱͯ͠දݱͰ͖Δɿ s -> (s, a) extension State { func map(g: @escaping (A) -> B) -> State { return State { s in let (s1, val) = self.run(s: s) return (s1, g(val)) } } func flatMap(g: @escaping (A) -> State) -> State { return State { s in let (s1, val) = self.run(s: s) return g(val).run(s: s1) } } }

Slide 44

Slide 44 text

Ϟφυ = flatmappable ܕ Image credit: Functors, Applicatives, And Monads In Pictures

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

ભҠؔ਺ (s, a) -> s ग़ྗؔ਺ (s, a) -> b Mealy Machine ભҠؔ਺ (s, a) -> (s, b) ભҠؔ਺ a -> s -> (s, b) ભҠؔ਺ a -> State[s, b] εςʔτϞφυ

Slide 47

Slide 47 text

MonadicAutomaton class MonadicAutomaton { typealias T = (A) -> State 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

Slide 48

Slide 48 text

εςʔτͱΠϕϯτΛఆٛ 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Ͱ͸ͳ͍ʂʂ

Slide 49

Slide 49 text

ભҠάϥϑΛఆٛ let transitionFunc: (REvent) -> State = { event in switch event { case .registerWithFacebook(let profile): let vc = ProfileRegisterViewController.make(withDependency: .init(facebookProfile: profile)) return State { s in let s1: RState = s == .registerRoot ? .profileRegister : .any return (s1, vc) //... } } let registrationMachine = MonadicAutomaton(f : transitionFunc) A S B A S B

Slide 50

Slide 50 text

ΦʔτϚτϯ + FRP

Slide 51

Slide 51 text

ΦʔτϚτϯ + FRP • ը໘಺ͷUIมԽ΋εςʔτʹදݱ͢Δ • ΠϕϯτΛετϦʔϜʹද͢ • ReactiveSwift • inamiy/ReactiveAutomaton • RxSwift • kzaher/RxFeedback

Slide 52

Slide 52 text

·ͱΊ • ΦʔτϚτϯͷԠ༻ • ༗ݶΦʔτϚτϯ • Mealy Machine • Moore Machine • ঢ়ଶ؅ཧʹ༏ΕΔ • ؆୯ͳભҠॲཧͳΒFSM • Mealy Machine = State Monad

Slide 53

Slide 53 text

https://github.com/orakaro/MonadicMealyMachine Twitter / Github: @orakaro Q&A