} }
val memquery = ::query.memoized()
We could add similar memoized functions on the other FunctionN classes if we wished.
Type alias
Kotlin 1.1 has introduced a new feature for referring to verbose types called type aliases. As the name suggests, a type alias allow us to declare a new type that is simply an alias of an existing type. We do this using the typealias keyword:
typealias Cache = HashMap<String, Boolean>
They are especially useful as a replacement for complex type signatures. Compare the following and see which you think is more readable:
fun process(exchange: Exchange<HttpRequest, HttpResponse>):
Exchange<HttpRequest, HttpResponse>
Or:
typealias HttpExchange = Exchange<HttpRequest, HttpResponse>
fun process2(exchange: HttpExchange): HttpExchange
A typealias carries no runtime overhead or benefit. The alias is simply replaced by the compiler. This means that new types are not created or allocated, so we suffer no
performance penalty. It also means that two aliases that have the same right-hand side, can be used interchangeably. For example, these three definitions all reference a string:
typealias String1 = String typealias String2 = String
fun printString(str: String1): Unit = println(str) val a: String2 = "I am a String"
printString(a)
As you can see, we define the function to accept a String1, which is an alias of String. So we are able to pass in a String2, which is also a String.
This has the drawback that we cannot use type aliases to increase type safety on parameters of the same type. For example, consider a method called volume:
fun volume(width: Int, length: Int, height: Int): Int
If we change this to use type aliases for each of the dimensions, they can still be used interchangeably:
typealias Width = Int typealias Length = Int typealias Height = Int
fun volume(width: Width, length: Length, height: Height): Int We are able to invoke this function in any of the following erroneous ways:
val width: Width = 2 val length: Length = 3 val height: Height = 4
volume(width, length, height) volume(height, width, length) volume(width, width, width)
At the time of writing, type aliases must be declared at the top level.
Either
In most functional programming languages, there is a type called Either (or a synonym).
The Either type is used to represent a value that can have two possible types. It is common to see Either used to represent a success value or a failure value, although that doesn't have to be the case.
Although Kotlin doesn't come with an Either as part of the standard library, it's very easy to add one.
Let's start by defining a sealed abstract class with two implementations for each of the two possible types that Either will represent:
sealed class Either<out L, out R>
class Left<out L>(value: L) : Either<L, Nothing>() class Right<out R>(value: R) : Either<Nothing, R>()
It is usual to call the two implementations Left and Right. By convention, when Either class is representing success or failure, the Right class is used for the success type.
Fold
The first function we'll add to Either is the fold operation. This will accept two functions.
The first will be applied if the Either is an instance of the Left type, and the second will apply if the Either is the Right type. The return value from whichever function is applied will be returned:
sealed class Either<out L, out R> {
fun <T> fold(lfn: (L) -> T, rfn: (R) -> T): T = when (this) { is Left -> lfn(this.value)
is Right -> rfn(this.value) }
}
Let's see how it can be used. Firstly, let's create some basic classes that we will use for the rest of the examples in this section:
class User(val name: String, val admin: Boolean) object ServiceAccount
class Address(val town: String, val postcode: String)
Then let's say we had a function that retrieved the current user, and another function that returns their addresses for a particular user:
fun getCurrentUser(): Either<ServiceAccount, User> = ...
fun getUserAddresses(user: User): List<Address> = ...
Note that the getCurrentUser function returns an Either, which contains two types of user. One is a regular user, and the other is a special ServiceAccount. We can then use that Either to get the addresses for the user:
val addresses = getCurrentUser().fold({ emptyList<Address>() }, { getUserAddresses(it) })
As you can see, we handle the lookup depending on the type we were given. In this case, the service account doesn't have any addresses, so we just return an empty list.
Projection
It is common to see functionality on an Either that allows us to map, filter, get the value, and so on. These functions are defined so that they apply to one of the types only, and are no-ops in the other case. The usual name for this is a left or right projection.
The user will decide whether they are interested in the left or right cases, and, by invoking a function, will receive a projection that contains the value they are interested in, or no value if the type they want is not the type the Either contains.
The way we will choose to implement this is to create two projection subclasses: A
ValueProjection, and an EmptyProjection. The ValueProjection will implement the functions, and the EmptyProjection will implement no-ops. The Either class will then contain functions to get a projection for whichever side was requested.
Let's start by creating an abstract Projection class, which will define the functions we are interested in and be the supertype for both the implementing classes:
sealed class Projection<out T> {
abstract fun <U> map(fn: (T) -> U): Projection<U>
abstract fun getOrElse(or: () -> T): T }
We're going to start with two functions for now: map, which will transform the value if the projection is one we are interested in, and getOrElse, which will return the value or apply a default function. The next step is to implement this for both the classes:
class ValueProjection<out T>(val value: T) : Projection<T>() { override fun <U> map(fn: (T) -> U): Projection<U> =
ValueProjection(fn(value))
override fun getOrElse(or: () -> T): T = value }
class EmptyProjection<out T> : Projection<T>() { override fun <U> map(fn: (T) -> U): Projection<U> = EmptyProjection<U>()
override fun getOrElse(or: () -> T): T = or() }
fun <T> Projection<T>.getOrElse(or: () -> T): T = when (this) { is EmptyProjection -> or()
is ValueProjection -> this.value }
Note that the EmptyProjection just returns another instance of EmptyProjection without mapping anything. The ValueProjection actually performs the mapping.
getOrElse is implemented as an extension function on Projection itself because the function signature requires that T is an output in the or function. This breaks co-variance unless we use an extension function.
Variance is covered in a later chapter.
The final step is to update our Either class to return these projections when asked for:
sealed class Either<out L, out R> {
fun <T> fold(lfn: (L) -> T, rfn: (R) -> T): T = when (this) { is Left -> lfn(this.value)
is Right -> rfn(this.value) }
fun leftProjection(): Projection<L> = when (this) { is Left -> ValueProjection(this.value)
is Right -> EmptyProjection<L>() }
fun rightProjection(): Projection<R> = when (this) { is Left -> EmptyProjection<R>()
is Right -> ValueProjection(this.value)
}
Now, we can use this as follows:
val postcodes = getCurrentUser().rightProjection() .map { getUserAddresses(it) }
.map { addresses.map { it.postcode } } .getOrElse { emptyList() }
This is a similar method to the earlier example, but note how we can continue to map over the results, and then apply a default at the end. If the Either returned was not a Right value, then the maps would have no effect.