Chapters

Hide chapters

Functional Programming in Kotlin by Tutorials

First Edition · Android 12 · Kotlin 1.6 · IntelliJ IDEA 2022

Section I: Functional Programming Fundamentals

Section 1: 8 chapters
Show chapters Hide chapters

Appendix

Section 4: 13 chapters
Show chapters Hide chapters

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

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 nullable Arrow higher-order function to achieve monad comprehension with nullable objects.

  • How to use Either<A, B> and achieve monad comprehension with either.

  • 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"
}
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
}
public fun <A> A.some(): Option<A> = Some(this)

public fun <A> none(): Option<A> = None
fun fetchAndParseOption(
  query: StringBuilder
): Option<List<ScoredShow>> =
  fetchOption(query)
    .flatMap(::parseOption)
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
  }
}

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.

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
}
fun fetchAndParseNullableFlatMap(query: String) =
  fetchNullable(query)
    .nullableFlatMap(::parseNullable)
import com.raywenderlich.fp.lib.flatMap as nullableFlatMap
suspend fun fetchAndParseNullable(
  query: String
): List<ScoredShow>? = nullable {
  val json = fetchNullable("Big Bang").bind()
  val result = parseNullable(json).bind()
  result
}
suspend fun mainWithComprehension() { // 1
  val searchResultOption = fetchAndParseNullable("Big Bang") // 2
  if (searchResultOption != null) {
    printScoresShow(searchResultOption) // 3
  } else {
    println("Something went wrong!")
  }
}
fun mainWithFlatMap() { // 1
  val searchResultOption = fetchAndParseNullableFlatMap("Big Bang") // 2
  if (searchResultOption != null) {
    printScoresShow(searchResultOption) // 3
  } else {
    println("Something went wrong!")
  }
}
fun main() {
  mainWithFlatMap()
  runBlocking {
    mainWithComprehension()
  }
}

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”.

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
}
fun fetchAndParseEither(
  query: String
): Either<Throwable, List<ScoredShow>> =
  fetchEither(query)
    .flatMap(::parseEither)
fun main() {
  fetchAndParseEither("Big Bang") // 1
    .fold( // 2
      ifRight = ::printScoresShow, // 3
      ifLeft = ::printException // 4
    )
}
Error api.tvmaze.com
fun parseEither(json: String): Either<Throwable, List<ScoredShow>> = try {
  TvShowParser.parse(json + "break").right() // HERE
} catch (e: Throwable) {
  e.left()
}
Error Unexpected JSON token at offset 14069: Expected EOF after parsing, but had b instead
JSON input: .....aze.com/episodes/531559"}}}}]break
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
  }
fun main() {
  runBlocking {
    fetchAndParseEitherComprehension("Big Bang")
      .fold(
        ifRight = ::printScoresShow,
        ifLeft = ::printException
      )
  }
}

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.

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"
    )
  )
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
    )
  )
)
fun main() {
  bigBangTheory pipe ::println
  updatedBigBangTheory pipe ::println
}
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))
@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
}
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
}
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))

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.

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
    }
  }
)
fun main() {
  addGenreLens.set(
    bigBangTheory, listOf("Science", "Comic")
  ) pipe ::println
}
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))
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) }
)
fun main() {
  // ...
  val updateName = showLens compose nameLens
  updateName.modify(bigBangTheory, String::toUpperCase) pipe ::println
}
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))

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 nullable higher-order function, you can use monad comprehension in the case of functions returning optional values.
  • Using the either higher-order function, you can use monad comprehension in the case of functions returning Either<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.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now