Scaling iOS Development with FRP

0ebf471a3ae8df42a84f93a7efbbdbd0?s=47 Ash Furrow
February 23, 2016

Scaling iOS Development with FRP

0ebf471a3ae8df42a84f93a7efbbdbd0?s=128

Ash Furrow

February 23, 2016
Tweet

Transcript

  1. Scaling iOS Development Using

  2. None
  3. None
  4. None
  5. BNE ADD1 BRA NONE1 ADD1 INY ;increment our counter of

    1's NONE1 LDAA TEMP ;reload accumulator A LSL MASK ;Shift the mask's 1 bit left BNE LOOP1 ;If we haven't finished our loop, branch LDAA #$01 ;load new mask into A STAA MASK ;store the reset mask into MASK TSX ;pull of return address and store in X PULA ;pull off A STAA TEMP ;store the value into temp TXS ;push return address back onto the stack LOOP2 LDAA TEMP ;Load A into TEMP ANDA MASK ;logical AND MASK with A BNE ADD2 ;add one if we need to BRA NONE2 ADD2 INY ;increment our counter of 1's NONE2 LDAA TEMP LSL MASK ;shift our mask left by one BNE LOOP2 ;loop back until we've exhausted positions
  6. None
  7. Agenda 1. Simplicity is the goal of software development 2.

    Statefulness is an enemy of simplicity 3. FRP conceals state rather than eliminate it
  8. None
  9. Simplicity is the goal of programming.

  10. Obvious? Maybe.

  11. What is simple? • Focused • Uncombined • Has one

    role, one purpose, one concept, etc… • Simple is not necessarily easy
  12. let numbers = [1, 5, 4, -1, 7] var sum

    = 0 for var i = 0; i < numbers.count; i++ { sum += numbers[i] } print(sum)
  13. let numbers = [1, 5, 4, -1, 7] let sum

    = numbers.reduce(0, combine: +) print(sum)
  14. Is that simpler? Is that easier?

  15. Complexity: incidental vs. essential.

  16. Minimize incidental complexity, maximize simplicity.

  17. Easy doesn’t scale.

  18. Choose : Easy to write? Or simple to read?

  19. –Harold Abelson, SICP “Programs must be written for people to

    read, and only incidentally for machines to execute.”
  20. Example • Form validation • Submit button is disabled until

    data entry is valid • Also disabled while the form is being submitted
  21. formDidChange(form: Form) { submitButton.enabled = validate(form) } submitForm(form: Form) {

    submitButton.enabled = false network.submitForm(form) { submitButton.enabled = true } }
  22. formDidChange(form: Form) { submitButton.enabled = validate(form) } submitForm(form: Form) {

    submitButton.enabled = false network.submitForm(form) { submitButton.enabled = validate(form) } }
  23. formDidChange(form: Form) { submitButton.enabled = validate(form) } submitForm(form: Form) {

    submitButton.enabled = false network.submitForm(form) { // Wait, do we reset the form first? Maybe. submitButton.enabled = validate(form) } }
  24. let handler = FormHandler(callback: { enabled in self.submitButton.enabled = enabled

    } formDidChange(form: Form) { handler.update(form) } submitForm(form: Form) { handler.submit(form) }
  25. State leads to incidental complexity.

  26. Make things simple by avoiding state.

  27. None
  28. What is state, even!?

  29. Current values of any accessible variables. (-ish)

  30. # of bools 1 2 3 4 5 possible states

    2 4 8 16 32
  31. State changes over time.

  32. Managing state is incidental complexity.

  33. class FormHandler { let callback: Bool -> Void private let

    submitter = FormSubmitter() private let validator = FormValidator() private let resetter = FormResetter() init(callback: Bool -> Void) { self.callback = callback } }
  34. OMG! All those classes!

  35. Relaaaaaaax.

  36. private func invokeCallback(form: Form) { callback( submitter.isSubmitting == false &&

    validator.validate(form) == true ) }
  37. func validate(form: Form) { invokeCallback(form) }

  38. func submit(form: Form) { submitter.submit(form, startedSubmission: { self.invokeCallback(form) }, completedSubmission:

    { self.resetter.resetIfNecessary(form) self.invokeCallback(form) }) }
  39. func submit(form: Form) { submitter.submit(form, startedSubmission: { self.invokeCallback(form) }, completedSubmission:

    { self.resetter.resetIfNecessary(form) self.invokeCallback(form) }) }
  40. func submit(form: Form) { submitter.submit(form, startedSubmission: { self.invokeCallback(form) }, completedSubmission:

    { self.resetter.resetIfNecessary(form) self.invokeCallback(form) }) }
  41. How much state is that?

  42. None.

  43. struct FormHandler { let callback: Bool -> Void private let

    submitter = FormSubmitter() private let validator = FormValidator() private let resetter = FormResetter() ...
  44. State contained within the submitter.

  45. This is not pedagogical.

  46. I would ship this.

  47. (After writing unit tests.)

  48. Simpler view controller.

  49. Isolated state.

  50. So. State doesn’t scale.

  51. Why?

  52. Because… Brains don’t scale.

  53. None
  54. So state is bad…

  55. Let’s remove it!

  56. Not so fast.

  57. Abstraction.

  58. –Edsger Dijkstra “The purpose of abstraction is not to be

    vague, but to create a new semantic level in which one can be absolutely precise.”
  59. var loginStatus = UserStatus.NotLoggedIn ... loginStatus = .LoggedIn

  60. let loginStatus = Variable(UserStatus.NotLoggedIn) ... loginStatus.value = .LoggedIn

  61. Okay, but why?

  62. class LoginNetworkModel { private let _loginStatus = Variable(UserStatus.NotLoggedIn) var loginStatus:

    Observable<UserStatus> { return _loginStatus.asObservable() } ... }
  63. class LoginNetworkModel { private let _loginStatus = Variable(UserStatus.NotLoggedIn) var loginStatus:

    Observable<UserStatus> { return _loginStatus.asObservable() } ... }
  64. class LoginNetworkModel { private let _loginStatus = Variable(UserStatus.NotLoggedIn) var loginStatus:

    Observable<UserStatus> { return _loginStatus.asObservable() } ... }
  65. Conceals state.

  66. loginNetworkModel .loginStatus .subscribeNext { [weak self] result in switch result

    { case .NotLoggedIn: self?.failedToLogin() case .LoggedIn: self?.welcomeUser() }
 }
  67. loginNetworkModel .loginStatus .subscribeNext { [weak self] result in switch result

    { case .NotLoggedIn: self?.failedToLogin() case .LoggedIn: self?.welcomeUser() }
 }
  68. loginNetworkModel .loginStatus .subscribeNext { [weak self] result in switch result

    { case .NotLoggedIn: self?.failedToLogin() case .LoggedIn: self?.welcomeUser() }
 }
  69. No access to state.

  70. Reacts to state changes.

  71. Functional.

  72. Why use a Variable?

  73. class LoginNetworkModel { func login(username: String, password: String) -> Observable<UserStatus>

    { return network .authUsername(username, password: password) .map { response in ... } } }
  74. class LoginNetworkModel { func login(username: String, password: String) -> Observable<UserStatus>

    { return network .authUsername(username, password: password) .map { $0.statusCode } .map { statusCode -> UserStatus in statusCode == 200 ? .LoggedIn : .NotLoggedIn } } }
  75. Decreased incidental complexity.

  76. Network access in an Observable.

  77. loginNetworkModel .login("ashfurrow", password: ...) .subscribeNext { [weak self] result in

    switch result { case .NotLoggedIn: self?.failedToLogin() case .LoggedIn: self?.welcomeUser() }
 }
  78. Seems like magic!

  79. Sure does.

  80. Subscribing “starts” the Observable. (-ish)

  81. Subscribing returns a Disposable.

  82. Tie disposal to object deallocation.

  83. class LoginViewController: UIViewController { let disposeBag = DisposeBag() ... loginNetworkModel

    .login("ashfurrow", password: ...) .subscribeNext { ... } .addDisposableTo(self.disposeBag) ...
  84. OK, cool! What else?

  85. Requirements Change! • View controller needs access to full user

    info • Let’s modify our network model
  86. class LoginNetworkModel { func login(username: String, password: String) -> Observable<User>

    { return network .authUsername(username, password: password) .filterSuccessfulStatusCodes() .mapToJSON() .mapToObject(User) } }
  87. loginNetworkModel .login("ashfurrow", password: …) .onError { [weak self] error in

    self?.handleFailure(error) } .subscribeNext { ... } .addDisposableTo(self.disposeBag)
  88. That’s really neat!

  89. network .accessToken // Observable<AccessToken> .flatMap { token in switch token

    { case .Expired: return network.fetchToken() case .Valid: return just(token) } } .flatMap { token in return networking.performRequest(...) }
  90. network .accessToken // Observable<AccessToken> .flatMap { token in switch token

    { case .Expired: return network.fetchToken() case .Valid: return just(token) } } .flatMap { token in return networking.performRequest(...) }
  91. network .accessToken // Observable<AccessToken> .flatMap { token in switch token

    { case .Expired: return network.fetchToken() case .Valid: return just(token) } } .flatMap { token in return networking.performRequest(...) }
  92. network .accessToken // Observable<AccessToken> .flatMap { token in switch token

    { case .Expired: return network.fetchToken() case .Valid: return just(token) } } .flatMap { token in return networking.performRequest(...) }
  93. Benefits • Token not validated until needed • Inject behaviour

    into existing networking pipeline • Transparently • Example: github.com/Moya/Moya
  94. Are Observables monads?

  95. We don’t use the ‘m’ word here.

  96. There’s a lot more.

  97. Remember the FormHandler?

  98. extension FormHandler { var enabled: Observable<Bool> { return [submitter.submitting, validator.valid]

    .combineLatest { values in return ( submitting: values[0], valid: values[1] ) // Create a tuple } .map { (submitting, valid) in return submitting == false && valid == true } } }
  99. extension FormHandler { var enabled: Observable<Bool> { return [submitter.submitting, validator.valid]

    .combineLatest { values in return ( submitting: values[0], valid: values[1] ) // Create a tuple } .map { (submitting, valid) in return submitting == false && valid == true } } }
  100. extension FormHandler { var enabled: Observable<Bool> { return [submitter.submitting, validator.valid]

    .combineLatest { values in return ( submitting: values[0], valid: values[1] ) // Create a tuple } .map { (submitting, valid) in return submitting == false && valid == true } } }
  101. extension FormHandler { var enabled: Observable<Bool> { return [submitter.submitting, validator.valid]

    .combineLatest { values in return ( submitting: values[0], valid: values[1] ) // Create a tuple } .map { (submitting, valid) in return submitting == false && valid == true } } }
  102. That’s testable!

  103. class LoginViewController { ... formHandler .enabled .bindTo(submitButton.rx_enabled) .addDisposableTo(disposeBag) ... }

  104. That’s testable, too!

  105. github.com/artsy/eidolon

  106. Wrap-up 1. Strive for simplicity because brains don’t scale 2.

    State increases incidental complexity 3. Rather than eliminate state, FRP abstract it away
  107. So is FRP the future?

  108. For now.