箭头挂起函数与 monad 理解之间的关系
Relation between Arrow suspend functions and monad comprehension
我是 Arrow 的新手,正在尝试建立我对它的效果系统如何工作的心智模型;特别是它如何利用 Kotlin 的 suspend
系统。我非常模糊的理解如下;如果有人可以确认、澄清或更正它,那就太好了:
因为 Kotlin 不支持更高种类的类型,所以将应用程序和单子实现为类型 类 很麻烦。相反,arrow 从 Kotlin 的挂起机制提供的延续原语中为 Arrow 的所有 monadic 类型派生了它的 monad 功能(绑定和 return)。这是正确的吗?特别是,短路行为(例如,对于 nullable
或 either
)以某种方式实现为定界延续。我不太明白 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.eager
和 either.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
操作。
编译器还在运行时进行了很多优化,就像 Haskell
对 IO
.
所做的那样
所以签名 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/
我希望这能完全回答你的问题。
我是 Arrow 的新手,正在尝试建立我对它的效果系统如何工作的心智模型;特别是它如何利用 Kotlin 的 suspend
系统。我非常模糊的理解如下;如果有人可以确认、澄清或更正它,那就太好了:
因为 Kotlin 不支持更高种类的类型,所以将应用程序和单子实现为类型 类 很麻烦。相反,arrow 从 Kotlin 的挂起机制提供的延续原语中为 Arrow 的所有 monadic 类型派生了它的 monad 功能(绑定和 return)。这是正确的吗?特别是,短路行为(例如,对于 nullable
或 either
)以某种方式实现为定界延续。我不太明白 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.eager
和 either.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
操作。
编译器还在运行时进行了很多优化,就像 Haskell
对 IO
.
所以签名 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/
我希望这能完全回答你的问题。