6

According to the docs I should implement an effect with an object.

fun interface JustEffect<A> : Effect<Just<A>> {
  suspend fun <B> Just<B>.bind(): B = value
}

object effect {
  operator fun <A> invoke(func: suspend JustEffect<*>.() -> A): Just<A> =
    Effect.restricted(eff = { JustEffect { it } }, f = func, just = { Just(it) })
}

This is the general guide from the tutorial. I'm curious if anyone knows why they use an object? My specific use case below for further context:

We already have a wrapper object, called PoseidonRes which can be success or error. We use this pervasively, and don't want to switch to Either types everywhere. That being said, here is my custom Effect, and how I've implemented it.

fun interface PoseidonResEffect<A> : Effect<PoseidonRes<A>> {
    suspend fun <T> PoseidonRes<T>.bind(): T = when (this) {
        is SuccessResponse -> this.response
        is ErrorResponse -> control().shift(this)
    }
}

fun <A> posRes(func: suspend PoseidonResEffect<A>.() -> PoseidonRes<A>): PoseidonRes<A> =
    Effect.restricted(
        eff = { PoseidonResEffect { it } },
        f = func,
        just = { it }
    )

The primary difference, is that I'm implemented the function interface as a function, rather than an invoked object. I really want to know why it's recommended one way, when this seems perfectly fine. I've dug around the docs, but can't find an answer. Please RTFM me if it's actually in the docs.

At the callsite it looks like

        posRes { 
            val myThing1 = thingThatsPoseidonResYielding().bind()
            val myThing2 = thingThatsPosiedonResYielding2().bind
            SuccessResponse(order.from(myThing1, myThing2))
        }

Either implementation works identically it seems. Tests pass either way. What's going on here?

1 Answers1

5

There is a newer API available, that we've recently released that simplifies building such abstractions in a more convenient way. While also offering bigger performance benefits!

Here is an example for Option.

Translated to your domain:

First we create a function that maps Effect<ErrorResponse, A> to the custom type. This is useful for when you'd write any other program/Effect that might result in ErrorResponse and you want to turn into your custom type.

public suspend fun <A> Effect<ErrorResponse, A>.toPoseidonRes(): PoseidonRes<A> =
  fold({ it }) { SuccessResponse(it) }

Next we create some extra DSL sugar, so you can conveniently call bind on your own type.

@JvmInline
public value class PoseidonResEffectScope(private val cont: EffectScope< ErrorResponse>) : EffectScope<ErrorResponse> {
  override suspend fun <B> shift(r: None): B =
    cont.shift(r)

  suspend fun <T> PoseidonRes<T>.bind(): T = when (this) {
    is SuccessResponse -> this.response
    is ErrorResponse -> shift(this)
  }

  public suspend fun ensure(value: Boolean): Unit =
    ensure(value) { ErrorResponse }
}

@OptIn(ExperimentalContracts::class)
public suspend fun <B> PoseidonResEffectScope.ensureNotNull(value: B?): B {
  contract { returns() implies (value != null) }
  return ensureNotNull(value) { ErrorResponse }
}

And finally we create a DSL function, that enables the above defined additional DSL syntax.

suspend fun <A> posRes(
  block: suspend PoseidonResEffectScope.() -> A
): PoseidonRes<A> = effect<ErrorResponse, A> {
  block(PoseidonResEffectScope(this))
}.toPoseidonRes()

Additional info:

With context receivers (and upcoming feature in Kotlin) we could simplify the above 2 code snippets a ton.

context(EffectScope<ErrorResponse>)
suspend fun <T> PoseidonRes<T>.bind(): T = when (this) {
  is SuccessResponse -> this.response
  is ErrorResponse -> shift(this)
}

suspend fun <A> posRes(
  block: suspend EffectScope<ErrorResponse>.() -> A
): PoseidonRes<A> =
  effect<ErrorResponse, A>(block).toPoseidonRes()

EDIT to answer additional questions from comment:

  1. ensure is a monadic function to check for invariants. In pure-functional land type-refinement is often used instead to check invariants at compile-time, or force runtime checks like this. In Java, and Kotlin, people typically use if(condition) throw IllegalArgumentException(...). ensure replaces that pattern for a monadic equivalent, and ensureNotNull does the same but it leverages Kotlin contracts to smart-cast the passed value to non-null.

  2. Yes, you could change the signatures to:

suspend fun <A> posRes(
  block: suspend EffectScope<PoseidonRes<A>>.() -> A
): PoseidonRes<A> =
  effect<PoseidonRes<A>, A>(block)
    .fold({ res: PoseidonRes<A> -> res }) { a -> SuccessResponse(a) }

This signature is valid, and you don't "lose anything", and there can be some good use-cases for this. For example if you finish early, and want to skip the remaining logic. Finishing early doesn't mean failure perse.

It can for example also mean that you returned 500 Internal Server to the user, and thus handled the result already. By-passing any additional computation since a response was already send.

  1. Currently there is no way to skip calling bind. At least not stably for Kotlin MPP. bind is the equivalent of <- in Haskells do-notation or Scala's for comprehensions. You can leverage experimental context receivers on the JVM to remove the need for calling bind though.
context(EffectScope<ErrorResponse>)
suspend fun one(): Int {
  shift(ErrorResponse) // We have access to shift here
  1
}

context(EffectScope<ErrorResponse>)
suspend otherCode(): Int = one() + one()

More details on this pattern can be found here:

Currently you have to explicitly enable the experimental feature, and it only works for the JVM:

  withType<KotlinCompile>().configureEach {
    kotlinOptions {
      freeCompilerArgs = freeCompilerArgs + "-Xcontext-receivers"
    }
  }
nomisRev
  • 1,881
  • 8
  • 15
  • 1
    Thanks for the response. It seems syntactically similar, but I have a few questions on this: 1. Not really sure what ensure is supposed to be doing here, can you explain? It seems to be a way to emit new failures if they happen? 2. Is there a way to shift with a SuccessResponse? I'm imagining turning my Shift parameter to a PoseidonRes so that I can shift out with a success at any time? What do I loose with this? 3. Is there a general way to not have to call bind? I'm coming from pure-functional land and used to more syntax sugar around binding. – Grant Everett Aug 02 '22 at 07:23
  • Hey @GrantEverett, My pleasure! I edited my original answer to include the answers to your follow-up questions. I hope they clarify your doubts! – nomisRev Aug 02 '22 at 09:50
  • Thanks! This really cleared things up for me. – Grant Everett Aug 02 '22 at 15:15
  • Thanks for all this again. I have one more clarifying question, which if you like could merit a separate SO post. I'm confused about this line `public value class PoseidonResEffectScope(private val cont: EffectScope< ErrorResponse>) : EffectScope` Why is the EffectScope the val in the constructor as well as the interface? I get that you're passing in the EffectScope from the posRes fun, but I'm just wondering why? Is it just to get context for shift so we use the library provided effect? – Grant Everett Aug 04 '22 at 17:26
  • We need to create a new type to add some additional syntax to the `EffectScope` based DSL. Since we can't use delegation `EffectScope by cont` we have to manually delegate the functions so we capture the library provided effect into a `private val`. Then we also use the `private val cont` to implement a custom DSL for your own domain. The reason for doing this is two fold, 1. to add custom syntax to the DSL 2. because you don't want to implement `shift` yourself but use delegate instead. – nomisRev Aug 04 '22 at 17:48