Last active
November 8, 2024 05:00
-
-
Save janojanahan/6ab593ed2ff92d8480b61b70ba75ec7f to your computer and use it in GitHub Desktop.
Kotlin Nullable can behave like a monad
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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() | |
} |
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. 😔
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
There seems to be one problem with using Kotlin's nullable types as monads.
If the function
f
accepts nullableInt
s like this:and then I pass a
null
value like this: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