在 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
和 Applicative
和 Monad
类似。
当然,对于具有多个可能值的参数类型,这种详尽的案例列表定义将变得完全不切实际,但它始终是相同的原则。
但要注意的重要一点是实例总是特定于一个特定参数。因此,Bool -> Int
和 Bool -> String
属于同一个单子,但 Int -> Int
和 Char -> 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 -> a
,funct <*> (+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
后跟三个参数往往会让我的大脑倾斜。
我最近一直在尝试使用 "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
和 Applicative
和 Monad
类似。
当然,对于具有多个可能值的参数类型,这种详尽的案例列表定义将变得完全不切实际,但它始终是相同的原则。
但要注意的重要一点是实例总是特定于一个特定参数。因此,Bool -> Int
和 Bool -> String
属于同一个单子,但 Int -> Int
和 Char -> 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 -> a
,funct <*> (+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
后跟三个参数往往会让我的大脑倾斜。