lifting time to the types
If a video is playing, the buttons should still work. If something is loading, a loading indicator should animate.
The user should be able to touch the button even if the app is doing something slow like talking to our server
Callbacks subvert the type system
and you get callback hell
// what type am I?
val x: ??? = doLongTaskInBackground { result ->
println("Result of task:" + result)
}
// callback hell
doLongTaskInBackground {
runInForeground {
makeButtonBlue()
doLongTaskInBackground {
runInForeground {
/* etc */
}
}
}
}
Options hold zero or one elements
opt: Int?
opt = nil (zero) or opt = 5 (one)
The contents of an option are available now
Futures hold zero or one elements
However, the contents are available eventually not necessarily now
Futures represent computations in the background in a composable, type-safe way
intro:
elim:
// intro
// if I have something now, then I have it eventually at time=0
val f: Future<Int> = Future.succeeded(5)
val f: Future<Int> = future { sleep(1000); return 5 }
// elim
val x: Int = f.force() // warning: this blocks!
The futures we use are actually can fail
This is the "nil" case for futures
// intro
Future.failed(err)
// elim
// Sum type T + Error aka Result<T,Error>
f.force(): T + Error
Non-blocking way to "eliminate" a future
(not quite elimination because we don't get a T out)
// non-blocking elim
val f: Future<Int>
f.onSuccess{ v: T -> /* do something with v */ }
f.onFailure{ e: Error -> /* do something error */ }
In languages without exceptions, we can also parameterize over the error type
// intro
// I want to fail with a string
Future.failed("error reason"): Future<Int, String>
// I don't care about the reason it failed
Future.failed(()): Future<Int, Unit>
// I cannot fail!
// remember: Zero is the type that has no values
// we cannot construct an error, so we can't fail
Future.succeeded(1): Future<Int, Zero>
I'm going to keep using Future<T> for the rest of the slides, but you can swap with Future<T, E> basically everywhere
Futures hold data, so they look like our "M"s from last week
Anything operator that makes sense on options makes sense on futures
// if I have
// f -----(time)------> v : Future<T>
// return
// f -----(time)------> t(v) : Future<U>
func map<T>(f: Future<T>, t: T -> U) -> Future<U>
val a: Future<User> = getUserFromServer()
val b: Future<Int> = a.map{ u -> u.name.length }
// if I have
// f ---(time)--> v1
// g ---(time)----------> v2
// return
// f,g ---(time)----------> (v1, v2)
func zip<T, U>(f: Future<T>, g: Future<U>) -> Future<(T, U)>
val a: Future<User> = getUserFromServer()
val b: Future<Feed> = getFeedFromServer()
val both: Future<(User, Feed)> = zip(a, b)
// if I have:
// v
// return
// f--(time)-->v where time=0
//
// you are weakening your notion of the value being complete or not
func pure<T>(t: T) -> Future<T>
// if I have
// f --(time)--> v
// and a transform where
// transform(v)----(time)----> u
//
// then I can give you back
//
// f --(time)--> transform(v) --(time)---> u
// f --------------(time)----------------> u
func flatMap<T, U>(f: Future<T>, transform: T -> Future<U>) -> Future<U>
// a then b then c =>
val abc = a.flatMap{
b
}.flatMap{
c
}
// flatmap is equivalent to indenting in callback hell
Futures are monads
[ todo screenshot of search ]
When new search results come in, we need to update our search results
When a new search query comes in, we need to ask for data locally and remotely that match the query
x := a + b
imperative -- x is equal to the current values of a and b
x$ := a$ + b$
x$ is always the sum of the current values of a$ and b$.
Whenever either a$ or b$ changes, x$ changes
ui$ := render(buttontaps$, friends$)
the ui$ always reflects the newest information as soon as we get it.
We don't have to do anything manually!
Express reactive programs using map/reduce/filter etc!
Use a primitive called Observables
Lists hold any number of elements
[] or [1,2,3,4]
The contents of a list are available now
Observables hold any number of elements
However, the contents are available eventually not necessarily now
Observables represent changing state in a pure type-safe way
André Staltz @andrestaltz 14h14 hours ago
Print this as a poster if you develop with Rx: "Observables are like VIDEOS, *not* like event buses."
A good tweet:
HOT observables are always emitting things
// cold observable
// whenever we fetch users from the network, get the length of them
val o$ = fetchUsersFromNetwork$.map{ u -> u.length }
// actually start the request
o$.subscribe(
onNext = { c ->
println(c)
}, onError = { e ->
// handle error
})
COLD observables only start when subscribed
// hot observable
// get the x location of touches
touches$.map{ (x, y) -> x }
Just like Futures, Observables can fail
// to create a hot observable
// use subjects (you can think of as a "pipe")
val subj = PublishSubject<Int>.create()
assert(subj is Observable<Int>)
// put on the pipe
subj.onNext(10)
// listen on the pipe
(subj as Observable).subscribe{ x ->
// I see a hot x
}
// to create a cold observable
Observable.create{ s ->
s.onNext(1)
s.onNext(2)
s.onCompleted()
}
Observables hold data, so they also look like our "M"s from last week
Any operator that makes sense on lists makes sense on observables
Check out http://rxmarbles.com/
and things that are unique to observables
A future is started when it is created
Composing futures is pure
A future calls your callbacks eventually when you attach a listener
// side-effectful -- when this line of code excutes the network request starts!
val friendsFuture = getFriendsOnRollAsync();
// pure -- we're composing things to do whenever the request finishes
val countFuture = friendsFuture.map{ friends -> friends.length }
// side-effectul -- call our callbacks eventually (not represented in type)
countFuture.onSuccess{ friends ->
println(friends);
}.onFailure{ e ->
// handle error
}
Creating / composing observables is pure
Observables are started (cold observables) and call your listeners when subscribed
// pure -- the network request has not started yet
val friendsObservable = getFriendsOnRollAsync();
// pure -- we're just composing
val countObservable = friendsFuture.map{ friends -> friends.length }
// side-effectul -- start the request AND call our callbacks eventually
countObservable.subscribe(
onNext = { friends ->
println(friends);
},
onError { e ->
// handle error
})
Only subscribing to observables is side-effectul so observables are easier to reason over
On the other hand, observables don't express through the type that you only have one result where futures do
That downside is also an upside, since you can compose one-shot streams:
val lookupInCache$ = lookInCacheAsync()
val refreshFromNetwork$ = refreshFromNetworkAsync()
// use the cached results and refresh from network
Observable.merge(lookupInCache$, refreshFromNetwork$)
// search for people in my addressbook
// and search for people on Roll (hit the server)
override fun search(queries$: Observable<String>): Observable<List<Contact>> {
val localContacts$: Observable<List<Contact>> = fetchLocalContactsFromCacheForever()
val filteredLocal$: Observable<(String, List<Contact>)> =
queries$.withLatestFrom(localContacts$).map{ (query, contacts) ->
contacts.filter{ c -> c.name.contains(query) }
}
val networkReqs$: Observable<(String, List<Contact>)> =
queries$.throttle(200ms).map{ query ->
networkRequestSearch(query)
}.switchOnNext()
return Observable.combineLatest(
queries$,
filteredLocals$,
networkReqs$
).map{ (currentQuery, (latestLocalQuery, locals), (latestRemoteQuery, remotes)) ->
val goodLocals = if (currentQuery == latestLocalQuery) locals else []
val goodRemotes = if (currentQuery == latestRemoteQuery) remotes else []
(goodLocals + goodRemotes).sort()
}
}