在 Haskell 中将函数理解为应用程序

Understanding Functions as Applicatives in Haskell

我最近一直在尝试使用 "Learn You a Haskell" 学习 Haskell,并且一直在努力理解作为应用程序的函数。我应该指出,使用其他类型的 Applicatives,如 Lists 和 Maybe,我似乎理解得足够好,可以有效地使用它们。

正如我在尝试理解某事时倾向于做的那样,我尝试尽可能多地使用示例,一旦模式出现,事情就会变得有意义。因此,我尝试了几个例子。附件是我尝试的几个示例的注释以及我绘制的图表,试图形象化正在发生的事情。

funct 的定义似乎与结果无关,但在我的测试中,我使用了具有以下定义的函数:

funct :: (Num a) => a -> a -> a -> a

在底部,我尝试使用普通数学符号来显示与图表中相同的内容。

所以这一切都很好,当我有一个具有任意数量参数的函数(尽管需要 2 个或更多)并将其应用于接受一个参数的函数时,我可以理解这种模式。然而直觉上,这种模式对我来说意义不大。

下面是我的具体问题:

理解我所看到的模式的直观方法是什么,特别是如果我将 Applicative 视为容器(这就是我查看 Maybe 和列表的方式)?

<*> 右侧的函数接受多个参数时的模式是什么(我主要使用函数 (+3)(+5)对)?

为什么 <*> 右侧的函数应用于左侧函数的第二个参数。例如,如果右边的函数是 f() 那么 funct(a,b,c) 变成 funct (x, f(x), c)?

为什么它适用于 funct <*> (+3) 但不适用于 funct <*> (+)?此外,它确实适用于 (\ a b -> 3) <*> (+)

任何能让我对这个概念有更好直观理解的解释都将不胜感激。我读了其他解释,比如我提到的书中用 ((->)r) 或类似模式解释函数,但即使我知道如何在定义函数时使用 ->) 运算符,我也不确定我在这种情况下理解它。

额外详情:

我还想包括我用来帮助我形成上面图表的实际代码。

首先,我定义了函数,如上所示:

funct :: (Num a) => a -> a -> a -> a

在整个过程中,我以各种方式改进函数以了解发生了什么。

接下来我尝试了这段代码:

funct a b c = 6 
functMod =  funct <*> (+3)
functMod 2 3

不出所料,结果是 6

所以现在我尝试像这样直接返回每个参数:

funct a b c = a
functMod =  funct <*> (+3)
functMod 2 3 -- returns 2

funct a b c = b
functMod =  funct <*> (+3)
functMod 2 3 -- returns 5

funct a b c = c
functMod =  funct <*> (+3)
functMod 2 3 -- returns 3

据此我可以确认第二张图是发生了什么。我也重复了这个模式来观察第三张图(这是第二次在顶部扩展的相同模式)。

您通常可以理解 Haskell 中的功能,如果您 将它的定义代入一些例子。你已经有一些 示例和您需要的定义是 <*> for (->) a 这是 这个:

(f <*> g) x = f x (g x)

我不知道你是否会找到比使用 定义几次。

在你的第一个例子中我们得到这个:

  (funct <*> (+3)) x
= funct x ((+3) x)
= funct x (x+3)

(因为没有 funct <*> (+3) 我无能为力 进一步的参数我只是将它应用到 x - 在你需要的时候做这个 到。)

其余的:

  (funct <*> (+3) <*> (+5)) x
= (funct x (x+3) <*> (+5)) x
= funct x (x+3) x ((+5) x)
= funct x (x+3) x (x+5)

  (funct <*> (+)) x
= funct x ((+) x)
= funct x (x+)

注意你不能对这两个使用相同的 funct - 在 第一个可以取四个数字,但第二个需要取 一个数字和一个函数。

  ((\a b -> 3) <*> (+)) x
= (\a b -> 3) x (x+)
= (\b -> 3) (x+)
= 3

  (((\a b -> a + b) <*> (+)) x
= (\a b -> a + b) x (x+)
= x + (x+)
= type error

可以将函数 monad 视为容器。请注意,对于每个参数类型,它实际上是一个单独的 monad,因此我们可以选择一个简单的示例:Bool.

type M a = Bool -> a

这相当于

data M' a = M' { resultForFalse :: a
               , resultForTrue :: a  }

并且可以定义实例

instance Functor M where            instance Functor M' where
  fmap f (M g) = M g'                 fmap f (M' gFalse gTrue) = M g'False g'True
   where g' False = f $ g False        where g'False = f $ gFalse
         g' True  = f $ g True               g'True  = f $ gTrue

ApplicativeMonad 类似。

当然,对于具有多个可能值的参数类型,这种详尽的案例列表定义将变得完全不切实际,但它始终是相同的原则。

但要注意的重要一点是实例总是特定于一个特定参数。因此,Bool -> IntBool -> String 属于同一个单子,但 Int -> IntChar -> Int 不属于。 Int -> Double -> Int 确实与 Int -> Int 属于同一个 monad,但前提是您将 Double -> Int 视为与 Int-> monad 无关的不透明结果类型。

因此,如果您正在考虑类似 a -> a -> a -> a 的问题,那么这实际上不是关于 applicatives/monads 的问题,而是关于 Haskell 的一般问题。因此,您不应该期望 monad=container 图片能带您到任何地方。要将 a -> a -> a -> a 理解为 monad 的成员,您需要找出您在谈论的是哪个箭头;在这种情况下,它只是最左边的一个,即 type M=(a->) monad 中的值 M (a->a->a)a->a->a 之间的箭头不以任何方式参与 monadic 动作;如果它们在您的代码中这样做,则意味着您实际上是在将多个 monad 混合在一起。在你这样做之前,你应该了解单个 monad 是如何工作的,所以坚持使用只有一个函数箭头的示例。

(<*>) 函数是:

(g <*> f) x = g x (f x)

有两张 (<*>) 函数的直观图片,虽然不能完全阻止它令人眼花缭乱,但可能有助于在您浏览使用它的代码时保持平衡。在接下来的几段中,我将使用 (+) <*> negate 作为 运行 示例,因此您可能希望在继续之前在 GHCi 中尝试几次。

第一张图片是(<*>),因为将一个函数的结果应用到另一个函数的结果:

g <*> f = \x -> (g x) (f x)

例如,(+) <*> negate将一个参数传递给(+)negate,分别给出一个函数和一个数字,然后将一个应用于另一个...

(+) <*> negate = \x -> (x +) (negate x)

... 这解释了为什么它的结果总是 0.

第二张图片是(<*>)作为函数组合的变体,其中参数还用于确定要组合的第二个函数

g <*> f = \x -> (g x . f) x

从这个角度来看,(+) <*> negate 否定参数,然后将参数添加到结果中:

(+) <*> negate = \x -> ((x +) . negate) x

如果您有 funct :: Num a => a -> a -> a -> afunct <*> (+3) 会起作用,因为:

  • 就第一张图而言:(+ 3) x是一个数字,所以你可以对它应用funct x,最后得到funct x ((+ 3) x),一个函数有两个参数。

  • 就第二张图而言:funct x是一个接受数字的函数(类型Num a => a -> a -> a),所以你可以用[=38=组合它].

另一方面,funct <*> (+),我们有:

  • 第一张图:(+) x不是一个数字,而是一个Num a => a -> a函数,所以不能对它应用funct x .

  • 就第二张图而言:(+)的结果类型,作为一个参数((+) :: Num a => a -> (a -> a))的函数来看,是Num a => a -> a(而不是 Num a => a),所以你不能用 funct x 组合它(需要 Num a => a)。

对于使用 (+) 作为 (<*>) 的第二个参数的任意示例,请考虑函数 iterate:

iterate :: (a -> a) -> a -> [a]

给定一个函数和一个初始值,iterate 通过重复应用该函数生成一个无限列表。如果我们将参数翻转为 iterate,我们最终会得到:

flip iterate :: a -> (a -> a) -> [a]

考虑到 funct <*> (+) 的问题是 funct x 不会采用 Num a => a -> a 函数,这似乎有一个合适的类型。果然:

GHCi> take 10 $ (flip iterate <*> (+)) 1
[1,2,3,4,5,6,7,8,9,10]

(切线说明,如果您使用 (=<<) 而不是 (<*>),则可以省略 flip。)


最后,这两张直观的图片都不太适合应用样式表达式的常见用例,例如:

(+) <$> (^2) <*> (^3)

要使用那里的直观图片,您必须考虑函数的 (<$>) 是如何 (.),这使事情变得有些模糊。将整个事情视为提升的应用程序会更容易:在本例中,我们将 (^2) 和 (^3) 的结果相加。等效拼写为...

liftA2 (+) (^2) (^3)

...有点强调这一点。不过,就个人而言,我觉得在此设置中编写 liftA2 的一个可能缺点是,如果您在同一表达式中正确应用结果函数,您最终会得到类似...

的结果
liftA2 (+) (^2) (^3) 5

... 看到 liftA2 后跟三个参数往往会让我的大脑倾斜。