:thread: Hi all. Looking for some advice/review on...
# arrow
y
🧵 Hi all. Looking for some advice/review on this small
effect
inspired builder/DSL that I created to wrap calls to gRPC services. Details inside the thread.
For context, the company that I work at is standardizing on defining/implementing internal APIs using protobufs/gRPC services. Kotlin artifacts are generated from the protobuf API definitions, which includes coroutine stub client classes for all services. Each service call in those client classes are suspendable functions that throw StatusException that adheres to Google Cloud's Errors design. The idea was to create a small DSL to easily wrap each API call such that they return an
Effect
type instead. Given the standardized error format, we can abstract away the error handling logic to return a common error ADT, but we also want to flexibility to provide a different error handling function when necessary. I created small Gist that shows what it could look like, with one file for the definition and one test file with a few tests that show how it could be used.
In short, given sample services:
Copy code
class GrpcService1 {
    suspend fun fun1(): String = "value_1"
    suspend fun fun1ThatFails(): String = throw RuntimeException("kaboom_1")
}

class GrpcService2 {
    suspend fun fun2(): String = "value_2"
    suspend fun fun2ThatFails(): String = throw RuntimeException("kaboom_2")
}
usage could look like this
Copy code
val result: Effect<String, String> = grpc(DefaultGrpcEffectContext) {
    val r1 = call {
        service1.fun1()
    }.bind()
    val r2 = call {
        service2.fun2()
    }.bind()
    "$r1 $r2"
}
or same call with a custom error handler:
Copy code
val customGrpcEffectContext = object : GrpcEffectContext<Int> {
    override fun handleThrowable(t: Throwable): Int = 0
}

val result: Effect<Int, String> = grpc(customGrpcEffectContext) {
    val r1 = call {
        service1.fun1()
    }.bind()
    val r2 = call {
        service2.fun2()
    }.bind()
    "$r1 $r2"
}
s
Hey @Yannick Lazzari, The implementation and usage of
Effect
and
EffectScope
is certainly correct! So everything looks good there. I would say that this is definitely a good use of
Effect
to make a custom DSL. You could make
fun <A> call(f: suspend EffectScope<R>.() -> A): Effect<R, A>
return
A
immediately by calling
bind
before returning. That however prevents you from doing:
Copy code
grpc(DefaultGrpcEffectContext) {
  val effect = call { ... }
  effect.bind() + effect.bind()
}
but would make most of the tests a lot nicer since the pattern now seems to be:
Copy code
grpc(DefaultGrpcEffectContext) {
  val effectA = call { ... }
  val effectB = call { ... }
  effectA.bind() + effectB.bind()
}
While it can become
Copy code
grpc(DefaultGrpcEffectContext) {
  val a = call { ... }
  val b = call { ... }
  a + b
}
Thanks for sharing 🙌
y
Wow. That was fast! Thanks for your prompt response @simon.vergauwen! I think I'll apply your suggestion. Nice to know I'm on the right track. I called this
grpc
but there's nothing overly gRPC related to it. It's mostly around bringing in scope some error handling logic that can be applied to a series of effectful API call, and reducing boilerplate by saving each call from being explicit about how to map any thrown Throwables into some error ADT. The gRPC specific part is in the implementation of the default context, but it could be used for other purposes. I may try to find a better name for this. Thanks for the feedback!
s
Yes, it could be generalised 🤔
Copy code
public fun <R, A> effect(
  transform: suspend (Throwable) -> R,
  f: suspend EffectScope<R>.() -> A,
): Effect<R, A> = effect { 
  f(ErrorMappingEffectScope(transform, this))
}

public class ErrorMappingEffectScope<R>(
  private val transform: suspend (Throwable) -> R,
  private val scope: EffectScope<R>,
) : EffectScope<R> {
  override suspend fun <B> Effect<R, B>.bind(): B =
    fold({ shift(transform(it)) }, { shift(it) }, ::identity)
  
  override suspend fun <B> shift(r: R): B = scope.shift(r)
}
This might be an interesting addition to Arrow.
y
Out of curiosity @simon.vergauwen, I tried the code snippet that you shared (see snippet with extra tests that I used). Not sure about the behavior of when simply overriding the
bind
. When establishing the error transformation in the scope of a particular effect block, I would expect it to be applied to any exceptions that may be thrown, not just to those arising from when calling
bind
on enclosing effects. Maybe I'm missing something or it's just that the snippet was quickly put together, but just wanted to share this. Thanks!
s
I would expect it to be applied to any exceptions that may be thrown, not just to those arising from when calling
bind
on enclosing effects.
Ah yes, that is indeed not included in my snippet but it can be easily added.
Copy code
public fun <R, A> effect(
    transform: suspend (Throwable) -> R,
    f: suspend EffectScope<R>.() -> A,
): Effect<R, A> = effect {
    try {
      f(ErrorMappingEffectScope(transform, this))
    } catch(t : Throwable) {
      shift(transform(t.nonFatalOrThrow()))
    }
}
y
I'll try that . Thanks!