在Haskell中理解作为应用的功能

时间:2019-05-05 10:45:22

标签: haskell monads functor applicative

我最近一直在尝试通过“ Learn a Haskell”来学习Haskell,并且一直在努力理解作为Applicatives的功能。我应该指出,使用Lists和Lists等其他类型的Applicatives似乎足够了解,可以有效地使用它们。

就像我在尝试理解某些事物时倾向于做的那样,我尝试尝试尽可能多的示例,一旦这种模式出现,事情就会变得有意义。因此,我尝试了一些示例。随附的是我为试过的几个示例以及绘制的图表以可视化正在发生的情况而做的笔记。

enter image description here

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

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

3 个答案:

答案 0 :(得分:2)

通常,您可以了解Haskell中的函数在做什么 将其定义替换为一些示例。你已经有一些 示例和您需要的定义是<*>的{​​{1}},即 这个:

(->) 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

答案 1 :(得分:1)

可以将功能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 -> IntInt -> Int属于同一个monad,但前提是您将Double -> Int视为与Int-> monad无关的不透明结果类型。

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

答案 2 :(得分:1)

As pointed out by 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)之所以有效,是因为:

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

  • 就第二张图片而言:funct x是一个具有数字的函数(类型为Num a => a -> a -> a),因此您可以将其与(+ 3) :: Num a => a -> a组成。

  • p>

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

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

  • 就第二张图片而言:(+)的结果类型被视为一个参数((+) :: Num a => a -> (a -> a)的函数时,为Num a => a -> a(而不是{{ 1}}),因此您不能将其与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] 而不是flipThat, however, is a different story,则可以省去(=<<)。)


最后,两张直观的图片都没有特别适合诸如如下的应用样式表达的常见用例:

(<*>)

要在此处使用直观的图片,您必须考虑(+) <$> (^2) <*> (^3) 的功能如何(<$>),这使事情变得相当混乱。将整个事情看作是提升后的应用程序会更容易:在此示例中,我们将(^ 2)和(^ 3)的结果相加。等同于...

(.)

...在某种程度上强调了这一点。但是,就我个人而言,我觉得在此设置中编写liftA2 (+) (^2) (^3) 的一个可能的缺点是,如果将结果函数正确地应用到同一表达式中,最终会得到类似...的信息。

liftA2

...看到liftA2 (+) (^2) (^3) 5 后跟三个参数往往会使我的大脑倾斜。