Skip to content

Instantly share code, notes, and snippets.

@marcellogalhardo
Created November 23, 2020 12:23
Show Gist options
  • Save marcellogalhardo/f26542c584fadd82a27f3048e3e201a9 to your computer and use it in GitHub Desktop.
Save marcellogalhardo/f26542c584fadd82a27f3048e3e201a9 to your computer and use it in GitHub Desktop.
An option class for Kotlin.
import java.io.Serializable
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
/**
* A type that represents either a wrapped value or null, the absence of a value.
*/
public sealed class Option<out T> : Serializable {
@PublishedApi
internal object None : Option<Nothing>()
@PublishedApi
internal data class Some<out T>(val value: T) : Option<T>()
/**
* Returns `true` if this instance represents a null.
* In this case [isSome] returns `false`.
*/
public val isNone: Boolean
get() = this is None
/**
* Returns `true` if this instance represents a value.
* In this case [isNone] returns `false`.
*/
public val isSome: Boolean
get() = this is Some
/**
* Returns the encapsulated value if this instance represents [some][Option.isSome] or `null`
* if it is [none][Option.isNone].
*
* This function is a shorthand for `getOrElse { null }` (see [getOrElse]) or
* `fold(onSome = { it }, onNone = { null })` (see [fold]).
*/
public fun getOrNull(): T? = when (this) {
is None -> null
is Some -> value
}
/**
* Returns a string `Some(v)` if this instance represents [Some][Option.isSome]
* where `v` is a string representation of the value or a string `None(null)` if
* it is [None][isNone] where `x` is always null.
*/
public override fun toString(): String = when (this) {
is None -> "None(null)"
is Some -> "Some($value)"
}
/**
* Companion object for [Option] class that contains its constructor functions
* [Some] and [None].
*/
public companion object {
/**
* Returns an instance that encapsulates the given [value] as [Some][Option.isSome].
*/
public fun <T> some(value: T): Option<T> = Some(value)
/**
* Returns an instance that encapsulates the given null as [None][Option.isNone].
*/
public fun <T> none(): Option<T> = None
}
}
/**
* Calls the specified function [block] and returns its encapsulated Option if invocation was not nullable,
* otherwise returns None.
*/
@Suppress("FunctionName", "TooGenericExceptionCaught")
public inline fun <T> Option(block: () -> T?): Option<T> {
return when (val value = block()) {
value != null -> Option.some<T>(value)
else -> Option.none()
}
}
/**
* Returns the encapsulated value if this instance represents [Some][Option.isSome] or throws an
* error if it is [None][Option.isNone].
*
* This function is a shorthand for `getOrElse { throw it }` (see [getOrElse]).
*/
public fun <T> Option<T>.getOrThrow(): T {
return when (this) {
is Option.None -> error("Required Option was None.")
is Option.Some -> value
}
}
/**
* Returns the encapsulated value if this instance represents [Some][Option.isSome] or the
* Option of [onNone] function if it is [None][Option.isNone].
*
* Note, that this function rethrows any [Throwable] exception thrown by [onNone] function.
*
* This function is a shorthand for `fold(onSome = { it }, onNone = onNone)` (see [fold]).
*/
@OptIn(ExperimentalContracts::class)
public inline fun <R, T : R> Option<T>.getOrElse(onNone: () -> R): R {
contract {
callsInPlace(onNone, InvocationKind.AT_MOST_ONCE)
}
return when (this) {
is Option.None -> onNone()
is Option.Some -> value
}
}
/**
* Returns the encapsulated value if this instance represents [Some][Option.isSome] or the
* [defaultValue] if it is [None][Option.isNone].
*
* This function is a shorthand for `getOrElse { defaultValue }` (see [getOrElse]).
*/
public fun <R, T : R> Option<T>.getOrDefault(defaultValue: R): R {
return when (this) {
is Option.None -> defaultValue
is Option.Some -> value
}
}
/**
* Returns the Option of [onSome] for the encapsulated value if this instance represents [Some][Option.isSome]
* or the Option of [onNone] function if it is [None][Option.isNone].
*
* Note, that this function rethrows any [Throwable] exception thrown by [onSome] or by [onNone] function.
*/
@OptIn(ExperimentalContracts::class)
public inline fun <R, T> Option<T>.fold(
onNone: () -> R,
onSome: (value: T) -> R
): R {
contract {
callsInPlace(onNone, InvocationKind.AT_MOST_ONCE)
callsInPlace(onSome, InvocationKind.AT_MOST_ONCE)
}
return when (this) {
is Option.None -> onNone()
is Option.Some -> onSome(value)
}
}
/**
* Returns the encapsulated Option of the given [transform] function applied to the encapsulated value
* if this instance represents [Some][Option.isSome] or none if it is [None][Option.isNone].
*
* Note, that this function rethrows any [Throwable] exception thrown by [transform] function.
* See [mapCatching] for an alternative that encapsulates exceptions.
*/
@OptIn(ExperimentalContracts::class)
public inline fun <R, T> Option<T>.map(transform: (value: T) -> R): Option<R> {
contract {
callsInPlace(transform, InvocationKind.AT_MOST_ONCE)
}
return when (this) {
is Option.None -> Option.none()
is Option.Some -> Option.some(transform(value))
}
}
/**
* Returns the encapsulated Option of the given [transform] function applied to the encapsulated value
* if this instance represents [Some][Option.isSome] or none if it is [None][Option.isNone].
*
* This function catches any [Throwable] exception thrown by [transform] function and encapsulates it as a None.
* See [map] for an alternative that rethrows exceptions from `transform` function.
*/
public inline fun <R, T> Option<T>.mapCatching(transform: (value: T) -> R): Option<R> {
return when (this) {
is Option.None -> Option.none()
is Option.Some -> Option { transform(value) }
}
}
/**
* Returns a [Option] of the given [transform] function applied to the encapsulated value
* if this instance represents [Some][Option.isSome] or none if it is [None][Option.isNone].
*
* Note, that this function rethrows any [Throwable] exception thrown by [transform] function.
* See [flatMapCatching] for an alternative that encapsulates exceptions.
*/
@OptIn(ExperimentalContracts::class)
public inline fun <R, T> Option<T>.flatMap(transform: (value: T) -> Option<R>): Option<R> {
contract {
callsInPlace(transform, InvocationKind.AT_MOST_ONCE)
}
return when (this) {
is Option.None -> Option.none()
is Option.Some -> transform(value)
}
}
/**
* Returns a [Option] of the given [transform] function applied to the encapsulated value
* if this instance represents [Some][Option.isSome] or none if it is [None][Option.isNone].
*
* This function catches any [Throwable] exception thrown by [transform] function and encapsulates it as a None.
* See [map] for an alternative that rethrows exceptions from `transform` function.
*/
public inline fun <R, T> Option<T>.flatMapCatching(transform: (value: T) -> Option<R>): Option<R> {
return when (this) {
is Option.None -> Option.none()
is Option.Some -> Option { transform(value).getOrThrow() }
}
}
/**
* Returns the encapsulated Option of the given [transform] function applied to the encapsulated [Throwable] exception
* if this instance represents [None][Option.isNone] or none if it is [None][Option.isNone].
*
* Note, that this function rethrows any [Throwable] exception thrown by [transform] function.
* See [recoverCatching] for an alternative that encapsulates exceptions.
*/
@OptIn(ExperimentalContracts::class)
public inline fun <R, T : R> Option<T>.recover(transform: () -> R): Option<R> {
contract {
callsInPlace(transform, InvocationKind.AT_MOST_ONCE)
}
return when (this) {
is Option.None -> Option.Some(transform())
is Option.Some -> this
}
}
/**
* Returns the encapsulated Option of the given [transform] function applied to the encapsulated [Throwable] exception
* if this instance represents [None][Option.isNone] or none if it is [None][Option.isNone].
*
* This function catches any [Throwable] exception thrown by [transform] function and encapsulates it as a None.
* See [recover] for an alternative that rethrows exceptions.
*/
public inline fun <R, T : R> Option<T>.recoverCatching(transform: () -> R): Option<R> {
return when (this) {
is Option.None -> Option { transform() }
is Option.Some -> this
}
}
/**
* Performs the given [action] if this instance represents [None][Option.isNone].
* Returns the original `Option` unchanged.
*/
@OptIn(ExperimentalContracts::class)
public inline fun <T> Option<T>.onNone(action: () -> Unit): Option<T> {
contract {
callsInPlace(action, InvocationKind.AT_MOST_ONCE)
}
if (this is Option.None) action()
return this
}
/**
* Performs the given [action] on the encapsulated value if this instance represents [Some][Option.isSome].
* Returns the original `Option` unchanged.
*/
@OptIn(ExperimentalContracts::class)
public inline fun <T> Option<T>.onSome(action: (value: T) -> Unit): Option<T> {
contract {
callsInPlace(action, InvocationKind.AT_MOST_ONCE)
}
if (this is Option.Some) action(value)
return this
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment