Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/l-briand/either

Either and Option implementation in Kotlin Multiplatform
https://github.com/l-briand/either

either kotlin kotlin-library kotlin-multiplatform kotlin-multiplatform-library kotlinx-serialization option optional

Last synced: 3 months ago
JSON representation

Either and Option implementation in Kotlin Multiplatform

Awesome Lists containing this project

README

        

# Kotlin Either & Option Multiplatform

`Either` and `Option` implementation in kotlin Multiplatform.

---

`Either` is, like the [Result](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-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.)

1. My variable is not here `{}` -> `None`
2. My variable is here but null `{"field":null}` -> `Some(null)`
3. My variable is here `{"field":"value"}` -> `Some("value")`

## Import from maven

### Multiplatform

```kotlin
repositories {
mavenCentral()
}
val commonMain by getting {
dependencies {
implementation("net.orandja.kt:either:2.0.0")
}
}
```

### Jvm

```kotlin
repositories {
mavenCentral()
}
dependencies {
implementation("net.orandja.kt:either:2.0.0")
}
```

## Usage

[dokka documentation here](https://l-briand.github.io/either/either/net.orandja.either/index.html)

### Either

This example is a bit convoluted, but it explains pretty well how to use `Either`.

```kotlin
enum class ErrCode { CONVERT, TOO_LOW, TOO_HIGH, /* ... */ }

// Get information from somewhere
var data: Either = getData()

// Transform left String value to Int
// Other methods can be used to transform a Either class.
val transformed: Either = data.letLeft { it.toIntOrNull }

// Transform left Int? to Float without exception.
fun doSomething(value: Either): Either {

// A 'require' block do not allow to be ended.
// You need to return or throw an exception inside.
val percent = value.requireLeft { it: Right ->
// 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](https://github.com/Kotlin/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.

### Option

```kotlin
val data: Option = getDataFromSomewhere()
val result: Option = 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](https://github.com/Kotlin/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`:

```kotlin
@Serializable
data class Data(val value: Option = 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](src/commonTest/kotlin/net/orandja/test/OptionJson.kt).

# Api

## Create

```kotlin
var option: Option
option = Some("value")
option = None

var either: Either
either = Left("value")
either = Right(1234)
```

## Side execution

```kotlin
val option: Option
val either: Either

val _: Option = option.alsoNone {}
val _: Option = option.alsoSome { str: String -> }
val _: Option = option.alsoBoth(onNone = { }, onSome = { str -> })

val _: Either = either.alsoLeft { str: String -> }
val _: Either = either.alsoRight { int: Int -> }
val _: Either = either.alsoBoth(onLeft = { str -> }, onRight = { int -> })
```

## Transform

### Either

[//]: # (@formatter:off)
```kotlin
val either: Either = 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 = either.invert() // invert types

val _: Either = either.letLeft { str: String -> Unit } // Transform left type
val _: Either = either.letRight { int: Int -> Unit } // Transform right type
val _: Either = 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 }
```
[//]: # (@formatter:on)

### Option

[//]: # (@formatter:off)
```kotlin
val option: Option = Some("value")

val _: String = option.value // Might throw an Exception
val _: String? = option.valueOrNull

val _: Option = 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" }
```
[//]: # (@formatter:on)

### Destructuring

```kotlin
val left = Left("value")
val right = Right(1234)
val value = Some(Any())

val (l: String) = left
val (r: Int) = right
val (v: Any) = value
```

### Either to Option

```kotlin
val either: Either = Left("value")

val a: Option = either.leftAsOption()
val b: Option = either.rightAsOption()

assert(a is Some)
assert(b is None)
```

### Option to Either

```kotlin
val option: Option = Some("value")

val _: Either = option.letNoneAsRight { 0 }
val _: Either = option.letNoneAsLeft { 0 }

val _: Either = option.letAsLeft(onSome = { 0L }, onNone = {})
val _: Either = 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)`

### Chaining

You can use the `try` function to chain calls and have a single error handle at the end.

```kotlin
object Error
fun String.toInt(): Either = this.toIntOrNull()?.let { Left(it) } ?: Right(ERROR)
fun Int.inBound(range: IntRange): Either = if (this in range) Left(this) else Right(ERROR)

val data: Either = 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`

```kotlin
fun String.toInt(): Option = this.toIntOrNull()?.let { Some(it) } ?: None
fun Int.inBound(range: IntRange): Option = if (this in range) Some(this) else none

val data: Option = Some("123")
val result: Int = data.trySome(String::toInt)
.tryLeft { it.inBound(0..<100) }
.requireSome { return }

assertEquals(123, result)
```

## Requiring values

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.

#### Either

```kotlin
val either: Either = Left("value")

val _: String = either.requireLeft { value: Right -> error("Will not fail") }
val _: Int = either.requireRight { value: Left -> error("Will fail") }

fun test() {
val _: String = either.requireLeft { (value: Int) -> return@test }
val _: Int = either.requireRight { (value: String) -> return@test }
}
```

#### Option

```kotlin
val option: Option = None

option.requireNone { opt: Option -> error("Will not fail") }
option.requireNone { (value: String) -> error("Will not fail") }

val _: String = option.requireSome { error("Will fail") }
```