https://kotlinlang.org logo
#arrow
Title
# arrow
t

tavish pegram

08/16/2022, 8:35 PM
hello! I’ve been out of the loop for a bit but have been using arrow a lot at work which has been great. One think that has come up recently is the possibility of reducing boilerplate / mapping with explicit sealed classes for failures between layers/function calls with a kotlin Union type. I remember there being a POC for a union type a while ago. Is that something that’s close to being a reality? 🤞 So instead of
Copy code
sealed interface FooFailure {
  object Failure1: FooFailure
  object Failure2: FooFailure
}
fun foo(): Either<FooFailure, Unit> = TODO()

sealed interface BarFailure {
  data class FooFailed(message: String): BarFailure
  object Failure1: BarFailure
  ...
}
fun bar(input: Input): Either<BarFailure, Unit> = either {
  input.validate().bind() // Could be BarFailure.Failure1
  foo().mapLeft { when (it) {
    FooFailure.Failure1 -> BarFailure.FooFailed("Failure 1")
    FooFailure.Failure2 -> BarFailure.FooFailed("Failure 2")
  }}.bind()
}
maybe
Copy code
sealed interface FooFailure {
  object Failure1: FooFailure
  object Failure2: FooFailure
}
fun foo(): Either<FooFailure, Unit> = TODO()

sealed interface BarFailure {
  object Failure1: BarFailure
  ...
}
fun bar(input: Input): Either<BarFailure|FooFailure, Unit> = either {
  input.validate().bind() // Could be BarFailure.Failure1 or some other bar specific failure
  foo().bind()
}
but still being able to exhaustively when/pattern match on it. Ideally, in a flat way, but even having it be nested is fine.
Copy code
bar(someInput()).fold(
  ifLeft = { when (it) {
    FooFailure.Failure1 -> TODO()
    FooFailure.Failure2 -> TODO()
    BarFailure.Failure1 -> TODO()
  }}, 
  ifRight = { TODO() }
)
Thanks!
Just noticed the previous message is also about error mapping, ill need to look through that and the discussion as well! Hopefully im not retreading old ground!
s

simon.vergauwen

08/17/2022, 6:42 AM
You can implement a generic version of this. Coproduct/Union type-ish. It's API is a bit clunky though. Would be great to have this in the language with proper inference support etc.
but have been using arrow a lot at work which has been great.
❤️ Any feedback you might have is very welcome Tavish! 🙏
t

tavish pegram

08/17/2022, 5:50 PM
I think the biggest complaint / suggestion is just mapping failures between layers at the moment. Though I am interested in how 1. the new context/multiple receiver stuff might change how we write code and how arrow would suggest doing it, especially w/r/t dependency injection and enforcing function purity (currently marking all impure functions as suspended). Would be open to any suggestions! 2. We use the
either
block a lot, because its great. But it does feel like it adds a bit of “clutter” and another bit of indentation to business logic. This is super nitpicky, but it might be cool to be able to use it as an annotation in some cases (similar to how I like to make logging / observability be an annotation so a function isn’t cluttered with log / dd metric calls that distract from the actual business logic). I assume it wouldn’t be as flexible as the function version though, but we could probably still get a lot of use out of it.
Copy code
@Observe("metric and logging name goes here") // Log input / output / time of execution and generate some basic data dog metrics
@Either // Hypothetically, this is the same as just adding `either {}` around the contents of this function. I assume the ordering of these annotation would matter. Do we lose typesafety when we start using annotations like this?
suspend fun foo(input: Input): Either<FooFailure, SomeResult> {
  // Only Bizness in here
  val x = bar(input.a).bind()
  return baz(x).bind()
}
The above fake code also assumes that bar and baz return FooFailure so we don’t have to do any extra mapping, which is the ask in the main post.
s

simon.vergauwen

08/17/2022, 6:01 PM
Hey @tavish pegram, With context receivers you can add
context(EffectScope<String>)
and you can now monadically raise
String
as an error inside of it. I.e.
either<String, Int> { /* this is of type EffectScope<String> */ }
Further more, you can add multiple of them on top of you function. This solves the layer problem
Copy code
context(EffectScope<String>)
suspend fun example(): Int = 1

suspend fun example2(): Either<String, Int> = either { example() }

object Error

context(EffectScope<String>, EffectScope<Error>)
suspend fun example3(): Int {
  example()
  shift(Error)
}

context(EffectScope<Error>)
suspend fun example4(): Int =
  either { example() }.getOrHandle { -1 }
I think this answers both 1, and 2 🙂 You can find some more examples, and details here. https://nomisrev.github.io/context-receivers/ And I also talked about it in a webinar,

https://youtu.be/g79A6HmbW5M?t=2564

You can use this in Kotlin JVM already. It's still experimental but it's working pretty well in a bunch of real-world-sized example microservices. All tests, and integration tests are passing so you could try it if you're targeting Kotlin JVM only.
8 Views