Youssef Shoaib [MOD]
08/22/2021, 2:01 PMclass Failure<A>(val exception: A)
transparent value class Either<out A, out B> private constructor(val underlying: Any?) {
companion object {
fun <B> Right(value: B) = Either<Nothing, B>(value)
fun <A> Left(exception: A) = Either<A, Nothing>(Failure<A>(exception))
}
val isRight: Boolean get() {
contract {
returns(true) implies (underlying is B)
returns(false) implies (underlying is Failure<A>)
}
return underlying !is Failure<*>
}
val isLeft: Boolean get() {
contract {
returns(false) implies (underlying is B)
returns(true) implies (underlying is Failure<A>)
}
return underlying is Failure<*>
}
// map, flatMap, etc.
}
// User code
class NetworkFailure(val causeOfError: String)
class Information(val information: String)
fun networkCall(shouldBeSuccessful: Boolean): Either<NetworkFailure, Information> = if(shouldBeSuccessful) Either.Right(Information("super secret user data")) else Either.Left(NetworkFailure("No internet"))
fun main() {
val result = networkCall(true)
// Smart casting go brrrrr...
if(result.isRight) {
println(result.information)
} else {
println(result.exception.causeOfError)
}
}
Now, the Arrow folks already kind of tried to do with, but with computational blocks. The only issue with them is that they're kind of friction-y to use since you have to call .bind()
on the returned result to get the value. However, this feature so far doesn't actually solve their use case because it has a missing piece: verification. What if, with an operator verify
, a value class can prove that its underlying value is of a specific type. To continue with the arrow use-case, consider this:
transparent value class Either<out A, out B> private constructor(val underlying: Any?) {
// Same code from before
...
context(EitherEffect<A>) // Can't quite remember the Arrow implementation but it is something like that
operator fun verify() { // This would be auto-magically called everytime the value class is used as its underlying type
contract {
returns() implies (this@Either is B)
}
if(isLeft) throw ShortCircuitException(underlying)
}
}
// User code
class NetworkFailure(val causeOfError: String)
class Information(val information: String)
fun networkCall(shouldBeSuccessful: Boolean): Either<NetworkFailure, Information> = if(shouldBeSuccessful) Either.Right(Information("super secret user data")) else Either.Left(NetworkFailure("No internet"))
fun main() {
val result = networkCall(true)
// Arrow computational block
either {
println(result.information)
println(networkCall(false).information)
// This never actually executes because the top one throws
println(networkCall(true).information)
}
}
or let's go even crazier. What if there's an operator fun transparentlyCoerce
(long name to avoid clashes) that can be used for any class (not only value classes) and can have different implementations depending on context parameters. Then, that can be defined for Either like this:
operator fun transparentlyCoerce(): B = if(isLeft) throw ShortCircuitException(underlying) else underlying
and in fact, have you noticed that Failure
is kind of ugly? You need to call its exception
property every time, what if you don't have to...:
operator fun <A> Failure<A>.transparentlyCoerce(): A = exception