Skip to content

Instantly share code, notes, and snippets.

@broadwaylamb
Last active February 2, 2024 18:13
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save broadwaylamb/264334008383669dd4c9ffcb16cd9363 to your computer and use it in GitHub Desktop.
Save broadwaylamb/264334008383669dd4c9ffcb16cd9363 to your computer and use it in GitHub Desktop.
Zero-cost Either type in Kotlin
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract
import kotlin.contracts.InvocationKind
/**
* Zero-cost union type.
*
* [Left] and [Right] must not be subtypes of one another, and must not be the same type.
*/
@JvmInline
value class GenericEither<CommonUpperBound, out Left : CommonUpperBound, out Right : CommonUpperBound> @PublishedApi internal constructor(
@Suppress("PropertyName")
@PublishedApi
internal val _value: Any /* (Left & Any) | (Right & Any) | LeftNull | RightNull */
) {
@PublishedApi
internal sealed class Null
@PublishedApi
internal object LeftNull : Null() {
override fun toString() = "Left(null)"
}
@PublishedApi
internal object RightNull : Null() {
override fun toString() = "Right(null)"
}
companion object {
inline fun <CommonUpperBound, reified Left : CommonUpperBound, reified Right : CommonUpperBound> genericLeft(value: Left) =
GenericEither<CommonUpperBound, Left, Right>(value ?: LeftNull)
inline fun <CommonUpperBound, reified Left : CommonUpperBound, reified Right : CommonUpperBound> genericRight(value: Right) =
GenericEither<CommonUpperBound, Left, Right>(value ?: RightNull)
inline fun <reified Left, reified Right> left(value: Left) = genericLeft<Any?, Left, Right>(value)
inline fun <reified Left, reified Right> right(value: Right) = genericRight<Any?, Left, Right>(value)
}
override fun toString(): String = _value.toString()
}
@OptIn(ExperimentalContracts::class)
inline fun <CommonUpperBound, reified Left, reified Right, Result> GenericEither<CommonUpperBound, Left, Right>.fold(
ifLeft: (Left) -> Result,
ifRight: (Right) -> Result
): Result where Left : CommonUpperBound, Right : CommonUpperBound {
contract {
callsInPlace(ifLeft, InvocationKind.AT_MOST_ONCE)
callsInPlace(ifRight, InvocationKind.AT_MOST_ONCE)
}
return when (_value) {
GenericEither.LeftNull -> ifLeft(null as Left)
GenericEither.RightNull -> ifRight(null as Right)
is Left -> ifLeft(_value)
is Right -> ifRight(_value)
else -> error("Invalid Either value")
}
}
inline val <CommonUpperBound, reified Left, Right> GenericEither<CommonUpperBound, Left, Right>.isLeft: Boolean
where Left : CommonUpperBound, Right : CommonUpperBound
get() = _value is GenericEither.LeftNull || _value is Left
inline val <CommonUpperBound, Left, reified Right> GenericEither<CommonUpperBound, Left, Right>.isRight: Boolean
where Left : CommonUpperBound, Right : CommonUpperBound
get() = _value is GenericEither.RightNull || _value is Right
inline val <CommonUpperBound, reified Left, Right> GenericEither<CommonUpperBound, Left, Right>.leftOrNull: Left?
where Left : CommonUpperBound & Any, Right : CommonUpperBound & Any
get() = _value as? Left
inline val <CommonUpperBound, Left, reified Right> GenericEither<CommonUpperBound, Left, Right>.rightOrNull: Right?
where Left : CommonUpperBound & Any, Right : CommonUpperBound & Any
get() = value as? Right
@OptIn(ExperimentalContracts::class)
inline fun <
CommonUpperBound,
reified Left,
reified Right,
reified NewLeft,
reified NewRight
> GenericEither<CommonUpperBound, Left, Right>.map(
transformLeft: (Left) -> NewLeft,
transformRight: (Right) -> NewRight
): GenericEither<CommonUpperBound, NewLeft, NewRight>
where Left : CommonUpperBound, Right : CommonUpperBound, NewLeft : CommonUpperBound, NewRight : CommonUpperBound {
contract {
callsInPlace(transformLeft, InvocationKind.AT_MOST_ONCE)
callsInPlace(transformRight, InvocationKind.AT_MOST_ONCE)
}
return fold(ifLeft = { GenericEither.genericLeft(transformLeft(it)) }, ifRight = { GenericEither.genericRight(transformRight(it)) })
}
@OptIn(ExperimentalContracts::class)
inline fun <CommonUpperBound, reified Left, reified Right, reified NewLeft> GenericEither<CommonUpperBound, Left, Right>.mapLeft(
transform: (Left) -> NewLeft
): GenericEither<CommonUpperBound, NewLeft, Right> where Left : CommonUpperBound, Right : CommonUpperBound, NewLeft : CommonUpperBound {
contract {
callsInPlace(transform, InvocationKind.AT_MOST_ONCE)
}
return fold(ifLeft = { GenericEither.genericLeft(transform(it)) }, ifRight = { GenericEither.genericRight(it) })
}
@OptIn(ExperimentalContracts::class)
inline fun <CommonUpperBound, reified Left, reified Right, reified NewRight> GenericEither<CommonUpperBound, Left, Right>.mapRight(
transform: (Right) -> NewRight
): GenericEither<CommonUpperBound, Left, NewRight> where Left : CommonUpperBound, Right : CommonUpperBound, NewRight : CommonUpperBound {
contract {
callsInPlace(transform, InvocationKind.AT_MOST_ONCE)
}
return fold(ifLeft = { GenericEither.genericLeft(it) }, ifRight = { GenericEither.genericRight(transform(it)) })
}
typealias Either<Left, Right> = GenericEither<Any?, Left, Right>
val <CommonUpperBound, Left, Right> GenericEither<CommonUpperBound, Left, Right>.value: CommonUpperBound
where Left : CommonUpperBound, Right : CommonUpperBound
@Suppress("UNCHECKED_CAST")
get() = when (_value) {
is GenericEither.Null -> null
else -> _value
} as CommonUpperBound
val <CommonUpperBound, Left, Right> GenericEither<CommonUpperBound, Left, Right>.value: CommonUpperBound & Any
where Left : CommonUpperBound & Any, Right : CommonUpperBound & Any
@Suppress("UNCHECKED_CAST")
get() = (_value as CommonUpperBound)!!
@zinoviy23
Copy link

How can I get a value of Either with nullable types? E.g. Either.left<String?, Int?>("a").leftOrNull doesn't work

@broadwaylamb
Copy link
Author

broadwaylamb commented Mar 6, 2023

@zinoviy23 you use fold for that. leftOrNull and rightOrNull are intentionally constrained to non-nullable Left and Right because of cases like Either<String?, Int?>.left(null).leftOrNull == null, which kind of implies that the value is Right, which is not true.

Instead of Either<String?, Int?> you can use use Either<String, Int>?. If that doesn't work for you either (no pun intended), then this class is not a good fit. Probably better use a sealed class-based implementation instead.

@zinoviy23
Copy link

And Either.left and Either.right can accept only 1 type parameter, the other one could be Nothing because of out.

@zinoviy23
Copy link

@broadwaylamb Thanks, got it!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment