Either
and Option
implementation in kotlin Multiplatform.
Either
is, like the Result class in kotlin, a
discriminated union of two types.
However, it lets you use any Type as the second Type.
Option
is useful when you need to have a third state to your variable, like in a json object where a field 'presence'
is important. (In most cases, using a nullable variable is fine.)
- My variable is not here
{}
->None
- My variable is here but null
{"field":null}
->Some(null)
- My variable is here
{"field":"value"}
->Some("value")
repositories {
mavenCentral()
}
val commonMain by getting {
dependencies {
implementation("net.orandja.kt:either:2.0.0")
}
}
repositories {
mavenCentral()
}
dependencies {
implementation("net.orandja.kt:either:2.0.0")
}
This example is a bit convoluted, but it explains pretty well how to use Either
.
enum class ErrCode { CONVERT, TOO_LOW, TOO_HIGH, /* ... */ }
// Get information from somewhere
var data: Either<String, ErrCode> = getData()
// Transform left String value to Int
// Other methods can be used to transform a Either class.
val transformed: Either<Int?, ErrCode> = data.letLeft { it.toIntOrNull }
// Transform left Int? to Float without exception.
fun doSomething(value: Either<Int?, ErrCode>): Either<Float, ErrCode> {
// A 'require' block do not allow to be ended.
// You need to return or throw an exception inside.
val percent = value.requireLeft { it: Right<ErrCode> ->
// Here we return the upper function early,
// with the already known error
return it
}
percent ?: return Right(ErrCode.CONVERT)
if (percent <= 0) return Right(ErrCode.TOO_LOW)
if (percent >= 100) return Right(ErrCode.TOO_HIGH)
return Left(percent.toFloat() / 100f)
}
// Show result
when (val result = doSomething(transformed)) {
is Left -> println("Current progress ${result.left}")
is Right -> println("Invalid value. Reason ${result.right}")
}
The Either
class is serializable with kotlinx-serialization.
It adds a depth level to your serialization with left
or right
value.
Left("value") <=> { "left": "value" }
Right(12345) <=> { "right": 12345 }
You will get a deserialization exception if both left
and right
are together.
val data: Option<String?> = getDataFromSomewhere()
val result: Option<Int?> = data.letSome { it?.toIntOrNull() }
when (result) {
is Some -> println("Success, result: ${result.value ?: "'no value'"}.")
None -> println("Nothing was found.")
}
The Option
class is also serializable with kotlinx-serialization.
Make sure to not encode defaults in your encoder. Like in Json with { encodeDefaults = false }
. Then, when defining a
data class, initialize fields to None
:
@Serializable
data class Data(val value: Option<String?> = None)
Doing it this way allows the deserializer to fall back to None
when the field is not present inside a json object.
If the field is present and null, it deserializes to Some(null)
.
Given the example above:
JSON <=> Kotlin
{ } <=> Data(None)
{ "value": null } <=> Data(Some(null))
{ "value": "value" } <=> Data(Some("value"))
You can see the test here.
var option: Option<String>
option = Some("value")
option = None
var either: Either<String, Int>
either = Left("value")
either = Right(1234)
val option: Option<String>
val either: Either<String, Int>
val _: Option<String> = option.alsoNone {}
val _: Option<String> = option.alsoSome { str: String -> }
val _: Option<String> = option.alsoBoth(onNone = { }, onSome = { str -> })
val _: Either<String, Int> = either.alsoLeft { str: String -> }
val _: Either<String, Int> = either.alsoRight { int: Int -> }
val _: Either<String, Int> = either.alsoBoth(onLeft = { str -> }, onRight = { int -> })
val either: Either<String, Int> = Left("value")
val _: String = either.left // Might throw an Exception
val _: Int = either.right // Might throw an Exception
val _: String? = either.leftOrNull
val _: Int? = either.rightOrNull
val _: Either<Int, String> = either.invert() // invert types
val _: Either<Unit, Int> = either.letLeft { str: String -> Unit } // Transform left type
val _: Either<String, Unit> = either.letRight { int: Int -> Unit } // Transform right type
val _: Either<Unit, Unit> = either.letBoth(onLeft = {}, onRight = {}) // Transform both types
val a: String = either.foldRight { int: Int -> "new" } // Transform right value to left type
val b: Int = either.foldLeft { str: String -> 0 } // Transform left value to right type
val _: Unit = either.foldBoth(onLeft = {}, onRight = {}) // Transform both left and right value to same type
assertTrue { a == "value" }
assertTrue { b == 0 }
val option: Option<String> = Some("value")
val _: String = option.value // Might throw an Exception
val _: String? = option.valueOrNull
val _: Option<Unit> = option.letSome { str: String -> Unit } // Transform value type
val a: String = option.foldNone { "new" } // Create value type on None
val _: Unit = option.foldBoth(onNone = {}, onSome = {}) // Transform bot
assertTrue { a == "value" }
val left = Left("value")
val right = Right(1234)
val value = Some(Any())
val (l: String) = left
val (r: Int) = right
val (v: Any) = value
val either: Either<String, Int> = Left("value")
val a: Option<String> = either.leftAsOption()
val b: Option<Int> = either.rightAsOption()
assert(a is Some<String>)
assert(b is None)
val option: Option<String> = Some("value")
val _: Either<String, Int> = option.letNoneAsRight { 0 }
val _: Either<Int, String> = option.letNoneAsLeft { 0 }
val _: Either<Long, Unit> = option.letAsLeft(onSome = { 0L }, onNone = {})
val _: Either<Unit, Long> = option.letAsRight(onSome = { 0L }, onNone = {})
If you know the type of the Option, just create a left or right value with it. val either = Left(option.value)
You can use the try
function to chain calls and have a single error handle at the end.
object Error
fun String.toInt(): Either<Int, ERROR> = this.toIntOrNull()?.let { Left(it) } ?: Right(ERROR)
fun Int.inBound(range: IntRange): Either<Int, ERROR> = if (this in range) Left(this) else Right(ERROR)
val data: Either<String, ERROR> = Left("123")
val result: Int = data.tryLeft(String::toInt)
.tryLeft { it.inBound(0..<100) }
.requireLeft { return }
assertEquals(123, result)
You can do the same thing with an Option
fun String.toInt(): Option<Int> = this.toIntOrNull()?.let { Some(it) } ?: None
fun Int.inBound(range: IntRange): Option<Int> = if (this in range) Some(this) else none
val data: Option<String> = Some("123")
val result: Int = data.trySome(String::toInt)
.tryLeft { it.inBound(0..<100) }
.requireSome { return }
assertEquals(123, result)
There are no operators like either.withLeft { }
or option.withValue { }
as it implies some sort of error
handling if the type is wrong. Instead, the lib provides the inverted thinking, handles the error case and
continues.
In a require
block you are requiring to return or throw an exception.
You cannot let the code go at the end of the block.
You can use destructuring syntax to handle Either
/ Option
values directly.
val either: Either<String, Int> = Left("value")
val _: String = either.requireLeft { value: Right<Int> -> error("Will not fail") }
val _: Int = either.requireRight { value: Left<String> -> error("Will fail") }
fun test() {
val _: String = either.requireLeft { (value: Int) -> return@test }
val _: Int = either.requireRight { (value: String) -> return@test }
}
val option: Option<String> = None
option.requireNone { opt: Option<String> -> error("Will not fail") }
option.requireNone { (value: String) -> error("Will not fail") }
val _: String = option.requireSome { error("Will fail") }