箭头挂起函数与 monad 理解之间的关系

Relation between Arrow suspend functions and monad comprehension

我是 Arrow 的新手,正在尝试建立我对它的效果系统如何工作的心智模型;特别是它如何利用 Kotlin 的 suspend 系统。我非常模糊的理解如下;如果有人可以确认、澄清或更正它,那就太好了:

因为 Kotlin 不支持更高种类的类型,所以将应用程序和单子实现为类型 类 很麻烦。相反,arrow 从 Kotlin 的挂起机制提供的延续原语中为 Arrow 的所有 monadic 类型派生了它的 monad 功能(绑定和 return)。这是正确的吗?特别是,短路行为(例如,对于 nullableeither)以某种方式实现为定界延续。我不太明白 Kotlin 挂起机制的哪个特殊功能在这里发挥作用。

如果以上大致正确,我有两个后续问题:我应该如何包含非 IO monadic 操作的范围?举一个简单的对象构造和验证例子:

suspend fun mkMessage(msgType: String, appRef: String, pId: String): Message? = nullable {
    val type = MessageType.mkMessageType(msgType).bind()
    val ref = ApplRefe.mkAppRef((appRef)).bind()
    val id = Id.mkId(pId).bind()
    Message(type, ref, id)
}

在Haskell的do-notation中,这将是

mkMessage :: String -> String -> String -> Maybe Message
mkMessage msgType appRef pId = do
    type <- mkMessageType msgType
    ref <- mkAppRef appRef
    id <- mkId pId
    return (Message type ref id)

在这两种情况下,函数 return 都是 monad 类型(一个可为 null 的值,resp. Maybe)。然而,虽然我可以在我认为合适的任何地方使用 Haskell 中的纯函数,但 Kotlin 中的挂起函数只能从挂起函数内调用。通过这种方式,Arrow 中的一个简单的非 IO monad 理解就像一个 IO monad 一样,它必须贯穿我的代码库;我想这是因为挂起机制是为实际的 IO 操作设计的。在不将所有函数都变成挂起函数的情况下,在 Arrow 中实现非 IO monad 理解的推荐方法是什么?或者这真的是要走的路吗?

其次:如果除了非 IO monad(可空、reader 等)之外,我还想拥有 IO - 比如读取文件并解析它 - 我将如何结合这两者影响?说会有多个挂起范围对应于所涉及的不同 monad 是否正确,我需要以某种方式嵌套这些范围,就像我将 monad 转换器堆叠在 Haskell?

上面的两个问题可能意味着我仍然缺乏一个心智模型来连接 Kotlin 挂起机制之上基于延续的实现与 Haskell 中的通用 monad-as-typeclass 实现之间的桥梁。

我不认为我可以回答你问的所有问题,但我会尽力回答我知道如何回答的部分。

What is the recommended way to implement non-IO monad comprehensions in Arrow without making all functions into suspend functions? Or is this actually the way to go?

对于纯代码,您可以分别使用 nullable.eagereither.eager。使用 nullable/either(不使用 .eager)允许您在内部调用挂起函数。使用 eager 意味着您只能调用 non-suspend 函数。 (并非所有在kotlin中有效的函数都被标记为suspend)

Second: If in addition to non-IO monads (nullable, reader, etc.), I want to have IO - say, reading in a file and parsing it - how would i combine these two effects? Is it correct to say that there would be multiple suspend scopes corresponding to the different monads involved, and I would need to somehow nest these scopes, like I would stack monad transformers in Haskell?

您可以使用扩展函数来模拟 Reader。例如:

suspend fun <R> R.doSomething(i: Int): Either<Error, String> = TODO()

合并 Reader + IO + Either。您可以从 Arrow 维护者 Simon 那里找到一个更大的例子 here

舒斯特,

你说得对,Arrow 使用 Kotlin 的暂停功能来编码诸如 monad comphrensions 之类的东西。

回答你的第一个问题:

Kotlin 在语言(和 Kotlin Std)中有 suspend,默认情况下 suspend 只能从其他 suspend 代码中调用。但是,编译器还有一个名为 RestrictsSuspension, this disallows for mixing suspend scopes and thus disallows the ablity to combine IO and Either for example. We expose a secondary DSL, either.eager 的特性,它使用 RestrictsSuspension 编码,它不允许调用 外部 挂起函数。

这允许您编码 mkMessage :: String -> String -> String -> Maybe Message

fun mkMessage(msgType: String, appRef: String, pId: String): Message? = nullable.eager {
    val type = MessageType.mkMessageType(msgType).bind()
    val ref = ApplRefe.mkAppRef((appRef)).bind()
    val id = Id.mkId(pId).bind()
    Message(type, ref, id)
}

回答你的第二个问题: IO 作为 Kotlin 中不需要的数据类型,因为 suspend 可以像在 Haskell 中一样以引用透明的方式实现所有 IO 操作。 编译器还在运行时进行了很多优化,就像 HaskellIO.

所做的那样

所以签名 suspend fun example(): Either<Error, Value> 等同于 Haskell 中的 EitherT IO Error Value。 然而,IO 操作并未在 Kotlin Std 中实现,但在库中 KotlinX Coroutines, and Arrow Fx Coroutines 还提供了一些数据类型和 higher-level 操作,例如在 KotlinX 协程之上定义的 parTraverse .

它与 Haskell 略有不同,因为我们可以 mix 效果而不是 stacking 它们与 monad 转换器。这意味着我们可以从 Either 操作中调用 IO 操作。这是由于特殊功能,以及编译器可以在悬挂系统中进行的优化。这篇博客解释了这种优化是如何工作的,以及它为什么如此强大。 https://nomisrev.github.io/inline-and-suspend/

这里还有一些关于 Kotlin 中的 Continuations 和无标签编码的更多背景知识。 https://nomisrev.github.io/continuation-monad-in-kotlin/

我希望这能完全回答你的问题。