`Raise` can be used to implement first-class gener...
# arrow
y
Raise
can be used to implement first-class generic lambdas (i.e rank-2 polymorphism)!
Copy code
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!
🧵 1
👀 1
Of course, any such generic function can throw an exception instead of returning a value of the expected type, but that's to be expected because any Kotlin function can throw exceptions at any time (it's not a bug, it's a feature ™️) This can also be very easily extended to having access to multiple existentially-quantified types. Also, because this makes generic functions truly first-class, we actually have Rank-N polymorphism! We can even store such functions in lists and use them willy-nilly. Of course, care must be taken to not violate the rules of
Raise
w.r.t. swallowing it, but that's a safe assumption we can make in a world with coroutines already!
An extra demonstration, showing that 1. one can easily provide contexts too and 2.
inline
really is supported:
Copy code
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)
}