Youssef Shoaib [MOD]
05/25/2025, 8:27 PMRaise
can be used to implement first-class generic lambdas (i.e rank-2 polymorphism)!
import arrow.core.raise.Raise
import arrow.core.raise.merge
private fun main() {
println("Identity:")
useIdentity {
raise.raise(value)
}
println("ListMaker: ")
useListMaker {
// "opening" an existential type
// ideally, this would work like the example above through compiler magic
fun <A> ListMaker<A>.block(): Nothing = raise.raise(listOf(value))
block()
}
useListMaker {
fun <A> ListMaker<A>.dishonest(): Nothing = when (value) {
is Unit -> raise.raise(listOf())
is Int -> raise.raise(listOf(value))
else -> raise.raise(listOf(value, value))
}
dishonest()
}
println("Choice: ")
useChoice {
raise.raise(second)
}
useChoice {
fun <A> Choice<A>.dishonest(): Nothing = when (first) {
is Unit -> raise.raise(second)
is Int -> raise.raise(first)
else -> raise.raise(second)
}
dishonest()
}
}
private class Identity<A>(val value: A, val raise: Raise<A>)
private fun useIdentity(block: Identity<*>.() -> Nothing) {
val value = merge {
Identity(Unit, this).block()
}
println(value)
val value2 = merge {
Identity(42, this).block()
}
println(value2)
}
private class ListMaker<A>(val value: A, val raise: Raise<List<A>>)
private fun useListMaker(block: ListMaker<*>.() -> Nothing) {
// we can convert this to a List<Unit>, which is equivalent to an Int
val listValue = merge {
ListMaker(Unit, this).block()
}
println(listValue)
val listValue2 = merge {
ListMaker(42, this).block()
}
println(listValue2)
// We can always keep the polymorphic function honest by wrapping in an unreachable type
// Using data class for clarity, but ideally you want a type that returns the same
// toString and same hashCode for all instances.
data class Wrapper<A>(val value: A)
val wrappedValue = merge {
ListMaker(Wrapper(Unit), this).block()
}
println(wrappedValue)
// hence it can't know the type of the value inside without reflection
// (but with reflection, all bets are off anyway)
val wrappedValue2 = merge {
ListMaker(Wrapper(42), this).block()
}
println(wrappedValue2)
}
private class Choice<A>(val first: A, val second: A, val raise: Raise<A>)
private fun useChoice(block: Choice<*>.() -> Nothing) {
// we can convert this to a Boolean
val value = merge {
Choice(false, true, this).block()
}
println(value)
val value2 = merge {
Choice(41, 42, this).block()
}
println(value2)
// Keeping it honest, just like in ListMaker
data class Wrapper<A>(val value: A)
val wrappedValue = merge {
Choice(Wrapper(false), Wrapper(true), this).block()
}
println(wrappedValue)
// so results are identical here
val wrappedValue2 = merge {
Choice(Wrapper(41), Wrapper(42), this).block()
}
println(wrappedValue2)
}
It turns out we can very easily implement generic lambdas (i.e. lambdas like <A> (A) -> List<A>
) without direct language support! Of course, we could already use a (not-fun
) interface ListMaker { fun <A> make(a: A): List<A> }
, but that'd require making an anonymous object at the call site, which has awkward syntax, and cannot be made inline
. Instead, we can define it directly by giving the lambda only one way to exist, which is to raise
something of an existentially-quantified type, while also potentially giving it several members of that unknown type.
Sadly, the compiler isn't super smart at the moment in dealing with existential types. It can handle some of them in very, very simple cases (as shown in the useIdentity
example), but it gives up whenever there's any function call in between (like listOf
, for instance). Hopefully the compiler can get smarter in the future!Youssef Shoaib [MOD]
05/25/2025, 8:36 PMRaise
w.r.t. swallowing it, but that's a safe assumption we can make in a world with coroutines already!Youssef Shoaib [MOD]
05/25/2025, 8:53 PMinline
really is supported:
private fun main() {
useWithContexts {
withContexts {
barValue
}
}
run {
useWithContexts {
withContexts {
return@run
}
}
}
// Nothing printed!
}
private class Foo<A>(val value: A)
context(f: Foo<A>)
private val <A> fooValue get(): A = f.value
private class Bar<A>(val value: A)
context(b: Bar<A>)
private val <A> barValue get(): A = b.value
private class ProvidesSomeContexts<A>(val foo: Foo<A>, val bar: Bar<A>, val raise: Raise<A>) {
inline fun withContexts(block: context(Foo<A>, Bar<A>) () -> A): Nothing = raise.raise(block(foo, bar))
}
private inline fun useWithContexts(block: ProvidesSomeContexts<*>.() -> Nothing) {
val value = merge {
ProvidesSomeContexts(Foo(41), Bar(42), this).block()
}
println(value)
}