19.
Arrow
Written by Massimo Carli
In the previous chapters, you learned the fundamental concepts of functional programming. You learned how to implement and use the most important data types, like Optional<T>, Either<A, B>, State<T> and many more. Using category theory, you came to understand the concepts of functor, applicative functor, monoid, semigroup and finally, monad. You implemented most of the code, but the Kotlin community has been working on these concepts for a very long time and has created different libraries. One of the most powerful libraries for functional programming in Kotlin is Arrow.
Note: If you want to learn all about Arrow, read Arrow’s official documentation, which is perfectly maintained by 47 Degrees.
To cover all the features this library provides in a single chapter is impossible. For this reason, you’ll focus on a specific problem: error handling. Using Arrow, you’ll see how to handle errors in a functional way and learn what data types Arrow provides. This is the Arrow solution to what you did in Chapter 14, “Error Handling With Functional Programming”.
In particular, you’ll see:
-
The
Option<T>data type. -
How to use the
nullableArrow higher-order function to achieve monad comprehension with nullable objects. -
How to use
Either<A, B>and achieve monad comprehension witheither. -
What Arrow optics are and how you can use them to handle complex immutable objects easily.
It’s time to have more fun! :]
Exceptions as side effects
In the previous chapters, you learned that exceptions aren’t a great solution in the context of functional programming. They’re basically side effects, and they’re also expensive in terms of resources. Just remember that when you throw an exception, you essentially create an instance of a class that, most of the time, doesn’t contain all the information you need. In Java — and Kotlin — you also have different types of exceptions that differ in name and not much more.
To see what tools Arrow provides for handling exceptions in a functional way, you’ll start with the same code you wrote in Chapter 14, “Error Handling With Functional Programming”, to fetch and parse data about some TV shows. Open the starter project in this chapter’s material, and look at the code in the tools subpackages. In TvShowFetcher.kt in tools.fetchers, you’ll find the following code:
object TvShowFetcher {
fun fetch(query: String): String {
val encodedUrl = java.net.URLEncoder.encode(query, "utf-8")
val localUrl =
URL("https://api.tvmaze.com/search/shows?q=$encodedUrl")
with(localUrl.openConnection() as HttpURLConnection) {
requestMethod = "GET"
val reader = inputStream.bufferedReader()
return reader.lines().toArray().asSequence()
.fold(StringBuilder()) { builder, line ->
builder.append(line)
}.toString()
}
}
}
This is quite straightforward and allows you to query the TVmaze database by passing a string as input and getting a JSON with the response as output. Run the previous method, executing the following main:
fun main() {
fetch("Big Bang") pipe ::println
}
As output, you’ll get a long JSON String like the following:
[{"score":1.1548387,"show":{"id":58514,"url":"https://www.tvmaze.com/shows/58514/big-bang","name":"Big bang","type":"Panel Show","language":"Norwegian","genres":["Comedy"],"status":"Ended","runtime":60,"averageRuntime":60,
// ...
canine sidekick, Lil' Louis.</p>","updated":1628627563,"_links":{"self":{"href":"https://api.tvmaze.com/shows/10115"},"previousepisode":{"href":"https://api.tvmaze.com/episodes/531559"}}}}]
You can also simulate a case where something goes wrong. Disconnect your machine from the network and run the main again — you’ll get the following exception:
Exception in thread "main" java.net.UnknownHostException: api.tvmaze.com
at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:196)
at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:394)
at java.net.Socket.connect(Socket.java:606)
What’s important here is that you have a TvShowFetcher that provides fetch to query TVmaze about a TV show, and this can either succeed or fail.
Now, open TvShowParser.kt in tools.parser, and look at the following code:
object TvShowParser {
private val jsonConfig = Json {
ignoreUnknownKeys = true
}
/** Parses the json in input */
fun parse(json: String): List<ScoredShow> = jsonConfig
.decodeFromString<List<ScoredShow>>(
ListSerializer(ScoredShow.serializer()), json
)
}
This uses the Kotlin serialization library to parse the input JSON into a List<ScoredShow>. It can also either succeed or fail. To test the second case, simply run:
fun main() {
TvShowParser.parse("Invalid JSON") pipe ::println
}
And get the output:
Exception in thread "main" kotlinx.serialization.json.internal.JsonDecodingException: Expected start of the array '[', but had 'EOF' instead
JSON input: Invalid JSON
at kotlinx.serialization.json.internal.JsonExceptionsKt.JsonDecodingException(JsonExceptions.kt:24)
Given these two functions, how can you fetch and parse the JSON from the server in a functional way? You already know the answer, but it’s useful to see what Arrow provides.
Using Option<T>
A first possible solution to the previous problem is the Option<T> data type Arrow provides with the core library. Find it already added it to your project with the following definition in build.gradle:
def arrow_version = '1.0.1'
dependencies {
// ...
implementation "io.arrow-kt:arrow-core:$arrow_version"
}
Note: Notice that the name for this data type is
Option<T>, notOptional<T>.
To see how this works, open TvShowOption.kt, and add the following code:
fun fetchOption(query: String): Option<String> = try { // 1
TvShowFetcher.fetch(query).some() // 2
} catch (ioe: IOException) {
none() // 3
}
fun parseOption(
json: String
): Option<List<ScoredShow>> = try { // 1
TvShowParser.parse(json).some() // 2
} catch (e: Throwable) {
none() // 3
}
In this code, you:
- Define
fetchOptionandparseOptionasOption<T>versions ofTvShowFetcher::fetchandTvShowParser::parse, respectively. - Invoke the related function in a
try/catchblock. If the invocation is successful, you invoke thesomeextension function, which returns anOption<T>, and more precisely, an instance ofSome<T>. - In the case of an exception, return
None, invoking thenoneextension function.
If you look at some and none implementation, you find simple code like this:
public fun <A> A.some(): Option<A> = Some(this)
public fun <A> none(): Option<A> = None
Where Some<T> and None are specific definitions of the sealed class Option<T>, just as in the previous chapters. This is all quite simple.
Now, you need to fetch and parse the result while handling possible failure. You already know how to do it. Just add the following code to the same file:
fun fetchAndParseOption(
query: StringBuilder
): Option<List<ScoredShow>> =
fetchOption(query)
.flatMap(::parseOption)
The Option<T> data type provides flatMap to compose functions the same way you did in the previous chapters.
To test everything together, use the following code:
fun main() {
val searchResultOption = fetchAndParseOption("Big Bang") // 1
if (searchResultOption.isDefined()) { // 2
val searchResult =
searchResultOption.getOrElse { emptyList() } // 3
if (!searchResult.isEmpty()) {
searchResult.forEach { // 4
with(it.show) {
println(
"Name: ${name} Genre: ${genres.joinToString()}"
)
}
}
} else {
println("No Results!") // 5
}
} else {
println("Something went wrong!") // 6
}
}
In this code, you:
- Invoke
fetchAndParseOption, passing theStringyou want to search. This will return anOption<List<ScoredShow>>. - Use
isDefinedto check if theOption<T>has a value and if it’s aSome<T>orNone. - Invoke
getOrElse, which returns what’s in theSome<T>if present, or the result of the lambda you pass as the parameter in the case ofNone. In this case, you already know you’re in theSome<T>case, butgetOrElseis the safe way to get a value of typeTin any case. - Check that the result isn’t empty, and display all the
ScoredShowdata as output, iterating over the resultingList<ScoredShow>. - Display a simple message if you get no results.
- Print an error message if the initial result was
None.
This is a pretty good solution if you’re not interested in the specific reason for failure.
At this point, you might ask why you need Option<T> in Kotlin when you already have the opportunity to use optional types. This is an excellent question, and Arrow has an answer for it.
Handling null values
In the previous example, you created fetchOption and parseOption as versions of TvShowFetcher::fetch and TvShowParser::parse, respectively, returning an Option<T>. Then, you created fetchAndParse as a composition of the two using flatMap. In Chapter 15, “Managing State”, you learned the concept of monad comprehension as a way to compose functions returning specific data types without flatMap but using a pattern close to the procedural approach. Well, this is what Arrow provides you for nullable types.
Open TvShowNullable.kt, and write the following code:
fun fetchNullable(query: String): String? = try { // 1
TvShowFetcher.fetch(query) // 2
} catch (ioe: IOException) {
null // 3
}
fun parseNullable(json: String): List<ScoredShow>? = try { // 1
TvShowParser.parse(json) // 2
} catch (e: Throwable) {
null // 3
}
As you can see here, you:
- Define
fetchNullableandparseNullableas versions ofTvShowFetcher::fetchandTvShowParser::parse, respectively, returning a value of the optional type. - Return the actual result in the case of success.
- Return
nullin the case of failure.
Using the flatMap implementation you find in Nullable.kt in the lib package, add the following:
fun fetchAndParseNullableFlatMap(query: String) =
fetchNullable(query)
.nullableFlatMap(::parseNullable)
Just remember to import nullableFlatMap like this to avoid conflicts with other flatMap extension functions:
import com.raywenderlich.fp.lib.flatMap as nullableFlatMap
Arrow provides you an alternative solution. In the same file, add the following code:
suspend fun fetchAndParseNullable(
query: String
): List<ScoredShow>? = nullable {
val json = fetchNullable("Big Bang").bind()
val result = parseNullable(json).bind()
result
}
This code has a few interesting things to note:
-
fetchAndParseNullableis a suspendable function. This is because thenullablefunction, which is suspendable, allowsfetchNullableandparseNullableto be suspendable functions as well. This is very useful, especially if you need to access the network, like in this case, or, in general, run tasks in a thread different from the main one. - You invoke
fetchNullableandparseNullableone after the other in the same way you would in a procedural case. The magic is provided by thenullablefunction but also by thebindyou invoke on the result of each invocation. This is how Arrow handlesNullableEffect<T>, which is anEffect<T>that can result in anullvalue. - The result type is
List<ScoredShow>?, and you explicitly wrote it in the previous code to make this clear, but it wouldn’t have been necessary. This is the type of the last expression you write in the body ofnullable.
To test this code, add the following to the same file:
suspend fun mainWithComprehension() { // 1
val searchResultOption = fetchAndParseNullable("Big Bang") // 2
if (searchResultOption != null) {
printScoresShow(searchResultOption) // 3
} else {
println("Something went wrong!")
}
}
Which isn’t very different from the one you would’ve executed in the case of using flatMap, which is:
fun mainWithFlatMap() { // 1
val searchResultOption = fetchAndParseNullableFlatMap("Big Bang") // 2
if (searchResultOption != null) {
printScoresShow(searchResultOption) // 3
} else {
println("Something went wrong!")
}
}
The differences are:
- The one using
nullableneeds to be a suspendable function. - The function you invoke to get the optional type
List<ScoredShow>?. - You use
printScoresShow, which you find in Util.kt, to print theList<ScoredShow>content.
Finally, run the following main. This lets you check how the two implementations work basically the same — besides the need for a scope for the suspendable one:
fun main() {
mainWithFlatMap()
runBlocking {
mainWithComprehension()
}
}
Which one is the best? The answer is always the same: It depends! :] If you need to compose different functions that execute tasks in the background, the Arrow solution is probably the best.
In both cases, you get a nullable object that doesn’t give you any information about what happens in the case of failure. You just get null without any other information. You’ve already met this problem in the previous chapters, and you already have a first solution: the Either<A, B> data type.
Using Either<A, B>
In the case of Option<T> and the use of the nullable function, you didn’t have any information in the case of failure — you just get a null value. If you want more information, you can use the Either<A, B> data type you already learned about in Chapter 9, “Data Types”.
Open TvShowEither.kt, and add the following code:
fun fetchEither(
query: String
): Either<IOException, String> = try { // 1
TvShowFetcher.fetch(query).right() // 2
} catch (ioe: IOException) {
ioe.left() // 3
}
fun parseEither(
json: String
): Either<Throwable, List<ScoredShow>> = try { // 1
TvShowParser.parse(json /* + "break" */).right() // 2
} catch (e: Throwable) {
e.left() // 3
}
In this code, you:
- Define
fetchEitherandparseEitheras versions ofTvShowFetcher::fetchandTvShowParser::parse, respectively, using the ArrowEither<A, B>implementation. - Use the
rightextension function, returning aRight<B>of the result in the case of success. Note howEither<A, B>is right biased, which means you conventionally useRight<B>as the success value andLeft<A>for failure. This is important in the case of composition usingflatMap. Note how the parameter forTvShowParser::parsehas a commented code you can use to simulate a failure. - Use the
leftextension function on the exception in the case of an error.
Now, add the following code to the same file:
fun fetchAndParseEither(
query: String
): Either<Throwable, List<ScoredShow>> =
fetchEither(query)
.flatMap(::parseEither)
Here, you simply use flatMap to compose the fetchEither and parseEither using the mentioned right bias. To test this, just run the following code:
fun main() {
fetchAndParseEither("Big Bang") // 1
.fold( // 2
ifRight = ::printScoresShow, // 3
ifLeft = ::printException // 4
)
}
Here, you:
-
Invoke
fetchAndParseEither, passing the query in input. -
Use
foldto handle success cases, the right, as well as failure cases, the left. -
Pass the reference to
printScoresShowto theifRightparameter in the case ofRight<B>, which represents the successful case. -
Pass the reference to
printExceptionin the case ofLeft<A>, which represents the failure case.
You can now simulate the failure of fetchEither by disconnecting your machine from the network. In that case, you’ll get:
Error api.tvmaze.com
To simulate the failure of parseEither, just remove the comments from the code like here and, of course, restore your connection:
fun parseEither(json: String): Either<Throwable, List<ScoredShow>> = try {
TvShowParser.parse(json + "break").right() // HERE
} catch (e: Throwable) {
e.left()
}
And you’ll get:
Error Unexpected JSON token at offset 14069: Expected EOF after parsing, but had b instead
JSON input: .....aze.com/episodes/531559"}}}}]break
This works exactly like the Either<A, B> data type you implemented in Chapter 9, “Data Types”, but Arrow gives you the monad comprehension power as well. In this case, you can use the either function like this:
suspend fun fetchAndParseEitherComprehension( // 1
query: String
): Either<Throwable, List<ScoredShow>> =
either { // 2
val json = fetchEither(query).bind() // 3
val result = parseEither(json).bind() // 4
result
}
Here, you:
-
Define
fetchAndParseEitherComprehensionas a suspendable function. -
Use the
eitherfunction. -
Invoke
bindon the result offetchEither. -
Use the result you get from
fetchEitheras input forparseEitherand invokebindagain.
To test this code, just repeat the previous scenarios on the following:
fun main() {
runBlocking {
fetchAndParseEitherComprehension("Big Bang")
.fold(
ifRight = ::printScoresShow,
ifLeft = ::printException
)
}
}
You can easily verify that the results will be the same.
Arrow optics
One of the most important principles in functional programming is immutability. An object is immutable if it doesn’t change its state after creation. A class is immutable if it doesn’t provide the operation to change the state of its instances.
As you know, the state of an object is the set of values for all its properties. Immutable objects have many different advantages. For instance, different threads can safely share them without any risk of deadlock or race conditions.
Sometimes you need to “update” the state of an immutable object. Wait, what? Well, in Chapter 18, “Mobius — A Functional Reactive Framework”, you saw that the Update function returns the new state and an optional set of effects, given the current state and the information about an event. Often, the new state has different values for some properties, leaving the remaining unchanged.
To solve problems like this, Arrow provides the optics library. It’s an automatic DSL that allows users to use dot notation when accessing, composing and transforming deeply nested immutable data structures.
To understand how this works, open Optics.kt, and add the following code:
val bigBangTheory =
ScoredShow(
score = 0.9096895,
Show(
id = 66,
name = "The Big Bang Theory",
genres = listOf("Comedy"),
url = "https://www.tvmaze.com/shows/66/the-big-bang-theory",
image = ShowImage(
original = "", // HERE
medium = "https://static.tvmaze.com/uploads/images/medium_portrait/173/433868.jpg"
),
summary = "<p><b>The Big Bang Theory</b> is a comedy about brilliant physicists, Leonard and Sheldon...</p>",
language = "English"
)
)
Here, you just create bigBangTheory with the values you get from the TVmaze database. To make the core more readable, you used named parameters and made the summary shorter. Now, imagine that when you got this data, the original version for the image wasn’t available, as you can see with the empty value. Now that information is available, and you want to update bigBangTheory with the new value.
A solution is the following:
val updatedBigBangTheory = bigBangTheory.copy(
show = bigBangTheory.show.copy( // 1
image = bigBangTheory.show.image?.copy( // 2
original = "https://static.tvmaze.com/uploads/images/medium_portrait/173/433868.jpg" // 3
)
)
)
In this code, you use copy to update the:
-
showproperty of theScoredShowwith an updated version ofShow. -
imageproperty of theShowwith an updated version ofShowImage. -
originalproperty with the new value.
You can check that this works by running the following code:
fun main() {
bigBangTheory pipe ::println
updatedBigBangTheory pipe ::println
}
And check that the original property now has a value.
ScoredShow(score=0.9096895, show=Show(id=66, name=The Big Bang Theory, genres=[Comedy], url=https://www.tvmaze.com/shows/66/the-big-bang-theory, image=ShowImage(original=, medium=https://static.tvmaze.com/uploads/images/medium_portrait/173/433868.jpg), summary=<p><b>The Big Bang Theory</b> is a comedy about brilliant physicists, Leonard and Sheldon...</p>, language=English))
ScoredShow(score=0.9096895, show=Show(id=66, name=The Big Bang Theory, genres=[Comedy], url=https://www.tvmaze.com/shows/66/the-big-bang-theory, image=ShowImage(original=https://static.tvmaze.com/uploads/images/medium_portrait/173/433868.jpg, medium=https://static.tvmaze.com/uploads/images/medium_portrait/173/433868.jpg), summary=<p><b>The Big Bang Theory</b> is a comedy about brilliant physicists, Leonard and Sheldon...</p>, language=English))
As mentioned, this works, but that’s a lot of code for just updating a property of an immutable object. With Arrow, you can use lenses, which are provided with the optics library. A lens is exactly what its name suggests. It’s a way to reach a property that might be very deep in a hierarchy of objects and basically see it as if it were closer.
Note: The project is already configured to use optics. Please be careful if you want to update the library and plugin versions because they’re strictly connected. A wrong version could cause the code to simply not work.
To use a lens, open Show.kt in model, and update all the data classes in it like this:
@optics // 1
@Serializable
data class ScoredShow(
val score: Double,
val show: Show
) {
companion object // 2
}
@optics // 1
@Serializable
data class Show(
val id: Int,
val name: String,
val genres: List<String>,
val url: String,
val image: ShowImage?,
val summary: String?,
val language: String
) {
companion object // 2
}
@optics // 1
@Serializable
data class ShowImage(
val original: String,
val medium: String
) {
companion object // 2
}
In this code, you:
- Annotate each data class with
@optics. This will cause the Arrow compiler to generate all the code you need. - Provide a companion object to each of the
@opticsclasses. Arrow needs this to attach all the generated extensions functions.
Now, simply return to Optics.kt, and add the following code:
fun main() {
val updateOriginalImageLens: Optional<ScoredShow, String> =
ScoredShow.show.image.original // 1
val updatedShow =
updateOriginalImageLens.modify(bigBangTheory) { // 2
"https://static.tvmaze.com/uploads/images/medium_portrait/173/433868.jpg"
}
updatedShow pipe ::println // 3
}
Note: Make sure you rebuild the project and import the generated
showandimageproperties to get this to compile.
Here, you:
-
Use the dot notation to get the reference of an object of type
Optional<ScoredShow, String>. This function basically allows you to go from theoriginalScoredShowto the updated one in a single step. You save it inupdateOriginalImageLens. If you considerOptional<S, T>, you can think ofSas the original type andTas the type of the variable you want to update inS. -
Invoke
modifyonupdateOriginalImageLens, passing the original object and the new value of the property you want to update. -
Finally, print the updated object,
updatedShow, to verify that it actually worked.
Running the previous code, you’ll get:
ScoredShow(score=0.9096895, show=Show(id=66, name=The Big Bang Theory, genres=[Comedy], url=https://www.tvmaze.com/shows/66/the-big-bang-theory, image=ShowImage(original=https://static.tvmaze.com/uploads/images/medium_portrait/173/433868.jpg, medium=https://static.tvmaze.com/uploads/images/medium_portrait/173/433868.jpg), summary=<p><b>The Big Bang Theory</b> is a comedy about brilliant physicists, Leonard and Sheldon...</p>, language=English))
Note how the original property now has a value.
Arrow lenses
In the previous section, you learned that optics are basically abstractions that help you update immutable data structures in an elegant and functional way. A lens is an optic that can focus into a structure and get, modify or set the value of a particular property.
In terms of functional types, you can say that a Lens<T, A> is a way to aggregate a couple of functions:
-
getof type(T) -> A, which extracts the value of typeAfrom the object of typeT. -
setterof type(A) -> (T) -> Twhich, given a value of typeAfor a property, provides the function you run to update the object of typeT.
Given a Lens<T, A>, you call T the source of the lens and A its target.
Lenses can be seen as a pair of functions: a getter and a setter. A Lens<S, A> represents a getter: get: (S) -> A, and setter: set: (A) -> (S) -> S, where S is the source of the lens and A is the focus or target of the lens.
Open Lens.kt, and add the following code:
val addGenreLens: Lens<ScoredShow, List<String>> = Lens( // 1
get = { scoredShow -> scoredShow.show.genres }, // 2
set = { scoredShow, newGenres ->
ScoredShow.show.genres.modify(scoredShow) { // 3
scoredShow.show.genres + newGenres
}
}
)
In this code, you define:
-
addGenreLensas a lens of typeLens<ScoredShow, List<String>>, which wants to allow the addition of new genres to a givenScoredShow. - The
getfunction using a lambda that simply accesses and returns the value of thegenresproperty of theshowfor theScoredShowin input. - The
setfunction using a lambda receiving as input aScoredShowand theList<String>for the genre to append. Note how you use the lenses Arrow already creates for you, as you learned above.
To use addGenreLens, simply add the following code to the same file:
fun main() {
addGenreLens.set(
bigBangTheory, listOf("Science", "Comic")
) pipe ::println
}
Run it, and you’ll get the following output with the new genres added:
ScoredShow(score=0.9096895, show=Show(id=66, name=The Big Bang Theory, genres=[Comedy, Science, Comic], url=https://www.tvmaze.com/shows/66/the-big-bang-theory, image=ShowImage(original=, medium=https://static.tvmaze.com/uploads/images/medium_portrait/173/433868.jpg), summary=<p><b>The Big Bang Theory</b> is a comedy about brilliant physicists, Leonard and Sheldon...</p>, language=English))
But lenses aren’t just a way to write more concise code. Suppose you want to update the name for a given show. In the same file, add the following code:
val showLens: Lens<ScoredShow, Show> = Lens( // 1
get = { scoredShow -> scoredShow.show },
set = { scoredShow, newShow -> scoredShow.copy(show = newShow) }
)
val nameLens: Lens<Show, String> = Lens( // 2
get = { show -> show.name },
set = { show, newName -> show.copy(name = newName) }
)
Here, you define:
-
showLensas a lens to update theShowin aScoredShow. -
nameLensas a lens to update thenamein aShow.
If you want to then update the name of a Show of a given ScoredShow, you can simply compose the previous lenses like this:
fun main() {
// ...
val updateName = showLens compose nameLens
updateName.modify(bigBangTheory, String::toUpperCase) pipe ::println
}
Running this code, you’ll get:
ScoredShow(score=0.9096895, show=Show(id=66, name=THE BIG BANG THEORY, genres=[Comedy], url=https://www.tvmaze.com/shows/66/the-big-bang-theory, image=ShowImage(original=, medium=https://static.tvmaze.com/uploads/images/medium_portrait/173/433868.jpg), summary=<p><b>The Big Bang Theory</b> is a comedy about brilliant physicists, Leonard and Sheldon...</p>, language=English))
As you can see, the name is now capitalized.
Key points
- Arrow is a library maintained by 47 Degrees that allows you to apply functional programming concepts to your Kotlin code.
- Arrow provides the implementation for the most important data types, like
Optional<T>,Either<A, B>,Monoid<T>and many others. - Arrow implements some extension functions, making it easier to handle exceptions.
- Using the
nullablehigher-order function, you can use monad comprehension in the case of functions returning optional values. - Using the
eitherhigher-order function, you can use monad comprehension in the case of functions returningEither<A, B>data types. - Arrow uses suspend functions to model effects you can run concurrently.
- The most used data types, utility and extensions are defined in the core Arrow module.
- Using the optics library, you can generate the code for reducing boilerplate in the case of handling immutable objects.
- A lens is an abstraction that helps you access properties and create new objects from existing immutable ones.
- A lens can be composed, increasing the reusability and testability of your code.
Where to go from here?
Wow, congratulations! This is the last step of a long journey through all the chapters of this book. Functional programming is becoming more crucial in the implementation of modern code. Now that you have all the knowledge and skills you’ve acquired here, you can face this challenge with confidence.