根据文档,我应该用对象实现效果。
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) })
}
这是教程中的一般指南。我很好奇是否有人知道他们为什么要使用一个物体?下面是我的具体用例,用于进一步的上下文:
我们已经有了一个包装器对象,称为PoseidonRes
,它可以是成功的,也可以是错误的。我们普遍使用这一点,不想到处切换到"非此即彼"类型。话虽如此,以下是我的自定义效果,以及我是如何实现它的
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 }
)
主要的区别是,我将函数接口实现为一个函数,而不是一个被调用的对象。我真的很想知道为什么推荐一种方式,而这似乎很好。我到处找医生,但找不到答案。如果它真的在文档中,请RTF M我。
在呼叫中心,它看起来像
posRes {
val myThing1 = thingThatsPoseidonResYielding().bind()
val myThing2 = thingThatsPosiedonResYielding2().bind
SuccessResponse(order.from(myThing1, myThing2))
}
两种实现的工作原理似乎都一样。测试通过任何一种方式。这是怎么回事?
我们最近发布了一个更新的API,它以更方便的方式简化了此类抽象的构建。同时还提供更大的性能优势!
以下是Option的一个示例。
翻译到您的域名:
首先,我们创建一个函数,将Effect<ErrorResponse, A>
映射到自定义类型。当您编写任何其他可能导致ErrorResponse
的程序/Effect
,并且您希望转换为自定义类型时,这非常有用。
public suspend fun <A> Effect<ErrorResponse, A>.toPoseidonRes(): PoseidonRes<A> =
fold({ it }) { SuccessResponse(it) }
接下来,我们创建一些额外的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 }
}
最后,我们创建了一个DSL函数,它支持上面定义的附加DSL语法。
suspend fun <A> posRes(
block: suspend PoseidonResEffectScope.() -> A
): PoseidonRes<A> = effect<ErrorResponse, A> {
block(PoseidonResEffectScope(this))
}.toPoseidonRes()
附加信息:
有了上下文接收器(以及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()
编辑以回答评论中的其他问题:
ensure
是一个用于检查不变量的一元函数。在纯函数领域,类型精化通常用于在编译时检查不变量,或强制执行类似的运行时检查。在Java和Kotlin中,人们通常使用if(condition) throw IllegalArgumentException(...)
。ensure
用一元等价物替换了该模式,ensureNotNull
也做了同样的事情,但它利用Kotlin契约将传递的值智能地转换为non-null
。是的,您可以将签名更改为:
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) }
这个签名是有效的;失去任何东西";,这方面可能有一些好的用例。例如,如果您提前完成,并希望跳过剩余的逻辑。提前完成并不意味着失败。
例如,这也可能意味着您向用户返回了500 Internal Server
,从而已经处理了结果。通过传递任何额外的计算,因为已经发送了响应。
- 当前无法跳过调用
bind
。至少Kotlin MPP不稳定。bind
相当于Haskellsdo表示法或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()
关于这种模式的更多细节可以在这里找到:
- https://youtu.be/g79A6HmbW5M?t=2571
- https://nomisrev.github.io/context-receivers/
- https://github.com/Kotlin/KEEP/blob/master/proposals/context-receivers.md
目前您必须显式启用实验功能,并且它只适用于JVM:
withType<KotlinCompile>().configureEach {
kotlinOptions {
freeCompilerArgs = freeCompilerArgs + "-Xcontext-receivers"
}
}