17.
Sequence & Flow
Written by Massimo Carli
In Chapter 16, “Handling Side Effects”, you learned how to use the IO<T> monad as a special case of the State<S, T> data type. You also learned that Kotlin provides coroutines to handle side effects as pure functions in a more idiomatic way. In this chapter, you’ll learn everything you need to know about the following special Kotlin data types:
Sequence<T>Flow<T>
You’ll also have a quick overview of SharedFlow<T> and StateFlow<T>.
You’ll learn how these data types work from a functional programming point of view. In particular, you’ll answer the following questions for each of these:
- What is the context they provide?
- Are they a functor?
- Are they an applicative functor?
- Are they a monad?
Note: If you don’t know coroutines yet, Kotlin Coroutines by Tutorials is the book for you.
This chapter is a big exercise that helps you take the concepts you’ve learned so far and apply them to the types you use every day in your job. It’s time to have fun!
The Sequence<T> data type
In Chapter 9, “Data Types”, you learned that List<T> is a data type with the functor and monad superpowers with the map and flatMap functions. In Chapter 4, “Expression Evaluation, Laziness & More About Functions”, you also learned that laziness is one of the main characteristics of functional programming. To remind you what this means, open ListDataType.kt in this chapter’s material and write the following code:
fun main() {
listOf(1, 2, 3, 4, 5) // 1
.filter(filterOdd.logged("filterOdd")) // 2
.map(double.logged("double")) // 3
}
In this code, you:
- Use
listOfas a builder for aList<Int>with five elements of typeInt. - Invoke
filter, passing the reference to the logged version offilterOdd. - Use
mapto transform the filter’s values using a logged version ofdouble.
Note:
filterOddanddoubleare two very simple functions you find in Util.kt in the lib sub-package.loggedis a utility higher-order function that decorates another function with a log message. Take a look at their simple implementation, if you want.
The interesting fact about the previous code happens when you run it, getting the following output:
filterOdd(1) = false
filterOdd(2) = true
filterOdd(3) = false
filterOdd(4) = true
filterOdd(5) = false
double(2) = 4
double(4) = 8
This happens because, in each line, you:
- Create a
List<Int>with five elements. - Invoke
filter, which returns anotherList<Int>containing only the even values. It’s crucial to see thatfilterOddhas been invoked for all the elements of the originalList<Int>. - Use
map, getting a newList<Int>with the double of the values in the previousList<Int>.
With this code, you basically created three lists without using any of the individual lists’ values. What happens if you don’t really need the values in the List<Int>? In this case, you started with a List<Int> of five elements. What if the list has a lot more elements? What if the elements in the List<T> are infinite?
Well, you don’t have to blame the List<T> data type because its job is to contain an ordered collection of elements of type T. That’s why it’s been created that way. That’s its context, or purpose, if you will. Another way to say it is that List<T> is eager.
If you don’t want to keep all the possible values in a List<T>, Kotlin provides the Sequence<T> data type.
Open SequenceDataType.kt and write the following code:
fun main() {
sequenceOf(1, 2, 3, 4, 5) // HERE
.filter(filterOdd.logged("filterOddSeq"))
.map(double.logged("doubleSeq"))
}
This code differs from the previous one because of the use of sequenceOf instead of listOf. More importantly, if you run the code, you’ll get nothing as output. This is because Sequence<T> is lazy. If you want to actually consume the values in the Sequence<Int> you just created, you need to consume them using a terminal operator. To see how, add .count() to the end of the method chain. It should now look like this:
fun main() {
sequenceOf(1, 2, 3, 4, 5)
.filter(filterOdd.logged("filterOddSeq"))
.map(double.logged("doubleSeq"))
.count() // HERE
}
Here, you’re just counting the elements in the sequence and, to do it, you need to consume all of them. This time, running the code, you’ll get the following:
filterOddSeq(1) = false
filterOddSeq(2) = true
doubleSeq(2) = 4
filterOddSeq(3) = false
filterOddSeq(4) = true
doubleSeq(4) = 8
filterOddSeq(5) = false
Note how the order of the log messages is different from the one you got from the List<T>. In that case, each operator read the values from the input List<T>. Now, the chain of operators is called for each value you consume.
Note: If you’re curious and want to look at the definition of
Sequence<T>, you’ll find that it differs from theIterable<T>interface in the use of theoperatorkeyword, which allows its use in an enhanced form.
This clarifies the context for a Sequence<T> as a container that produces the values it contains only when required. That means it’s lazy. But is Sequence<T> a functor?
Sequence<T> as a functor
Looking at the Sequence<T> documentation, you see the definition of map with the following signature:
public fun <T, R> Sequence<T>.map(transform: (T) -> R): Sequence<R>
You used map in the example in the previous section. In this case, you want to do something more and use property-based testing to prove the functor laws for Sequence<T>.
Note: You already used property-based testing in Chapter 12, “Monoids & Semigroups”.
You want to prove that, given the two functions f and g and the identity i:
map(i) == imap(f compose g) == map(f) compose map(g)
If you want to use property-based testing, you should define a way to generate random functions using a specific implementation of the following interface you find in PropertyTest.kt in the lib sub-package in this chapter’s material:
fun interface Generator<T> {
fun generate(n: Int): List<T>
}
As the first step, add the following to the PropertyTestFun.kt file:
fun <T, R> Generator<T>.map(fn: (T) -> R): Generator<R> = object : Generator<R> {
override fun generate(n: Int): List<R> = this@map.generate(n).map(fn)
}
This describes the map function for a Generator<T>. It’s just a simple way to create a Generator<R> from a Generator<T> using a function of type (T) -> R or, using the typealiases in Definitions.kt, Fun<T, R>.
Note: Be careful that
Generator<T>isn’t a functor because it’s not even pure: It generates random values.
Now, you can get a Generator<Fun<A, B>> from a Generator<A> and Generator<B> with the following code you can add to the same PropertyTestFun.kt:
fun <A, B> funGenerator(bGen: Generator<B>): Generator<Fun<A, B>> =
bGen.map { b: B -> { b } }
Every Fun<A, B> describes a way to map values of type A into values of type B. The Fun<A, B> you’ll get from funGenerator is a function that maps the same random value of type B to any values of type A you’ll pass as input to the generated function. Because that return value is the same for all the input values, you can assume that the same would happen for a specific value you might generate during testing.
Open SequenceDataTypeTest.kt in the test build type, and add the following code:
@Test
fun `Identity Functor Law for Sequences`() {
val intToStringFunGenerator =
funGenerator<Int, String>(StringGenerator(5)) // 1
val i = { s: String -> s } // 2
100.times { // 3
val f = intToStringFunGenerator.one() // 4
val seq = IntGenerator.generate(5).asSequence() // 5
val list1 = seq.map(f compose i).toList() // 6
val list2 = seq.map(f).toList() // 7
Truth.assertThat(list1).isEqualTo(list2) // 8
}
}
Here, you:
- Use
funGeneratorto generate a random function of typeFun<Int, String>. This function mapsInts toStrings of length5. - Provide the identity function
i. - Iterate
100times over the following commands. - Get a
Fun<Int, String>fromintToStringFunGeneratorand store it inf. - Generate a sequence of
5random elements inseq. - Apply the composition between
fanditosequsingmap. - In the same way, you apply only
f. - Verify that the results are the same.
Run the test, and check that everything is successful. Now, you can be confident that the first law of Sequence<T> as a functor is valid.
The second law is very simple. Just add this code to the same file:
@Test
fun `Composition Functor Law for Sequences`() {
val intToStringFunGenerator =
funGenerator<Int, String>(StringGenerator(5))
val stringToLongFunGenerator =
funGenerator<String, Long>(LongGenerator) // 1
100.times {
val f = intToStringFunGenerator.one()
val g = stringToLongFunGenerator.one() // 2
val seq = IntGenerator.generate(5).asSequence()
val list1 = seq.map(f compose g).toList() // 3
val list2 = seq.map(f).map(g).toList() // 4
Truth.assertThat(list1).isEqualTo(list2) // 5
}
}
The only difference here is that you:
- Create a new
Generator<Fun<String, Long>>instringToLongFunGenerator. - Use
intToStringFunGeneratorandstringToLongFunGeneratorto generate two different functions:fandgof typeFun<Int, String>andFun<String, Long>, respectively. - Invoke
map, passing the composition offandgas a parameter. - Invoke
map, first withfand then withg. - Compare the results of the two cases.
Now, just run the test and see that all the tests are successful. Great job!
Sequence<T> as an applicative functor
You just proved that the Sequence<T> data type is a functor because of the existing implementation of map. But what about applicative functors? Looking at the Kotlin documentation, you don’t see any higher-order functions like your ap and app. No problem — you can do this!
Open SequenceDataType.kt, and add the following code:
fun <A, B> Sequence<A>.ap(
fn: Sequence<(A) -> B>
): Sequence<B> = TODO()
This is the signature for the ap function for a Sequence<T>. It’s an extension function on the type Sequence<A> and accepts an input parameter of type Sequence<(A) -> B> or Sequence<Fun<A, B>> if you use the type alias. The return type is Sequence<B>. Basically, you have two sequences. The first generates values of type A, and the second generates functions of type Fun<A, B>. The result, then, is a Sequence<B> of the value you get by applying the function in Sequence<Fun<A, B>> to the values in Sequence<A>. How would you implement ap?
Note: Feel free to provide your implementation as a fun exercise if you want.
Now, replace the previous code with the following:
fun <A, B> Sequence<A>.ap(fn: Sequence<(A) -> B>): Sequence<B> =
sequence { // 1
val iterator = iterator() // 2
while (iterator.hasNext()) {
val fnIterator = fn.iterator() // 3
val item = iterator.next()
while (fnIterator.hasNext()) {
yield(fnIterator.next().invoke(item)) // 4
}
}
}
In this code, you:
- Generate a
Sequence<B>using thesequencebuilder. - Get the reference to the
Iterator<A>from theSequence<A>and iterate over it. - Get the reference to the
Iterator<Fun<A, B>>from theSequence<Fun<A, B>>you get as an input parameter and iterate over them. - Use
yieldto produce the value you get by applying the current functionFun<A, B>to the current valueA.
To see how it works, start by adding the following utility function. It allows you to use ap as an infix operator, as you did with the other applicative functor implementations:
infix fun <A, B> Sequence<(A) -> B>.appl(a: Sequence<A>) = a.ap(this)
To see it working, replace the main implementation in SequenceDataType.kt with the following:
fun main() {
data class User( // 1
val id: Int,
val name: String,
val email: String
)
val userBuilder = ::User.curry() // 2
val userBuilderSeq = sequenceOf(userBuilder) // 3
val idSeq = sequenceOf(10, 20, 30) // 4
val nameSeq = sequenceOf("Minnie", "Donald", "Mickey") // 4
val emailSeq =
sequenceOf("aaaaaa@aaaaa.com", "bbbbb@bbbbbb.com") // 4
val userSeq =
userBuilderSeq appl idSeq appl nameSeq appl emailSeq // 5
userSeq.forEach(::println) // 6
}
In this example, you:
- Create a
Userdata class representing a user withid,nameandemailproperties. - Use
curryto get theUserconstructor as a function of type(Int) -> (String) -> (String) -> User. - Create a
Sequence<(Int) -> (String) -> (String) -> User>using thesequenceOfbuilder. - Use
sequenceOffor creating aSequence<Int>for theids,Sequence<String>fornames andSequence<String>for theemails. - Use
applto create aSequence<User>. - Print all the values in the
Sequence<User>.
When you run this code, you’ll get 3 * 3 * 2 = 18 different values, like the following:
User(id=10, name=Minnie, email=aaaaaa@aaaaa.com)
User(id=20, name=Minnie, email=aaaaaa@aaaaa.com)
// ...
User(id=20, name=Mickey, email=bbbbb@bbbbbb.com)
User(id=30, name=Mickey, email=bbbbb@bbbbbb.com)
Considering that the context of a Sequence<T> is to provide values of type T in a lazy way, applying a function with multiple parameters leads to a number of values, like in the previous example.
Sequence<T> as a monad
Is Sequence<T> finally a monad? Of course it is, because of the flatMap operation that Kotlin APIs provide with the following signature, similar to the Kleisli category:
fun <T, R> Sequence<T>.flatMap(
transform: (T) -> Sequence<R>
): Sequence<R>
You can test how this works by running the following example:
fun main() {
// ...
val seqTo = { n: Int -> (1..n).toList().asSequence() }
val seqOfSeq = sequenceOf(1, 2, 3, 4, 5).flatMap(seqTo)
seqOfSeq.forEach { print("$it ") }
}
Getting an output like the following:
1 1 2 1 2 3 1 2 3 4 1 2 3 4 5
The Flow<T> data type
In Chapter 16, “Handling Side Effects”, you learned that Kotlin allows you to achieve with suspendable functions what you can do with the IO<T> monad. In this chapter, you’ve already learned how to produce a theoretically infinite sequence of values in a lazy way. If the values you want to generate are the result of a suspendable function, the Flow<T> data type is what you need.
You can then say that the context of the Flow<T> data type is the generation of a sequence of values you create using a suspendable function which, as you know, allows you to handle side effects in a pure fashion.
As you’ll see, a Flow<T> is very similar to a Sequence<T> in terms of functional programming concepts. So, the following sections are basically a good review of things you’ve already learned: repetita juvant, as the Romans used to say! :]
Flow<T> as a functor
To prove that Flow<T> is a functor, you could repeat the same process you did for Sequence<T> using property-based testing. In this case, you’ll keep things easier, implementing some practical examples.
Note: As an interesting exercise, you could use property-based testing for
Flow<T>as well.
Open FlowDataType.kt and add the following code:
fun inputStringFlow(question: String = "") = flow { // 1
val scanner = java.util.Scanner(System.`in`) // 2
print(question) // 3
while (scanner.hasNextLine()) { // 4
val line = scanner.nextLine() // 4
if (line.isNullOrEmpty()) { // 5
break
}
emit(line) // 6
print(question) // 3
}
scanner.close() // 7
}
In this code, you:
- Define
inputStringFlowas a function that returns aFlow<String>of the text you write as input using aScannerreading from the standard input. This is a flow version of the effect you used in Chapter 16, “Handling Side Effects”. You have aquestionparameter that allows you to print some text before the user enters anything. - Initialize the
Scannerreading from the standard input. - Print the question.
- Read all the input one line at a time.
- Exit the cycle if the user enters an empty line.
- Emit the value from the user as a value from the
Flow<String>. - Close the
Scanner.
As an example of a functor, add the following code to the same file:
fun main() {
val strLengthFlow = inputStringFlow("Insert a word: ") // 1
.map { str -> // 2
str to str.length
}
runBlocking { // 3
strLengthFlow.collect { strInfo -> // 4
println("${strInfo.first} has length ${strInfo.second}")
}
}
}
In this code, you:
- Use
inputStringFlowto get aFlow<String>for the user input. Note that you run this outside any specificCoroutineScope. You’re not executing anything — you’re just stating you eventually might. - Invoke
map, passing a lambda that returns aPair<String, Int>of the inputStringand its length. It’s important to note that the lambda here is executed as a suspendable block. This means that it has a scope, and it can contain invocations to other suspendable functions. In other words, usingmap(String::length)would give a compilation error becauseString::lengthisn’t a suspendable function. In your context, this also means you can apply a transformation,Fun<A, B>, to the values you get from aFlow<A, B>, which is the consequence of a side effect. - Define a
runBlockingblock. This is because you’ll consume, or better, collect what theFlow<Pair<String, Int>>produces, so the side effects you described will actually run. - Collect and print the output values of type
Pair<String, Int>you get from the flow.
Now, you can run the code and get an output like the following:
To answer the initial question, yes, Flow<T> is a functor, but remember that the transformation Fun<A, B> must be a suspendable function.
Flow<T> as an applicative functor
To see if the Flow<T> also behaves as an applicative functor, either repeat what you did for the Sequence<T> or just follow along. In FlowDataType.kt, add the following code:
fun <A, B> Flow<A>.ap(fn: Flow<(A) -> B>): Flow<B> = flow { // 1
collect { a -> // 2
fn.collect { f -> // 3
emit(f(a)) // 4
}
}
}
infix fun <A, B> Flow<(A) -> B>.appl(
a: Flow<A>
) = a.ap(this) // 5
Here, you:
- Define
apwith the usual signature as an extension function forFlow<A>. - Collect the value of type
Afrom theFlow<A>you have as the receiver. - Collect the function
fof typeFun<A, B>from theFlow<Fun<A, B>>orFlow<(A) -> B>you pass as the input parameterfn. - Apply the function
fto the valueaandemitthe result. - As usual, define
applas an infix version ofap.
You can then test everything by adding the following code to the same file:
fun main() {
val userBuilder = { id: Int ->
{ name: String ->
{ email: String -> User(id, name, email) }
}
}
val userBuilderFlow = flowOf(userBuilder)
val idFlow = listOf(10, 20, 30).asFlow()
val nameFlow = listOf("Pippo", "Pippo2", "Pippo3").asFlow()
val emailFlow = listOf(
"pippo@pippo.com", "pippo2@pippo.com", "pippo3@pippo.com"
).asFlow()
val userFlow =
userBuilderFlow appl idFlow appl nameFlow appl emailFlow
runBlocking {
userFlow.collect(::println)
}
}
This code shouldn’t be a surprise anymore. When you run it, you’ll get:
User(id=10, name=Pippo, email=pippo@pippo.com)
User(id=20, name=Pippo, email=pippo@pippo.com)
// ...
User(id=20, name=Pippo3, email=pippo3@pippo.com)
User(id=30, name=Pippo3, email=pippo3@pippo.com)
The same notes you learned for Sequence<T> are valid here.
Flow<T> as a monad
To answer the last question, you’ll implement a more complex example using some of the code you already implemented in Chapter 14, “Error Handling With Functional Programming”, that you can find in the material for this project. You basically want to use inputStringFlow to allow a user to insert some text to search for in the TV show database using the TVmaze API. Now, you’re in the world of coroutines, so you should use their power. It’s time for an interesting exercise to improve your functional thinking.
Imagine you have a basic function, like the following you can write in Basic.kt:
fun doSomeWork(name: String): Int = 10
It doesn’t really matter what doSomeWork does. What’s important is the type, which is (String) -> Int, and how the function achieves its goal. If it needs to do some hard work, you probably want to run it in the background, so in the context of a coroutine. You can do this with the following code, which you should add to the same file:
suspend fun doSomeBgWork(
ctx: CoroutineContext,
name: String
): Int = withContext(ctx) {
doSomeWork(name)
}
This simple code has a few interesting things to note. doSomeBgWork:
- Is a suspend function.
- Accepts two parameters now. The first is of type
CoroutineContext, and the second is the input fordoSomeWork. - Uses
withContextto rundoSomeWorkin theCoroutineContextyou provide as input. - Has the return type
Int.
You already know that in functional programming, you don’t like functions with multiple parameters. No problem — you also know you can curry them, but with doSomeBgWork, there’s a problem. You can see the problem by adding the following code:
fun main() {
doSomeBgWork.curry()
}
You’ll get the following error:
The reason is very simple. When you set a function as suspend, you’re basically changing its type. The Kotlin compiler adds implicit parameters of type Continuation that keep track of the state of the coroutine.
Note: Look at the decompiled code, and you can see how the
Continuationis used in a way that reminds you how you implemented theState<S, T>andIO<T>data types.
How can you then implement curry for a suspendable function? You already know that. In the same Basic.kt file, add the following definitions:
typealias SuspendFun<A, B> = suspend (A) -> B // 1
typealias SuspendFun2<A, B, C> = suspend (A, B) -> C // 2
typealias SuspendChain2<A, B, C> =
suspend (A) -> suspend (B) -> C // 3
In this simple code, you define an alias for suspendable functions of:
- One input parameter of type
Aand one output parameter of typeB. - Two input parameters of type
AandBand one output parameter of typeC. - One input parameter of type
Aand a suspendable function of one input parameter of typeBand output of typeC.
These allow you to define curry for suspendable functions by adding the following code:
fun <A, B, C> SuspendFun2<A, B, C>.curry(): SuspendChain2<A, B, C> =
{ a: A ->
{ b: B ->
this(a, b)
}
}
Now, the previous code compiles successfully:
Implementing curry for suspendable functions is a kind of a warm-up. Going back to doSomeBgWork, you can say that the type of ::doSomeBgWork.curry() is now (String) -> (CoroutineContext) -> Int. Umm, that’s starting to ring a bell.
What really matters in functional programming is composition. What you did earlier is say that you can create a suspendable function from a non-suspendable one, providing a CoroutineContext, and this carrying on reminds you of something you already learned: the State<S, T> monad.
CoroutineContext as a state
To relate the State<S, T> monad to what you saw about suspendable functions, look again at doSomeBgWork, which you wrote in Basic.kt:
suspend fun doSomeBgWork(ctx: CoroutineContext, name: String): Int =
withContext(ctx) {
doSomeWork(name)
}
It has the type (CoroutineContext, String) -> Int. If the CoroutineContext is something you want to carry on, you can create doSomeMoreBgWork like the following:
suspend fun doSomeMoreBgWork(
ctx: CoroutineContext,
name: String
): Pair<CoroutineContext, Int> = withContext(ctx) {
ctx to doSomeWork(name)
}
doSomeMoreBgWork has the type (CoroutineContext, String) -> Pair<CoroutineContext, Int>. Applying curry, you can get a function of type (String) -> (CoroutineContext) -> Pair<CoroutineContex, Int>.
Note: Yes, the
CoroutineContextandStringtypes have different orders, but you already know how to implementflip, right?
Now, the analogy with State<S, T> is almost obvious. You just need to fix a problem caused by the presence of the suspend modifier. No problem.
Open SuspendState.kt and add the following first definition:
typealias SuspendStateTransformer<S, T> =
suspend (S) -> Pair<S, T>
Ring a bell now? Using this definition, the type of doSomeMoreBgWork is suspend (String) -> SuspendStateTransformer<CoroutineContext, Int>.
You can now follow the same process you did for State<S, T>, keeping in mind you’re working with suspendable functions, and the state is a CoroutineContext.
In SuspendState.kt, add the following code:
data class SuspendableState<S, T>(
val sst: SuspendStateTransformer<S, T>
) {
companion object {
@JvmStatic
fun <S, T> lift(
value: T
): SuspendableState<S, T> =
SuspendableState { state -> state to value }
}
}
Here, you define the SuspendableState data type using SuspendStateTransformer.
You can now add the implementation for map:
fun <S, A, B> SuspendableState<S, A>.map(
fn: SuspendFun<A, B>
): SuspendableState<S, B> =
SuspendableState { s0: S ->
val (s1, a) = this.sst(s0)
s1 to fn(a)
}
Finally, add the implementation for flatMap like this:
fun <S, A, B> SuspendableState<S, A>.flatMap(
fn: suspend (A) -> SuspendableState<S, B>
): SuspendableState<S, B> =
SuspendableState { s0: S ->
val (s1, a) = this.sst(s0)
fn(a).sst(s1)
}
Note: This time, you can’t override the
invokeoperator as you did in a non-coroutine environment. To be useful, it should also be suspendable, and this wouldn’t work.
How can you use this for your initial TV show problem? There’s actually quite a bit more fun in store for you. :]
Back to the TV show
In the previous section, you created the SuspendableState<S, T> data type and implemented lift, map and flatMap. How can you use these for getting data about a TV show? In the tools sub-package in this chapter’s material, you find TvShowFetcher and TvShowParser for, respectively, fetching and parsing data using the TVmaze API.
Look at the existing code, and you’ll see that TvShowFetcher and TvShowParser don’t actually handle exceptions. This is also why you used those objects in many different ways in Chapter 14, “Error Handling With Functional Programming”.
Now, you want to run them as side effects in a suspendable function and handle errors. How can you do that?
Open ShowSearchService.kt and add the following code:
suspend fun fetchTvShowResult( // 1
ctx: CoroutineContext,
query: String
): Result<String> = // 2
withContext(ctx) { // 3
try {
Result.success(TvShowFetcher.fetch(query)) // 4
} catch (ioe: IOException) {
Result.failure(ioe) // 5
}
}
This code should look familiar, even if it combines a few concepts. Here, you:
- Define
fetchTvShowResultas a suspendable function with aCoroutineContextas the first parameter and aStringas the second. Note how the structure of this function is very similar to the one ofdoSomeMoreBgWork. - Set
Result<String>as the return type. This is a little bit more complicated than a simpleString. You’ll need to do some more work because of this, as you’ll see later. - Use the
CoroutineContextyou receive inctxto create a coroutine. - Invoke
TvShowFetcher.fetch, passing thequeryas input. In the case of success, you return theStringwrapped in aResult<String>. - In the case of error, you encapsulate the
IOExceptionin aResult<String>. Yes, the value for the type parameter isString.
For TvShowParser.parse, you can follow the same pattern, adding this to the same file:
suspend fun parseTvShowResult(
ctx: CoroutineContext,
json: String
): Result<List<ScoredShow>> =
withContext(ctx) {
try {
Result.success(TvShowParser.parse(json))
} catch (e: Exception) {
Result.failure(e)
}
}
This time, the resulting type is Result<List<ScoredShow>>, but the structure is the same.
So far, so good. Now, you have two functions:
-
fetchTvShowResultof typesuspend (CoroutineContext, String) -> Result<String>. -
parseTvShowResultof typesuspend (CoroutineContext, String) -> Result<List<ScoredShow>>.
They look like the functions of type suspend (A) -> SuspendStateTransformer<CoroutineContext, B> you can compose using the flatMap implementation you created earlier for SuspendableState.
You can try to solve this problem by adding the following code:
val fetchSuspend: (String) -> SuspendableState<
CoroutineContext, Result<String>> = { query ->
SuspendableState { ctx: CoroutineContext ->
ctx to fetchTvShowResult(ctx, query)
}
}
val parseSuspend: (String) -> SuspendableState<
CoroutineContext, Result<List<ScoredShow>>> = { json ->
SuspendableState { ctx: CoroutineContext ->
ctx to parseTvShowResult(ctx, json)
}
}
Now:
-
fetchSuspendhas type(String) -> SuspendableState<CoroutineContext, Result<String>>. -
parseSuspendhas type(String) -> SuspendableState<CoroutineContext, Result<List<ScoredShow>>>.
This is a problem because, in both cases, you have a SuspendableState<CoroutineContext, Result<T>>. This means a Result<T> data type encapsulates into a SuspendableState<S, T> data type. Composition, as defined in flatMap for SuspendableState, doesn’t work. How can you fix it?
Composing SuspendableState<CoroutineContext, Result<T>>
To implement composition now is simpler than it seems. Open SuspendableStateResult.kt, and add the following code:
typealias SuspendStateResultTransformer<S, T> =
suspend (S) -> Pair<S, Result<T>> // 1
data class SuspendableStateResult<S, T>( // 2
val sst: SuspendStateResultTransformer<S, T>
) {
companion object {
@JvmStatic
fun <S, T> lift( // 3
value: T
): SuspendableStateResult<S, T> =
SuspendableStateResult { state ->
state to Result.success(value)
}
}
}
fun <S, A, B> SuspendableStateResult<S, A>.map( // 4
fn: SuspendFun<A, B>
): SuspendableStateResult<S, B> =
SuspendableStateResult { s0: S ->
val (s1, a) = this.sst(s0)
s1 to a.fold(
onSuccess = { Result.success(fn(it)) },
onFailure = { Result.failure(it) }
)
}
fun <S, A, B> SuspendableStateResult<S, A>.flatMap( // 5
fn: suspend (A) -> SuspendableStateResult<S, B>
): SuspendableStateResult<S, B> = SuspendableStateResult { s0 ->
val (s1, res) = sst(s0)
res.fold(onSuccess = { a: A ->
fn(a).sst(s1)
}, onFailure = { thowable ->
s1 to Result.failure(thowable)
})
}
It’s a lot of code, but everything should be clear. In particular, you define:
-
SuspendStateResultTransformer<S, T>as a type of suspendable functions returning aPair<S, Result<T>>. -
SuspendableStateResult<S, T>as a data type encapsulating aSuspendStateResultTransformer<S, T>. -
liftto create aSuspendableStateResult<S, T>from a value of typeT. -
mapto apply a function of typeSuspendFun<A, B>to aSuspendableStateResult<S, A>to get aSuspendableStateResult<S, B>. -
flatMapto finally be able to compose functions returningSuspendableStateResult<S, T>.
Now, you’re finally ready to access your TV show information.
Finally flatMap
It’s finally time to put everything together so you can access the TVmaze database. Open ShowSearchService.kt, and add the following code:
val fetchSuspendResult: (String) -> SuspendableStateResult<
CoroutineContext, String> = { query ->
SuspendableStateResult { ctx: CoroutineContext ->
ctx to fetchTvShowResult(ctx, query)
}
}
val parseSuspendResult: (String) -> SuspendableStateResult<
CoroutineContext, List<ScoredShow>> = { json ->
SuspendableStateResult { ctx: CoroutineContext ->
ctx to parseTvShowResult(ctx, json)
}
}
Here, you create:
-
fetchSuspendResultof type(String) -> SuspendableStateResult<CoroutineContext, String>. -
parseSuspendResultof type(String) -> SuspendableStateResult<CoroutineContext, List<ScoredShow>>.
You can now compose these functions. Just add the following code to the same file:
@OptIn(FlowPreview::class) // 1
suspend fun searchTvShow(ctx: CoroutineContext) = // 2
withContext(ctx) {
inputStringFlow("Search Your Show: ") // 3
.flatMapConcat { query -> // 4
fetchSuspendResult(query)
.flatMap(parseSuspendResult).sst(ctx) // 5
.second.fold(
onSuccess = { it.asFlow() }, // 6
onFailure = { emptyFlow() }) // 7
}
}
In this code, you:
- Opt-in to the experimental
Flow<T>API. - Define
searchTvShowas a function accepting aCoroutineContextin input. - Invoke
inputStringFlow, passing aStringto use as a message. - Use the predefined
flatMapConcatto composeinputStringFlowwith the suspendable function you get by composingfetchSuspendResultandparseSuspendResult. - Pass the
CoroutineContextyou get as input to the function you get from the composition offetchSuspendResultandparseSuspendResult. - Return the value in
Result<List<ScoredShow>>as aFlow<ScoredShow>in case of success. - Return an empty
Flow<ScoredShow>in case of error.
To test this, add the following code:
@OptIn(FlowPreview::class)
fun main() {
runBlocking { // 1
searchTvShow(Dispatchers.IO) // 2
.collect { // 3
println("Score: ${it.score} " +
"Name: ${it.show.name} " +
"Genres: ${it.show.genres}") // 4
println(it.show.summary)
println("--------------------------")
}
}
}
Here, you:
- Use
runBlockingto give some scope tosearchTvShow. - Invoke
searchTvShow, passingDispatchers.IOasCoroutineContext. This allows your code to run in the background. - Collect all the results.
- Print its content, if any.
Now, you can run the previous code and have some fun, like this:
Everything works as expected, and this has been a great exercise to understand how to:
- Define the right abstraction.
- Reuse what you learned in the previous chapters of the book.
- Implement composition for the previous abstractions.
- Think in a functional way.
Great job!
The SharedFlow<T> & StateFlow<T> data types
SharedFlow<T> and StateFlow<T> are two additional flavors the coroutines API provides for flows. In terms of data types and the functions they provide, you can think of SharedState<T> and StateFlow<T> as implementations of Flow<T> with specific behavior when collected by multiple collectors.
For this reason, all the concepts you’ve seen so far are also valid for SharedState<T> and StateFlow<T>.
Key points
- The
List<T>data type allows you to store an ordered collection of elements of typeTin an eager way. - All the elements of a
List<T>, which is immutable, are present at the moment you create it. - The
List<T>data type is a functor and monad because of the presence ofmapandflatMap. You can also make it an applicative functor by implementingap. - The
Sequence<T>data type allows you to generate a sequence of values of typeTin a lazy way. - In a
Sequence<T>,mapandflatMapConcatare invoked when the values need to be collected and consumed. - A
Sequence<T>can work as a functor, applicative functor and monad. - The
Flow<T>data type is similar toSequence<T>but in the context of a coroutine. - Suspendable functions are an idiomatic and powerful tool to handle side effects in Kotlin.
- A
Flow<T>allows you to generate a sequence, or flow, of values of typeTthat can be generated from suspendable functions. - You can implement
curryand composition for suspendable functions as you did for non-suspendable ones, just following the functional programming principles you learned in the previous chapters. - You can repeat for
SharedFlow<T>andStateFlow<T>the same process you followed for aFlow<T>.
Where to go from here?
Congratulations! In this chapter, you had the opportunity to apply concepts you learned in the previous chapter in a concrete example that allowed you to fetch information about your favorite TV shows. You’ve learned how to create Sequence<T> and how to use Flow<T> in an environment of concurrency. Finally, you’ve empowered your functional thinking by implementing abstractions for composing suspendable functions, returning a Result<T> monad. It’s been a lot of work and also a lot of fun!
As mentioned previously, you can take a look at Kotlin Coroutines by Tutorials to learn more about coroutines, SharedFlow<T> and StateFlow<T>. The following chapters will talk about a couple more libraries that embody functional programming principles.