Friday, February 14, 2014

Kotlin: An Option type #1

This pair of blogs introduces the Kotlin Option type that allows the programmer to specify where something is present or absent. It can be used in place of null to denote absence of a result. The Option type offers more than a new data type: more powerful abstractions, such as higher order functions, that hide many of the details of mundane operations such as mapping a function across a data type. We get to focus on the what, not the how making our code more declarative: incentivizing us to make our code blocks simpler.

Probably all Java developers have experienced a NullPointerException. Usually this occurs because some function returns it when not expected and when not dealing with that possibility in your client code. The value null is also used to represent the absence of a value such as when performing the member function get on a Map.

Kotlin, of course, allows you to work safely with null. The null-safe operator for accessing properties is foo?.bar?.baz will not throw an exception if either foo or its bar property is null. Instead, it returns null as its result.

An alternative approach that seeks to reduce the need for null is to provide a type for representing optional values, i.e. values that may be present or not: the OptionIF<T> trait. By stating that a value may or mat not be present on the type level, you and other developers are required by the compiler to deal with this possibility. Further, by providing various combinators we can chain together function calls on option values.

Kotlin OptionIF<T> is a container for zero or one element of a given type. An OptionIF<T> can be either a Some<T> wrapping a value of type T, or can be a None<T> which represents a missing value. You create an OptionIF<T> by instantiating the Some or None classes:

val name: OptionIF<String> = Some("Ken Barclay")
val absentName: OptionIF<String> = None<String>()

Given an instance of OptionIF<T> and the need to do something with it, how is this done? One way would be to check if a value is present by means of the isDefined member function, and if that is the case obtain the value with the get member function. This is shown in Example 1.

Example 1Accessing options

package example1

import com.adt.kotlin.data.immutable.option.*

fun main(args: Array<String>) {
    val op: OptionIF<Int> = Some(5)
    if (op.isDefined())
        println("op: ${op.get()}")  // => op: 5
}

Deliberately, I have chosen to include types explicitly as an aid to the reader. Of course, Kotlin's type inferencing would allow me to omit some and, in turn, make the code more compact.

The most common way to take optional values apart is through a pattern match. Example 2 introduces the show function which pattern matches on the OptionIF parameter and for a Some instance the member function get is used to retrieve the enclosed value.

Example 2: Pattern matching options

package example2

import com.adt.kotlin.data.immutable.option.*

fun show(op: OptionIF<String>) {
    when (op) {
        is Some<*> -> println(op.get())
        else -> println("Missing")
    }
}

fun main(args: Array<String>) {
    val name: OptionIF<String> = Some("Ken Barclay")
    val absentName: OptionIF<String> = None<String>()

    show(name)              // => Ken Barclay
    show(absentName)    // => Missing
}

If you think this is clunky and expect something more elegant from Kotlin OptionIF<T> you are correct. If you use the member function get, you may forget about checking with isDefined leading to a runtime exception, and this scenario is no different from using null. You should avoid using options this way. One simple improvement we can make to these use cases is provided by the getOrElse member function as shown in Example 3.

Example 3: Provide a default

package example3

import com.adt.kotlin.data.immutable.option.*

fun main(args: Array<String>) {
    val name: OptionIF<String> = Some("Ken Barclay")
    val absentName: OptionIF<String> = None<String>()

    println(name.getOrElse("Missing"))              // => Ken Barclay
    println(absentName.getOrElse("Missing"))    // => Missing

}

I suggested that we consider an option as a collection of zero or one elements. Consequently it is provided with many of the behaviors we associate with containers such as filtering, mapping and folding. Example 4 illustrates transforming an OptionIF<String> into an OptionIF<Int>. When you map an OptionIF<String> that is a None<String> then you get a None<Int>.

Example 4: Mapping

package example4

import com.adt.kotlin.data.immutable.option.*

fun main(args: Array<String>) {
    val name: OptionIF<String> = Some("Ken Barclay")
    val absentName: OptionIF<String> = None<String>()

    println(name.map{(str: String) -> str.length()}.getOrElse(0))         // => 11
    println(absentName.map{str -> str.length()}.getOrElse(0))           // => 0
}

The member function map is a higher order function, accepting a transformer function as parameter. Using customizable higher order functions allows us to think about solutions differently. Function map allows us to apply a generic operation to the data type and means we can focus on the result.

A somewhat more elaborate illustration is given in Example 5 which implements a repository of users. We need to be able to find a user by their unique id. A request made with a non-existent id suggests a return type of OptionIF<User>.

Example 5: User repository

package example5

import com.adt.kotlin.data.immutable.option.*

class User(val id: Int, val firstName: String, val lastName: String, val age: Int)

class UserRepository {

    fun findById(id: Int): OptionIF<User> {
        return if (users.containsKey(id)) {
            val user: User = users.get(id)!!
            Some(user)
        } else
            None<User>()
    }

// ---------- properties ----------------------------------

    val users: Map<Int, User> = hashMapOf(
            1 to User(1, "Ken", "Barclay", 25),
            2 to User(2, "John", "Savage", 31)
    )
}

fun main(args: Array<String>) {
    val repository: UserRepository = UserRepository()
    val user: OptionIF<User> = repository.findById(1)
    val userName: OptionIF<String> = user.map{u -> "${u.firstName} ${u.lastName}"}
    if (userName.isDefined())
        println("User: ${userName.get()}")      // => User: Ken Barclay
}

Our dummy implementation uses a HashMap. Perhaps we might develop a Map<K, V> implementation with a lookUpKey member function that returns an OptionIF<V>.

In the follow-on blog I will consider some more advanced use-cases with OptionIF<T>. I aim to show how more powerful abstractions over the OptionIF<T> type can reduce the complexity of functions, encouraging us to cede control to these abstractions and remove the need to code each atomic step in an algorithm.


3 comments:

ctran said...

Where is this OptionIF type from?

Greg Zak said...

Hello Ken,
I have really appreciated your writing on Groovy both here in your blog and in your book.

Apologies for using your blog comment to ask this, as it's not related to the post, but I'm running out of paths to the answer.

The Groovy Programming Book solutions/example/code site put up via the University is as I'm sure you know, down. Do you have an alternate location where I might find those. Or can you email them to me directly. I'm a new programmer and would like the assurance that I'm going about groovy properly.
Thanks.
gzak001@yahoo.com

uri said...

Any way you can make this avail in Maven?
I've tried to use the lib you provided in your git repo but for some reason, it's not recognizing the classes inside of it.
I'm guessing because you built it using an older version of Java.