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

  • 801