flatMap combinatorThe idea behind flatMap captures a fundamental programming concept of
computation context. Below are examples of contexts:
| context | computation | example |
| list | item in list | [1,2,3] |
| dictionary | key-value pair | {myValue: true} |
| string | character | 'hello' |
| promise | asynchronous value | Promise.resolve(42).delay(500) // Bluebird |
| stream | many asynchronous values | Bacon.sequentially(500, [1,2,3]) |
The concept of computation context allows us to handle the computation and the context separately. This results in greater flexibility.
The idea of computation contexts is not particular to stream programming. Once you understand the idea, you can benefit from it in various contexts.
$ scala
scala> Seq(1,2,3).flatMap(num => Seq(num, num * 2))
res0: Seq[Int] = List(1, 2, 2, 4, 3, 6)
$ ghci
Prelude> [1,2,3] >>= (\num -> [num, num * 2])
[1,2,2,4,3,6]
Next, let’s turn back to flatMap.
flatMapWe can think of a stream as a context for asynchronous values. With the map
combinator we can transform the value in the stream, but what if want to
change the structure of the stream? We need to use flatMap.
Let’s imagine that we have a stream of strings from a text input field (think of a search field on a web application). Now we’d like to transform each of those strings into a database query:
searchInputs = Bacon.fromArray(['h', 'hel', 'hello'])
databaseResults = searchInputs.flatMap(textInput => Bacon.fromPromise(
fetch(`https://httpbin.org/response-headers?result=Results for ${textInput}`)
.then(response => response.json())
))
databaseResults.onValue(result => {
console.log('HTTP response', result)
})
Above, we transformed a stream of strings into a stream of HTTP responses. We
could not have done that with map, because map only allows us to change the
values in the stream. In contrast, flatMap allowed us to spawn a new stream
from each event in the searchInputs stream.
flatMapCreate a stream of numbers:
numbersStream = Bacon.fromArray([1,2,3])
Then transform numbersStream such that the resulting stream will emit the values 1
second(s), 2 second(s) and 3 second(s) every one second.
Because streams are computation contexts, they allow us to neatly separate successful computations from failed ones.
Let’s consider the search example we introduced above. What if the HTTP request
fails? How will an error manifest in a stream? Let’s change the hostname
httpbin.org to foobar.org and see what happens.
searchInputs = Bacon.fromArray(['h', 'hel', 'hello'])
databaseResults = searchInputs.flatMap(textInput => Bacon.fromPromise(
fetch(`https://foobar.org/response-headers?result=Results for ${textInput}`)
.then(response => response.json())
))
databaseResults.onValue(result => {
console.log('HTTP response', result)
})
Notice how the HTTP response string did not appear in the console log. This is
because the fetch
promise resolved into a failure, and Bacon.fromPromise translated that failure
into an error event in the databaseResults stream.
Let’s add a handler for the error events:
searchInputs = Bacon.fromArray(['h', 'hel', 'hello'])
databaseResults = searchInputs.flatMap(textInput => Bacon.fromPromise(
fetch(`https://foobar.org/response-headers?result=Results for ${textInput}`)
.then(response => response.json())
))
databaseResults.onError(errorMessage => {
console.log('Got error', errorMessage)
})
Above, we defined the onError handler instead of the success-handler
onValue.
combine combinatorWe learned that flatMap allows us to define new event sources. Let’s see how
we can combine streams.