Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Scaling iOS Development with FRP

Ash Furrow
February 23, 2016

Scaling iOS Development with FRP

Ash Furrow

February 23, 2016
Tweet

More Decks by Ash Furrow

Other Decks in Programming

Transcript

  1. Scaling iOS
    Development Using

    View Slide

  2. View Slide

  3. View Slide

  4. View Slide

  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

    View Slide

  6. View Slide

  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

    View Slide

  8. View Slide

  9. Simplicity is the goal
    of programming.

    View Slide

  10. Obvious? Maybe.

    View Slide

  11. What is simple?
    • Focused
    • Uncombined
    • Has one role, one purpose, one concept, etc…
    • Simple is not necessarily easy

    View Slide

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

    View Slide

  13. let numbers = [1, 5, 4, -1, 7]
    let sum = numbers.reduce(0, combine: +)
    print(sum)

    View Slide

  14. Is that simpler?
    Is that easier?

    View Slide

  15. Complexity:
    incidental vs. essential.

    View Slide

  16. Minimize incidental complexity,
    maximize simplicity.

    View Slide

  17. Easy doesn’t scale.

    View Slide

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

    View Slide

  19. –Harold Abelson, SICP
    “Programs must be written for people to read,
    and only incidentally for machines to execute.”

    View Slide

  20. Example
    • Form validation
    • Submit button is disabled until data entry is valid
    • Also disabled while the form is being submitted

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  24. let handler = FormHandler(callback: { enabled in
    self.submitButton.enabled = enabled
    }
    formDidChange(form: Form) {
    handler.update(form)
    }
    submitForm(form: Form) {
    handler.submit(form)
    }

    View Slide

  25. State leads to
    incidental complexity.

    View Slide

  26. Make things simple
    by avoiding state.

    View Slide

  27. View Slide

  28. What is state, even!?

    View Slide

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

    View Slide

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

    View Slide

  31. State changes
    over time.

    View Slide

  32. Managing state is
    incidental complexity.

    View Slide

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

    View Slide

  34. OMG! All those
    classes!

    View Slide

  35. Relaaaaaaax.

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  41. How much state
    is that?

    View Slide

  42. None.

    View Slide

  43. struct FormHandler {
    let callback: Bool -> Void
    private let submitter = FormSubmitter()
    private let validator = FormValidator()
    private let resetter = FormResetter()
    ...

    View Slide

  44. State contained
    within the submitter.

    View Slide

  45. This is not
    pedagogical.

    View Slide

  46. I would ship this.

    View Slide

  47. (After writing unit tests.)

    View Slide

  48. Simpler view
    controller.

    View Slide

  49. Isolated state.

    View Slide

  50. So.
    State doesn’t scale.

    View Slide

  51. Why?

    View Slide

  52. Because…
    Brains don’t scale.

    View Slide

  53. View Slide

  54. So state is bad…

    View Slide

  55. Let’s remove it!

    View Slide

  56. Not so fast.

    View Slide

  57. Abstraction.

    View Slide

  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.”

    View Slide

  59. var loginStatus = UserStatus.NotLoggedIn
    ...
    loginStatus = .LoggedIn

    View Slide

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

    View Slide

  61. Okay, but why?

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  65. Conceals state.

    View Slide

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

    }

    View Slide

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

    }

    View Slide

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

    }

    View Slide

  69. No access to state.

    View Slide

  70. Reacts to
    state changes.

    View Slide

  71. Functional.

    View Slide

  72. Why use a Variable?

    View Slide

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

    View Slide

  74. class LoginNetworkModel {
    func login(username: String, password: String)
    -> Observable {
    return network
    .authUsername(username, password: password)
    .map { $0.statusCode }
    .map { statusCode -> UserStatus in
    statusCode == 200 ? .LoggedIn : .NotLoggedIn
    }
    }
    }

    View Slide

  75. Decreased incidental
    complexity.

    View Slide

  76. Network access in
    an Observable.

    View Slide

  77. loginNetworkModel
    .login("ashfurrow", password: ...)
    .subscribeNext { [weak self] result in
    switch result {
    case .NotLoggedIn: self?.failedToLogin()
    case .LoggedIn: self?.welcomeUser()
    }

    }

    View Slide

  78. Seems like magic!

    View Slide

  79. Sure does.

    View Slide

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

    View Slide

  81. Subscribing returns
    a Disposable.

    View Slide

  82. Tie disposal to
    object deallocation.

    View Slide

  83. class LoginViewController: UIViewController {
    let disposeBag = DisposeBag()
    ...
    loginNetworkModel
    .login("ashfurrow", password: ...)
    .subscribeNext { ... }
    .addDisposableTo(self.disposeBag)
    ...

    View Slide

  84. OK, cool!
    What else?

    View Slide

  85. Requirements Change!
    • View controller needs access to full user info
    • Let’s modify our network model

    View Slide

  86. class LoginNetworkModel {
    func login(username: String, password: String)
    -> Observable {
    return network
    .authUsername(username, password: password)
    .filterSuccessfulStatusCodes()
    .mapToJSON()
    .mapToObject(User)
    }
    }

    View Slide

  87. loginNetworkModel
    .login("ashfurrow", password: …)
    .onError { [weak self] error in
    self?.handleFailure(error)
    }
    .subscribeNext { ... }
    .addDisposableTo(self.disposeBag)

    View Slide

  88. That’s really neat!

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  93. Benefits
    • Token not validated until needed
    • Inject behaviour into existing networking pipeline
    • Transparently
    • Example: github.com/Moya/Moya

    View Slide

  94. Are Observables
    monads?

    View Slide

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

    View Slide

  96. There’s a lot more.

    View Slide

  97. Remember the
    FormHandler?

    View Slide

  98. extension FormHandler {
    var enabled: Observable {
    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
    }
    }
    }

    View Slide

  99. extension FormHandler {
    var enabled: Observable {
    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
    }
    }
    }

    View Slide

  100. extension FormHandler {
    var enabled: Observable {
    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
    }
    }
    }

    View Slide

  101. extension FormHandler {
    var enabled: Observable {
    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
    }
    }
    }

    View Slide

  102. That’s testable!

    View Slide

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

    View Slide

  104. That’s testable, too!

    View Slide

  105. github.com/artsy/eidolon

    View Slide

  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

    View Slide

  107. So is FRP the future?

    View Slide

  108. For now.

    View Slide