The shape of time
lifting time to the types
In a modern app...
No hanging!
No hanging
You have 4 cores. Use them
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
No hanging
Classic solution: Callbacks
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 */
}
}
}
}
Holding things
Options (T?)
Options hold zero or one elements
opt: Int?
opt = nil (zero) or opt = 5 (one)
The contents of an option are available now
Holding things
Future<T>
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
Holding things
Future<T>
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!
Holding things
Future<T>
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
Holding things
Future<T>
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 */ }
Holding things
Future<T,E>
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
Holding things
Future<T>
Futures hold data, so they look like our "M"s from last week
Anything operator that makes sense on options makes sense on futures
Holding things
Future<T>
// 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)
Future<T>
// 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
In a modern app...
Show new data in realtime
[ todo screenshot of search ]
UI reacts to changes in underlying data
When new search results come in, we need to update our search results
Data reacts to underlying changes in UI
When a new search query comes in, we need to ask for data locally and remotely that match the query
Reactive Programming
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
Reactive Programming
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!
Functional Reactive Programming
Express reactive programs using map/reduce/filter etc!
Use a primitive called Observables
Holding things
List<T>
Lists hold any number of elements
[] or [1,2,3,4]
The contents of a list are available now
Holding things
Observable<T>
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
Holding things
Observable<T>
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:
Holding things
Observable<T>
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 }
Holding things
Observable<T>
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()
}
Holding things
Observable<T>
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
Holding things
Observable<T>
Check out http://rxmarbles.com/
- map
- zip
- flatMap
and things that are unique to observables
- merge
- combineLatest
- withLatestFrom
- debounce
Side Effects
an aside
Side Effects
Futures
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
}
Side Effects
Observables
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
})
Which to use?
Observables
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$)
Obligatory Observable Allegory
I couldn't help the alliteration
// 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()
}
}
Lift Time Into the Value/Type
By bkase
Lift Time Into the Value/Type
PL Part 3
- 807