繁体   English   中英

在Haskell中理解作为应用的函数

[英]Understanding Functions as Applicatives in Haskell

我最近一直在尝试通过“向您学习Haskell”来学习Haskell,并且一直在努力理解作为Applicatives的功能。 我应该指出,使用其他类型的Applicatives(例如List)和Maybe我似乎已经足够了解,可以有效地使用它们。

正如我在尝试理解某些事物时通常会做的那样,我会尝试使用尽可能多的示例,并且一旦模式出现,事情就会变得有意义。 因此,我尝试了一些示例。 随附的注释是我尝试的几个示例以及绘制的图表以可视化正在发生的情况的图表。

在此处输入图片说明

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

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

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

因此,所有这些都很好,当我具有任意数量的参数的函数(尽管需要2个或更多)时,我可以理解模式,并将其应用于采用一个参数的函数。 但是从直觉上来说,这种模式对我来说没有太大意义。

所以这是我有的具体问题:

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

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

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

为什么它对funct <*> (+3)无效,但对funct <*> (+)无效? 而且它确实适用于(\\ ab -> 3) <*> (+)

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

额外详情:

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

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

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中的作用。 您已经有一些示例,所需的定义是(->) 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属于同一个monad,但是Int -> IntChar -> Int不属于同一个单子。 Int -> Double -> Int不属于同一个单子的Int -> Int ,但是只有当你考虑Double -> Int为具有无关的不透明结果类型Int->单子。

因此,如果您正在考虑类似a -> a -> a -> a a-> a- a -> a -> a -> a那么这实际上不是关于应用程序/单子的问题,而是关于Haskell的一般问题。 因此,您不应该期望monad = container图片可以带您到任何地方。 要了解a -> a -> a -> a a- a -> a -> a -> a a- a -> a -> a -> a作为monad的成员,您需要挑选出您正在谈论的箭头; 在这种情况下,它只是最左边的一个,即您在type M=(a->) monad中具有值M (a->a->a) a->a->a之间的箭头不以任何方式参与单子动作。 如果它们在您的代码中包含,则意味着您实际上是在混合多个monad。 在执行此操作之前,您应该了解单个monad的工作原理,因此请坚持仅使用单个功能箭头的示例。

正如David Fletcher指出的 ,函数的(<*>)是:

(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) funct :: Num a => a -> a -> a -> afunct <*> (+3)之所以起作用是因为:

  • 就第一张图片而言: (+ 3) x是一个数字,因此您可以对其应用funct x ,最后得到funct x ((+ 3) x) ,该函数需要两个参数。

  • 就第二张图片而言: funct x是一个带有数字的函数(类型为Num a => a -> a -> a ),因此您可以使用(+ 3) :: Num a => a -> a

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

  • 就第一张图片而言: (+) x不是数字,而是Num a => a -> a函数,因此您无法对其应用funct x

  • 就第二张图片而言: (+)的结果类型,被视为一个参数( (+) :: Num a => a -> (a -> a) Num a => a -> a (+) :: Num a => a -> (a -> a)Num 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会使我的大脑倾斜。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM