Skip to content

Instantly share code, notes, and snippets.

@janojanahan
Last active November 8, 2024 05:00
Show Gist options
  • Save janojanahan/6ab593ed2ff92d8480b61b70ba75ec7f to your computer and use it in GitHub Desktop.
Save janojanahan/6ab593ed2ff92d8480b61b70ba75ec7f to your computer and use it in GitHub Desktop.
Kotlin Nullable can behave like a monad
/**
* A Gist proving that Kotlin's nullable type can be made into a monad without wrapping into another
* object and satisfy the Monadic laws
*
* Kotlin has comprehensive null safety built into the language enforced at compile time, using its
* nullable type.
*
* Its language structure makes dealing with nullable values simple and succinct. Unlike other language
* monadic constructs such as Option (scala), Optional(Java8+) and Maybe(Haskell), it is enforced at
* compile time and is compatible with existing non monad aware API (for example,
* anObject.getNullableProperty() which may return a value or null).
* See: https://kotlinlang.org/docs/reference/null-safety.html
*
* Indeed the Monadic constructs of Scala and Java (Option/Optional) have there own interesting issue
* in that the actual Reference to the instance can be null eg: Optional<Int> n = null !!!!
*
* Some Functional purists however appear to assume that the nullable type on its own is not a Monad,
* and create new Monadic types such as Option (eg http://kenbarclay.blogspot.co.uk/2014/02/kotlin-option-type-1.html).
* or even, to avoid re-inventing the wheel, use the Java 8 Optional, which is perfectly useable in Kotlin
*
* I believe that approach is unnecessary as in fact Kotlin's nullable type can in fact be made to behave
* as a Monad creating just two extenstion functions (for bind and return), and follow the three monadic
* laws without having to create Yet Another Type.
*
* It is still not a pure nomad, as Any? is just a super class of Any, therefore the actual object is the same
* and not a container. As such it is handled distinctly by the compiler, which does the null safety checks.
*
* As such equality behaves differently to when there is a true container type monad. In traditional monads
* using Java 8 Optional
* val a = Optional.of(2)
* val b = 2;
* println(a.equals(b)) // displays false
*
* However:
* val a: Int = 2
* val b: Int? = 2
* println(a.equals(b)) // displays true
* println(a == b) // displays true
* println(a === b) // displays true
*
* This is due to type a and b actally having a common ancester of Any? and the compiler auto casting to that
* before doing the equality. Personally I think for the purposes of Null Safety this is not a bad thing,
* however purists may disagree
*
*
* For more information on monads and the three laws with respect to Haskell, see:
* http://learnyouahaskell.com/a-fistful-of-monads#monad-laws
*
* @author Jano Janahan <jano.janahan@gmail.com>
*/
/**
* Return funtion which places a value into its monadic context:
* T -> m T
*
* Implemented as an extension function for Any?.
*
* Note it is equivalent to simply assigning into a nullable type such as:
* val a: Int = 2
* val m: Int? = a // this does the same as .toNullable, and puts it into a monadic context
*/
fun <T> T.toNullable():T? = this
/**
* Bind function (m T >>= (T -> m V), named flatMap to be similar to Java and Scala
*/
fun <T, U> T?.flatMap(body: (T) -> U?): U? =
if (this != null) body(this) else null
fun f(x : Int) : Int? = (x * 2)
fun g(x : Int) : String? = ("$x pounds")
fun fThenG(x : Int) = f(x).flatMap(::g)
/**
* Law 1: Left identity
* The first monad law states that if we take a value, put it in a default context with return and
* then feed it to a function by using >>=, it's the same as just taking the value and applying
* the function to it. To put it formally:
*
* return x >>= f is the same damn thing as f x
*
* http://learnyouahaskell.com/a-fistful-of-monads#monad-laws
*/
fun assertLaw1() {
val value = 2
val lhs = f(value)
val rhs = value.toNullable().flatMap(::f)
println("Satisfies law 1: ${lhs == rhs} ($lhs)")
}
/**
* Law 2: Right Identity
* The second law states that if we have a monadic value and we use >>= to feed it to return,
* the result is our original monadic value. Formally:
*
* m >>= return is no different than just m
*
* http://learnyouahaskell.com/a-fistful-of-monads#monad-laws
*/
fun assertLaw2() {
val monadValue = "Test".toNullable()
val rhs = monadValue.flatMap { it.toNullable() }
println("Satisfies law 2: ${monadValue == rhs} ($monadValue)")
}
/**
* Law 3: Associativity
*
* The final monad law says that when we have a chain of monadic function applications with >>=,
* it shouldn't matter how they're nested. Formally written:
* Doing (m >>= f) >>= g is just like doing m >>= (\x -> f x >>= g)
*
* http://learnyouahaskell.com/a-fistful-of-monads#monad-laws
*/
fun assertLaw3() {
val monadValue = 23.toNullable()
val lhs = monadValue.flatMap (::f).flatMap (::g)
val rhs = monadValue.flatMap (::fThenG)
println("Satisfies law 3: ${lhs == rhs} (${lhs})")
}
fun main(args : Array<String>) {
assertLaw1()
assertLaw2()
assertLaw3()
}
@seanf
Copy link

seanf commented Oct 2, 2018

There seems to be one problem with using Kotlin's nullable types as monads.

If the function f accepts nullable Ints like this:

fun f(x : Int?) : Int? = 
    if (x == null) -1
    else x * 2

and then I pass a null value like this:

fun assertLaw1() {
    val value: Int? = null //was 2
    val lhs = f(value)
    val rhs = value.toNullable().flatMap(::f)
    println("Satisfies law 1: ${lhs == rhs} ($lhs)")
}

the first law fails (LHS is -1, but RHS is null).

It's a bit contrived, but I don't see a way of preventing it at compile time, since flatMap can't specify that it only accepts functions which don't allow nullable parameters (AFAIK). I think it would be possible to detect this with a custom Detekt rule though (in simple cases).

https://giscus.co/ is handy for Gist notifications

@lucapiccinelli
Copy link

Actually there exist a way to constraint T to be only of non nullable types. It can be constrained with <T: Any>. In this way nullable are not allowed. Anyway this invalidates as well the thesis that nullables are monads without the need to wrap them. As adding the constraint is equivalent to wrap it. 😔

@tabasavr
Copy link

I don't quite agree with the counterexample by @seanf. The type of f in their example is m T -> m T which doesn't fit the first law. Instead it should be T -> m T (which is in the original gist) or m T -> m m T. And with the second case we run into a problem because we can't stack nullability - Int?? is equivalent to Int?.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment