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 1: Accessing 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
}
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.
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
}
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
}
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:
Where is this OptionIF type from?
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
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.
Post a Comment