Why does ArrowKt reccomend I implement my effect interface with an object instead of a function?

Gra*_*ett 6 monads android functional-programming kotlin arrow-kt

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) })
}
Run Code Online (Sandbox Code Playgroud)

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 }
    )
Run Code Online (Sandbox Code Playgroud)

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))
        }
Run Code Online (Sandbox Code Playgroud)

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

nom*_*Rev 5

我们最近发布了一个更新的 API,它可以以更方便的方式简化此类抽象的构建。同时还提供更大的性能优势!

这是Option的示例。

翻译成您的域名:

首先,我们创建一个映射Effect<ErrorResponse, A>到自定义类型的函数。当您编写任何其他程序/Effect可能会导致ErrorResponse并且您想要转换为自定义类型时,这非常有用。

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

接下来我们创建一些额外的 DSL 糖,以便您可以方便地调用bind您自己的类型。

@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 }
}
Run Code Online (Sandbox Code Playgroud)

最后我们创建一个 DSL 函数,它启用上面定义的附加 DSL 语法。

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

附加信息:

借助上下文接收器(以及 Kotlin 中即将推出的功能),我们可以大大简化上述 2 个代码片段。

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()
Run Code Online (Sandbox Code Playgroud)

编辑以回答评论中的其他问题:

  1. ensure是一个检查不变量的一元函数。在纯功能域中,类型细化通常用于在编译时检查不变量,或强制执行这样的运行时检查。在 Java 和 Kotlin 中,人们通常使用if(condition) throw IllegalArgumentException(...). ensure将该模式替换为一元等价物,并ensureNotNull执行相同的操作,但它利用 Kotlin 合约将传递的值智能转换为non-null.

  2. 是的,您可以将签名更改为:

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) }
Run Code Online (Sandbox Code Playgroud)

这个签名是有效的,你不会“丢失任何东西”,并且可能有一些很好的用例。例如,如果您提前完成,并且想要跳过剩余的逻辑。提前完成本身并不意味着失败。

例如,它也可能意味着您已返回500 Internal Server给用户,因此已经处理了结果。由于已发送响应,因此绕过任何额外的计算。

  1. 目前没有办法跳过调用bind。至少对于 Kotlin MPP 来说不稳定。bind相当于<-Haskell 的do-notation或 Scala 的推导式。不过,您可以利用 JVM 上的实验性上下文接收器来消除调用的需要bind
context(EffectScope<ErrorResponse>)
suspend fun one(): Int {
  shift(ErrorResponse) // We have access to shift here
  1
}

context(EffectScope<ErrorResponse>)
suspend otherCode(): Int = one() + one()
Run Code Online (Sandbox Code Playgroud)

有关此模式的更多详细信息可以在此处找到:

目前您必须显式启用实验性功能,并且它仅适用于 JVM:

  withType<KotlinCompile>().configureEach {
    kotlinOptions {
      freeCompilerArgs = freeCompilerArgs + "-Xcontext-receivers"
    }
  }
Run Code Online (Sandbox Code Playgroud)