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
- It has a greate community