Skip to content

Instantly share code, notes, and snippets.

@janojanahan
Last active March 23, 2023 02:46
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()
}
@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