FRP in Swift

这是 2017/05/26 我在知乎 iOS 团队内部做的一次分享
第一次做技术分享,最大的问题还是出在准备上。确定了分享的时间,但是到底要讲些什么内容内心却一直没有一个准确的范围,结果就是到了前一天还是往里面加东西,前一刻还在修改 Kyenote。分享的时候因为有很多写代码的环节,所以一开始有些小紧张,手还发抖,不过到后来慢慢好起来了。本来想把实况录屏全程记录下来的,但是因为 Ariplay 的录屏貌似有些小问题,为了不耽误大家时间,我就直接放弃了,还是有些小遗憾。
 
以下是 Keynote 和分享的主要内容

Functional Reactive Programming in Swift

What?

Functional Programming

First-class function

Treats functions as first-class citizens

Higher-order function

A function that does at least one of the following:
  • takes one or more functions as arguments
  • returns a function as its result

Pure function

Function which has no side-effects

Example

// pure function
func addTen(_ a: Int) -> Int {
    return a + 10
}

// higher order function
func twice(_ f: @escaping (Int) -> (Int)) -> (Int) -> (Int) {
    return {
        f(f($0))
    }
}

// first-class citizen
let addTenTwice = twice(addTen)
addTenTwice(10) //30
let addTenFourTimes = twice(addTenTwice)
addTenFourTimes(10) //50


// a little more harder
func multiplyBySelf(_ a: Int) -> Int {
    return a * a
}

let g = twice(multiplyBySelf)
g(3) // 81
twice(g)(3) // 43046721

let a = 3 * 3 //9
let b = a * a //81
let c = b * b //6561
let d = c * c //43046721

Reactive Programming

Asynchronous Data Streams

--a---b-c---d---X---|->
  • A stream is a sequence of ongoing events ordered in time
  • Everything can be a stream
    • touch event
    • KVO
    • Notification
    • callback
    • Network response
    • timer
    • ...

Functional + Reactive

Stream

  • Like an Array, it can hold anything
  • Unlike an Array, you can't access it anytime you want, instread, you get notified when it's value get changed
  • Like a pipe, if you missed the thing through it, it's gone forever

Transformation

  • Change a stream to another stream, just like change a sequence to another
  • Higher-order functions, map, filter, reduce, flatMap, etc
let reviewers = ["kimi", "qfu", "dhc", "x", "gaoji"]

// implement our own trnasformation functions
extension Array {
    func xy_map<T>(_ transform: (Element) -> T) -> [T] {
        var result: [T] = []

        for i in self {
            result.append(transform(i))
        }

        return result
    }

    func xy_filter(_ condition: (Element) -> Bool) -> [Element] {
        var result: [Element] = []
        for i in self {
            if condition(i) {
                result.append(i)
            }
        }
        return result
    }

    func xy_reduce<T>(_ initialValue: T, _ combine: (T, Element) -> T) -> T {
        var value = initialValue

        for i in self {
            value = combine(value, i)
        }

        return value
    }
}

reviewers.xy_map {
    $0.uppercased()
}

reviewers.xy_filter {
    $0.characters.count > 3
}

// chain transformations
reviewers
    .xy_filter { $0.characters.count > 3 }
    .xy_reduce("") { return $0 + "\\($1) review my code please~\\n" }

// the original value hasn't been changed
reviewers

// a little bit about flatMap
let xxs = [[1, 2], [3, 4], [5, 6]]
let xso = [1, 2, 3, nil, 5]
// flatMap has 2 signature
xxs.flatMap { arr in
    arr.map {$0}
} // [1, 2, 3, 4, 5, 6]
xso.flatMap {
    $0
} // [1, 2, 3, 5]

Binding

Binding makes program more reactive

in Swift!

  • Functional language
  • Compiler & strong typed
Functional + Reactive + Swift, write awesome program!

Why

Good

  • Improve productivity
  • Less and more centralised code
  • Easy to maintain
  • Avoid complexity with mutable state growing over time
  • Change the way you think when coding

Bad

  • Learning curve is steep, but not that steep
  • Hard to debug
The benefits it brings are worth we give it a try

How

Unserstand the basic reactive unit

Observable

It send messages

Subscriber

It consume messages
Observables are like Sequence
// Observables are like Sequence
let xs = [1, 2, 3, 4, 5]

// iterate a sequence
for x in xs {
    print(x)
}

// the operation above equals
var xsIte = xs.makeIterator()
while let x = xsIte.next() {
    print(x)
}

// we can use Sequence feature to make a CountDown
struct CountDown: Sequence, IteratorProtocol {
    var num: Int

    var notify: (Int?) -> ()

    mutating func next() -> Int? {
        notify(num)
        if num == 0 {
            return nil
        }

        defer {
            num -= 1
        }
        return num
    }
}

var ite = CountDown(num: 10) {
        // as a subscriber, we are consuming messages
        print($0)
    }.makeIterator()

// now it's kind like a stream
// once next() called, it'll print the latest value, it's reactive now
ite.next() //10
ite.next() //9
ite.next() //8
How can we make our own Observable/Subscriber pattern?
import Foundation
import UIKit

class KeyValueObserver<A>: NSObject {
    let block: (A) -> ()
    let keyPath: String
    let object: NSObject
    init(object: NSObject, keyPath: String, _ block: @escaping (A) -> ()) {
        self.block = block
        self.keyPath = keyPath
        self.object = object
        super.init()
        object.addObserver(self, forKeyPath: keyPath, options: .new, context: nil)
    }

    deinit {
        print("deinit")
        object.removeObserver(self, forKeyPath: keyPath)
    }

    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        block(change![.newKey] as! A)
    }
}

class Observable<A> {
    private var callbacks: [(A) -> ()] = []
    var objects: [Any] = []

    static func pipe() -> ((A) -> (), Observable<A>) {
        let observable = Observable<A>()
        return ({ [weak observable] value in
            observable?.send(value)}, observable
        )
    }

    private func send(_ value: A) {
        for callback in callbacks {
            callback(value)
        }
    }

    func subscribe(callback: @escaping (A) -> ()) {
        callbacks.append(callback)
    }
}

extension UITextField {
    func observable() -> Observable<String> {
        let (sink, observable) = Observable<String>.pipe()
        let observer = KeyValueObserver(object: self, keyPath: #keyPath(text)) {
            sink($0)
        }
        observable.objects.append(observer)
        return observable
    }
}

var textField: UITextField? = UITextField()

textField?.text = "asd"
var observable = textField?.observable()

observable!.subscribe {
    print($0)
}

textField?.text = "asdjlas"
textField?.text = "asdjk"
textField = nil
observable = nil

Integrate a reactive programming library

  • ReactiveSwift
  • ReactiveKit
  • RxSwift
Neither can goes wrong, but I prefer RxSwift because,
  • It's a ReactiveX official Swift implementation which means
    • Developer won't give it up (Maybe?)
    • You can easily switch to other platform

Credits


© Xinyu 2014 - 2024