Skip to content

Instantly share code, notes, and snippets.

@ramseyboy
Created August 26, 2016 02:50
Show Gist options
  • Save ramseyboy/91bcc85cb5f28e47c9b5464c1501a983 to your computer and use it in GitHub Desktop.
Save ramseyboy/91bcc85cb5f28e47c9b5464c1501a983 to your computer and use it in GitHub Desktop.
Optional/Maybe type in Kotlin
package me.ramseyboy.function
import java.util.*
/**
* A container object which may or may not contain a non-null value. If a value is defined, `isdefined()` will return `true` and `get()` will return the value.
*
* Additional methods that depend on the presence or absence of a contained value are provided,
* such as [orElse()][.orElse] (return a default value if value not defined)
* and [ifdefined()][.ifdefined] (execute a block of code if
* the value is defined).
*/
interface Maybe<T> {
/**
* If non-null, the value; if null, indicates no value is defined
*/
val value: T?
/**
* If a value is defined in this `Maybe`, returns the value, otherwise throws `NoSuchElementException`.
* @return the non-null value held by this `Maybe`
* *
* @throws NoSuchElementException if there is no value defined
* *
* @see Maybe.isDefined
*/
public fun get(): T? {
if (value == null) {
throw NoSuchElementException("No value defined")
}
return value
}
/**
* Return `true` if there is a value defined, otherwise `false`.
* @return `true` if there is a value defined, otherwise `false`
*/
public fun isDefined(): Boolean {
return value != null
}
/**
* If a value is defined, invoke the specified consumer with the value, otherwise do nothing.
* @param consumer block to be executed if a value is defined
* *
* @throws NullPointerException if value is defined and `consumer` is null
*/
public fun ifDefined(consumer: Consumer<in T?>) {
if (value != null)
consumer.accept(value)
}
/**
* If a value is defined, and the value matches the given predicate, return an `Maybe`
* describing the value, otherwise return an empty `Maybe`.
* @param predicate a predicate to apply to the value, if defined
* *
* @return an `Maybe` describing the value of this `Maybe` if a value is
* * defined and the value matches the given predicate, otherwise an empty `Maybe`
* *
* @throws NullPointerException if the predicate is null
*/
public fun filter(predicate: Predicate<in T>?): Maybe<T> {
if (predicate == null) {
throw NullPointerException()
}
if (!isDefined())
return this
else
return if (predicate.test(value!!)) this else empty<T>()
}
/**
* If a value is defined, apply the provided mapping function to it, and if the result is
* non-null, return an `Maybe` describing the result. Otherwise return an empty `Maybe`.
* @param The type of the result of the mapping function
* *
* @param mapper a mapping function to apply to the value, if defined
* *
* @return an `Maybe` describing the result of applying a mapping function to the value
* * of this `Maybe`, if a value is defined, otherwise an empty `Maybe`
* *
* @throws NullPointerException if the mapping function is null
* *
* @apiNote This method supports post-processing on Maybe values, without the need to
* * explicitly check for a return status. For example, the following code traverses a stream of
* * file names, selects one that has not yet been processed, and then opens that file, returning
* * an `Maybe&lt;FileInputStream&gt;`:
* *
* * `Maybe&lt;FileInputStream&gt; fis =
* names.stream().filter(name -&gt; !isProcessedYet(name))
* .findFirst()
* .map(name -&gt; new FileInputStream(name));
` *
* *
* * Here, `findFirst` returns an `Maybe&lt;String&gt;`, and then `map` returns an
* * `Maybe&lt;FileInputStream&gt;` for the desired file if one exists.
*/
public fun <U> map(mapper: Function<in T, out U>?): Maybe<U> {
if (mapper == null) {
throw NullPointerException()
}
if (!isDefined())
return empty()
else {
return Maybe.ofNullable(mapper.apply(value!!))
}
}
/**
* If a value is defined, apply the provided `Maybe`-bearing mapping function to it,
* return that result, otherwise return an empty `Maybe`. This method is similar to
* [.map], but the provided mapper is one whose result is already an `Maybe`, and if invoked, `flatMap` does not wrap it with an additional `Maybe`.
* @param The type parameter to the `Maybe` returned by
* *
* @param mapper a mapping function to apply to the value, if defined the mapping function
* *
* @return the result of applying an `Maybe`-bearing mapping function to the value of
* * this `Maybe`, if a value is defined, otherwise an empty `Maybe`
* *
* @throws NullPointerException if the mapping function is null or returns a null result
*/
public fun <U> flatMap(mapper: Function<in T, Maybe<U>>?): Maybe<U> {
if (mapper == null) {
throw NullPointerException()
}
if (!isDefined())
return empty()
else {
return mapper.apply(value!!)
}
}
/**
* Return the value if defined, otherwise return `other`.
* @param other the value to be returned if there is no value defined, may be null
* *
* @return the value, if defined, otherwise `other`
*/
public fun orElse(other: T): T {
return value ?: other
}
/**
* Return the value if defined, otherwise invoke `other` and return the result of that
* invocation.
* @param other a `Supplier` whose result is returned if no value is defined
* *
* @return the value if defined otherwise the result of `other.get()`
* *
* @throws NullPointerException if value is not defined and `other` is null
*/
public fun orElseGet(other: Supplier<out T>): T {
return value ?: other.get()
}
/**
* Return the contained value, if defined, otherwise throw an exception to be created by the
* provided supplier.
* @param Type of the exception to be thrown
* *
* @param exceptionSupplier The supplier which will return the exception to be thrown
* *
* @return the defined value
* *
* @throws X if there is no value defined
* *
* @throws NullPointerException if no value is defined and `exceptionSupplier` is null
* *
* @apiNote A method reference to the exception constructor with an empty argument list can be
* * used as the supplier. For example, `IllegalStateException::new`
*/
@Throws(Throwable::class)
public fun <X : Throwable> orElseThrow(exceptionSupplier: Supplier<out X>): T? {
if (value != null) {
return value
} else {
throw exceptionSupplier.get()
}
}
/**
* Indicates whether some other object is "equal to" this Maybe. The other object is
* considered equal if: * it is also an `Maybe` and; * both instances have no
* value defined or; * the defined values are "equal to" each other via `equals()`.
*
* @param obj an object to be tested for equality
* *
* @return {code true} if the other object is "equal to" this object otherwise `false`
*/
override fun equals(obj: Any?): Boolean {
if (obj == null) {
throw NullPointerException()
}
if (this === obj) {
return true
}
if (obj !is Maybe<*>) {
return false
}
return equals(value, obj.value)
}
/**
* Returns the hash code value of the defined value, if any, or 0 (zero) if no value is
* defined.
* @return hash code value of the defined value or 0 if no value is defined
*/
override fun hashCode(): Int {
return hashCode(value)
}
/**
* Returns a non-empty string redefinedation of this Maybe suitable for debugging. The exact
* definedation format is unspecified and may vary between implementations and versions.
* @return the string redefinedation of this instance
* *
* @implSpec If a value is defined the result must include its string redefinedation in the
* * result. Empty and defined Maybes must be unambiguously differentiable.
*/
override fun toString(): String {
return if (value != null){
"Maybe[$value]"
} else {
"Maybe.empty"
}
}
companion object {
/**
* Common instance for `empty()`.
*/
private val EMPTY = None<Any>()
/**
* Returns an empty `Maybe` instance. No value is defined for this Maybe.
* @param Type of the non-existent value
* *
* @return an empty `Maybe`
* *
* @apiNote Though it may be tempting to do so, avoid testing if an object is empty by comparing
* * with `==` against instances returned by `Option.empty()`. There is no guarantee
* * that it is a singleton. Instead, use [.isdefined].
*/
public fun <T> empty(): Maybe<T> {
val t = EMPTY as Maybe<T>
return t
}
/**
* Returns an `Maybe` with the specified defined non-null value.
* @param the class of the value
* *
* @param value the value to be defined, which must be non-null
* *
* @return an `Maybe` with the value defined
* *
* @throws NullPointerException if value is null
*/
public fun <T> of(value: T): Maybe<T> {
return Some(value)
}
/**
* Returns an `Maybe` describing the specified value, if non-null, otherwise returns an
* empty `Maybe`.
* @param the class of the value
* *
* @param value the possibly-null value to describe
* *
* @return an `Maybe` with a defined value if the specified value is non-null,
* * otherwise an empty `Maybe`
*/
public fun <T> ofNullable(value: T?): Maybe<T> {
return if (value == null) empty<T>() else of(value)
}
/**
* Null-safe equivalent of `a.equals(b)`.
*/
public fun equals(a: Any?, b: Any?): Boolean {
return if ((a == null)) (b == null) else a == b
}
/**
* Returns 0 for null or `o.hashCode()`.
*/
public fun hashCode(o: Any?): Int {
return if ((o == null)) 0 else o.hashCode()
}
}
}
final class Some<T> : Maybe<T> {
override val value: T
/**
* Constructs an instance with the value defined.
* @param value the non-null value to be defined
* *
* @throws NullPointerException if value is null
*/
constructor(value: T) {
if (value == null) {
throw NullPointerException()
}
this.value = value
}
}
final class None<T> : Maybe<T> {
override val value: T?
/**
* Constructs an empty instance.
* @implNote Generally only one empty instance, [Maybe.EMPTY], should exist per VM.
*/
constructor() {
this.value = null
}
}
fun main(args: Array<String>) {
val maybe: Maybe<String> = Some("defined")
val unwrapped = when(maybe) {
is Some -> maybe.get()
is None -> "default value"
else -> throw RuntimeException("neva gonna happen")
}
println(unwrapped)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment