4

这是 kleisli 组合的常见实现:

kleisli :: Monad m => (a -> m b) -> (b -> m c) -> a -> m c
kleisli = \f g x -> f x >>= g

为什么它不期望一元上下文中的值呢?我确信这是有充分理由的。我只是没有设法看到它。

kleisli' :: Monad m => (a -> m b) -> (b -> m c) -> m a -> m c
kleisli' = \f g x -> x >>= f >>= g

该类型似乎更易于组合,并且return可以在调用站点上只有纯值的情况下使用。

4

2 回答 2

6

Kleisli 组合实际上是回答常见问题的最简单方法之一:单子有什么用?

我们可以用普通函数做的最有用的事情之一就是组合它们。给定f :: a -> band g :: b -> c,我们可以先执行f然后g对结果执行,给我们g . f :: a -> c

只要我们只需要使用“普通”函数,那就太棒了。但是一旦我们开始在“现实世界”中编程,如果我们希望我们的语言保持纯粹和引用透明,我们很可能会遇到无法继续使用这些功能的情况。事实上,在这种情况下,其他没有 Haskell 原则性的语言放弃任何纯粹的伪装。考虑这些日常情况:

  • 我们的函数f有时可能无法返回值。在许多其他语言中,这将通过返回来表示null,但您不能将其输入g. (您当然可以进行调整g以应对null输入,但这很快就会重复。)

在 Haskell 中,我们没有nulls,我们有Maybe类型构造函数来显式地表示可能没有值。这意味着f需要有 type a -> Maybe b。出于同样的原因g将有类型。b -> Maybe c但是在这样做的过程中,我们失去了组合这两个函数的能力,因为我们不能直接将 type 的值提供Maybe b给需要 type 输入的值b

  • 的结果f可能取决于一些副作用(例如来自用户的输入,或数据库查询的结果)。这在不纯语言中没有问题,但在 Haskell 中,为了保持纯洁性,我们必须以 type 函数的形式实现它a -> IO b。再一次,g会以相同的形式结束,b -> IO c我们已经失去了天真地组合这两个函数的能力。

我相信你可以看到这是怎么回事。在这两种情况下(并且可以很容易地提供更多,每个 monad 一个),我们不得不用 type 中的一个来替换一个简单的 type 函数,a -> ba -> m b解决特定类型的“副作用” - 或者,如果您愿意,适用于函数结果的某种特定类型的“上下文”。这样一来,我们就失去了组合两个函数的能力,而这正是我们在“无副作用”世界中所拥有的。

monad 真正的目的是克服这一点,让我们为这种“不纯函数”恢复一种组合形式。当然,这正是 Kleisli 组合给我们的,一种形式的函数组合,a -> m b它完全满足我们期望的函数组合的属性(即关联性,以及每种类型的“恒等函数”,这里是return :: a -> m a)。

您对类型的“不完全组合”的建议(a -> m b) -> (b -> m c) -> (m a -> m c)通常不会有用,因为结果函数需要一个单子值作为其输入,而在实践中单子值出现的主要方式是作为output s。但是,您仍然可以在需要时执行此操作,只需采用“正确的” Kleisli 组合,并通过>>=.

于 2020-04-24T18:26:58.220 回答
4

a到的 Kleisli 箭头b定义为函数a -> m b。让我们记下它a ~> b(保留m假设)。组合其中两个箭头是什么意思?它应该有这种类型:

(<=<) :: (b ~> c) -> (a ~> b) -> (a ~> c)

现在,如果我们扩展它:

(<=<) :: (b -> m c) -> (a -> m b) -> (a -> m c)

你有它。看起来您正在查看翻转版本(>=>),但这是相同的想法。

这些运算符在 中定义Control.Monad

标准库中还有更正式的Kleisli 箭头定义。

newtype Kleisli m a b = Kleisli { runKleisli :: a -> m b }

它带有一个Category将这种组合实现为(.)运算符的实例(但您必须使用新类型包装)。

于 2020-04-24T17:55:48.860 回答