Yannick Lazzari
08/08/2022, 3:04 PMeffect
inspired builder/DSL that I created to wrap calls to gRPC services. Details inside the thread.Yannick Lazzari
08/08/2022, 3:05 PMEffect
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.Yannick Lazzari
08/08/2022, 3:10 PMclass 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
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:
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"
}
simon.vergauwen
08/08/2022, 3:16 PMEffect
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:
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:
grpc(DefaultGrpcEffectContext) {
val effectA = call { ... }
val effectB = call { ... }
effectA.bind() + effectB.bind()
}
While it can become
grpc(DefaultGrpcEffectContext) {
val a = call { ... }
val b = call { ... }
a + b
}
simon.vergauwen
08/08/2022, 3:17 PMYannick Lazzari
08/08/2022, 3:23 PMgrpc
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!simon.vergauwen
08/08/2022, 3:52 PMpublic 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)
}
simon.vergauwen
08/08/2022, 3:52 PMYannick Lazzari
08/10/2022, 12:42 PMbind
. 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!simon.vergauwen
08/10/2022, 1:17 PMI would expect it to be applied to any exceptions that may be thrown, not just to those arising from when callingAh yes, that is indeed not included in my snippet but it can be easily added.on enclosing effects.bind
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()))
}
}
Yannick Lazzari
08/10/2022, 1:21 PM