应用仿函数更有趣

Mat*_*hid 19 monads haskell applicative

之前我曾询问过将monadic代码翻译为仅使用Parsec的applicative functor实例.不幸的是,我得到了几个回复,回答了我真正问过的问题,但并没有给我太多的了解.那么让我再试一次......

总结我到目前为止的知识,一个应用函子比一个monad更受限制.在"少即是多"的传统中,限制代码可以做什么会增加疯狂代码操作的可能性.无论如何,很多人似乎相信使用applicative而不是monad是一种可行的优越解决方案.

Applicative类的定义中Control.Applicative,它的黑线鳕的上市有益分离类的方法和实用功能与他们之间的类实例的广阔裹,使其很难迅速在屏幕上看到的一切在一次.但相关的类型签名是

pure ::    x              -> f x
<*>  :: f (x -> y) -> f x -> f y
 *>  :: f  x       -> f y -> f y
<*   :: f  x       -> f y -> f x
<$>  ::   (x -> y) -> f x -> f y
<$   ::    x       -> f y -> f x
Run Code Online (Sandbox Code Playgroud)

做得很完美,对吧?

好吧,Functor已经给了我们fmap,基本上就是这样<$>.即,由于从功能xy,我们可以映射一个f xf y.Applicative添加两个基本上新的元素.一个是pure,其类型与return(以及各种类别理论类中的其他几个运算符)大致相同.另一个是<*>,它使我们能够获取一个容器的容器和一个输入容器,并产生一个输出容器.

使用上面的运算符,我们可以非常巧妙地做一些事情

foo <$> abc <*> def <*> ghi
Run Code Online (Sandbox Code Playgroud)

这允许我们采用N-ary函数并从N个函子中以一种易于推广到任何N的方式来源它的参数.


这个我已经明白了.我还有两件不太明白的事情.

一,功能*>,<*<$.从它们的类型,<* = const,*> = flip const,和<$可能是类似的东西.据推测,这并没有描述这些功能实际上做了什么.(?!)

其次,在编写Parsec解析器时,每个可解析的实体通常最终看起来像这样:

entity = do
  var1 <- parser1
  var2 <- parser2
  var3 <- parser3
  ...
  return $ foo var1 var2 var3...
Run Code Online (Sandbox Code Playgroud)

由于应用程序仿函数不允许我们以这种方式将中间结果绑定到变量,所以我很困惑如何在最后阶段收集它们.我无法完全理解这个想法,以便理解如何做到这一点.

Tik*_*vis 26

<**>功能都非常简单:他们的工作方法一样>>.该<*会的工作方式相同<<,除了<<不存在.基本上,给定a *> b,你首先"做" a,然后你"做" b并返回结果b.因为a <* b,你仍然首先"做" a然后"做" b,但你返回的结果a.(当然,对于"do"的适当含义.)

<$函数是fmap const.所以a <$ b等于fmap (const a) b.你只需丢弃"动作"的结果并返回一个常量值.具有类型的Control.Monad函数可以写为.voidFunctor f => f a -> f ()() <$

这三个函数对于applicative functor的定义并不重要.(<$事实上,它适用于任何仿函数.)这同样>>适用于monad.我相信他们在课堂上可以更轻松地针对特定情况对其进行优化.

当您使用applicative functor时,您不会从仿函数中"提取"该值.在monad中,这是什么>>=,以及什么是foo <- ...desugars.相反,您使用<$>和直接将包装的值传递给函数<*>.所以你可以将你的例子重写为:

foo <$> parser1 <*> parser2 <*> parser3 ...
Run Code Online (Sandbox Code Playgroud)

如果你想要中间变量,你可以使用一个let语句:

let var1 = parser1
    var2 = parser2
    var3 = parser3 in
foo <$> var1 <*> var2 <*> var3
Run Code Online (Sandbox Code Playgroud)

正如你所推测的那样,pure只是另一个名字return.因此,为了使共享结构更加明显,我们可以将其重写为:

pure foo <*> parser1 <*> parser2 <*> parser3
Run Code Online (Sandbox Code Playgroud)

我希望这能澄清事情.

现在只是一点点说明.人们建议使用适用函子功能解析.但是,如果它们更有意义,你应该只使用它们!对于足够复杂的东西,monad版本(尤其是带有do-notation)实际上可以更清晰.人们推荐这个的原因是

foo <$> parser1 <*> parser2 <*> parser3
Run Code Online (Sandbox Code Playgroud)

比起来更简短,更易读

do var1 <- parser1
   var2 <- parser2
   var3 <- parser3
   return $ foo var1 var2 var3
Run Code Online (Sandbox Code Playgroud)

从本质上讲,f <$> a <*> b <*> c它基本上就像提升功能应用程序.您可以将其<*>视为替换空间(例如,函数应用程序),其方式与fmap替换函数应用程序的方式相同.这也应该为您提供一个直观的概念,说明我们使用它们的原因<$>- 就像它的升级版本一样$.

  • 事实上,`(<$)`和`(<$>)`在`Data.Functor`中定义,只从`Control.Applicative`模块重新导出:) (5认同)
  • 因此,关键的见解似乎是,不是运行解析器,将结果绑定到名称,然后在最后使用这些名称,我们可以简单地执行`foo <$> parser1 <*> parser2 <*> parser3`.再加上yatima的提示,我认为这给了我所有我需要知道的东西. (2认同)

Wil*_*ess 12

我可以在这里发表一些评论,希望对你有所帮助.这反映了我的理解本身可能是错误的.

pure异常命名.通常,函数的名称是指它们产生的东西,但pure xx纯粹的.pure x产生一个"携带"纯净的应用函子x."携带"当然是近似的.一个例子:pure 1 :: ZipList Int是一个ZipList带有纯粹Int价值的人1.

<*>,*><*,不是功能,而是方法(这回答了你的第一个问题).f在它们的类型中不是通用的(就像它对于函数一样),而是特定的,由特定实例指定.这就是为什么他们确实不仅仅是$,flip const而且const.专用类型f指定组合的语义.在通常的应用风格编程中,组合意味着应用.但是对于仿函数,存在另外的维度,由"载体"类型表示f.在f x,有一个"内容",x但也有一个"上下文",f.

"applicative functors"风格试图通过效果实现"应用风格"编程.由仿函数,载体,背景提供者代表的效果; "应用"指的是功能应用的正常应用方式.写作只是f x为了表示应用程序曾经是一个革命性的想法.不需要额外的语法,不(funcall f x),没有CALL语句,没有这些额外的东西 - 组合应用程序 ......不是这样,有效果,似乎 - 在使用效果进行编程时,还需要特殊的语法.被杀的野兽再次出现了.

因此,应用程序编程与效果再次使组合意味着应用 - 在特殊(可能有效)的上下文中,如果它们确实这样的上下文中.因此,对于a :: f (t -> r)b :: f t(几乎纯)组合a <*> b的携带的内容的应用程序(或类型t -> rt),在给定的上下文中(类型的f).

与monad的主要区别在于,monad是非线性的.在

do {  x        <-  a
   ;     y     <-  b x
   ;        z  <-  c x y
   ;               return 
     (x, y, z) }
Run Code Online (Sandbox Code Playgroud)

计算b x取决于x,并c x y取决于xy.这些函数是嵌套的:

a >>= (\x ->  b x  >>= (\y ->  c x y  >>= (\z ->  .... )))
Run Code Online (Sandbox Code Playgroud)

如果bc依赖于以前的结果(x,y),这可制成扁平通过使计算阶段返回重新包装,复合数据(这解决您的第二个问题):

a  >>= (\x       ->  b  >>= (\y-> return (x,y)))       -- `b  ` sic
   >>= (\(x,y)   ->  c  >>= (\z-> return (x,y,z)))     -- `c  `
   >>= (\(x,y,z) ->  ..... )
Run Code Online (Sandbox Code Playgroud)

这实质上是一种应用性样式(b,c预先充分已知的,独立于所述值的x所产生a,等).因此,当您的组合创建包含进一步组合所需的所有信息的数据时,并且不需要"外部变量"(即所有计算已经完全已知,独立于任何前一阶段产生的任何值),您可以使用这种风格的组合.

但是如果你的monadic链的分支依赖于这种"外部"变量的值(即monadic计算的前一阶段的结果),那么你就不能用它来形成一个线性链.那基本上是 monadic.


作为一个例子,该论文的第一个例子显示了"monadic"功能

sequence :: [IO a] ? IO [a]
sequence [ ] = return [ ]
sequence (c : cs) = do
  {  x       <-  c
  ;      xs  <-  sequence cs  -- `sequence cs` fully known, independent of `x`
  ;              return 
    (x : xs) }
Run Code Online (Sandbox Code Playgroud)

实际上可以用这种"扁平,线性"的方式编码

sequence :: (Applicative f) => [f a] -> f [a]
sequence []       = pure []
sequence (c : cs) = pure (:) <*> c <*> sequence cs
                  --     (:)     x     xs
Run Code Online (Sandbox Code Playgroud)

这里没有使用monad能够分支以前的结果.


关于优秀的PetrPudlák答案的说明:在我的"术语"中,他pair是没有申请的组合.它表明,Applictive Functors为简单Functor添加的内容的本质是结合的能力.然后由好老人实现申请.这表明组合仿函数可能是一个更好的名称(更新:事实上,"Monoidal Functors"就是这个名字).fmap


Pet*_*lák 8

你可以像这样查看仿函数,应用程序和monad:它们都带有一种"效果"和"价值".(请注意,术语"效果"和"值"只是近似值 - 实际上并不需要任何副作用或值 - 例如in IdentityConst.)

  • 有了Functor你可以修改的可能值内使用fmap,但你不能做内影响任何东西.
  • 使用Applicative,您可以创建一个没有任何效果的值pure,并且您可以对效果进行排序并将其值组合在一起.但效果和值是分开的:在排序效果时,效果不能取决于前一个效果的值.这反映在<*,<*>*>:它们对效果进行排序并组合它们的值,但您无法以任何方式检查内部的值.

    您可以Applicative使用此备用函数集进行定义:

    fmap     :: (a -> b) -> (f a -> f b)
    pureUnit :: f ()
    pair     :: f a -> f b -> f (a, b)
    -- or even with a more suggestive type  (f a, f b) -> f (a, b)
    
    Run Code Online (Sandbox Code Playgroud)

    (在哪里pureUnit没有任何效果)pure<*>从中定义(反之亦然).这里pair排序两个效果,并记住它们的两个值.这个定义表达了Applicative一个monoidal仿函数的事实.

    现在考虑任意的(有限的)表达包括pair,fmap,pureUnit和一些原始应用性值.我们有几个可以使用的规则:

    fmap f . fmap g           ==>     fmap (f . g)
    pair (fmap f x) y         ==>     fmap (\(a,b) -> (f a, b)) (pair x y)
    pair x (fmap f y)         ==>     -- similar
    pair pureUnit y           ==>     fmap (\b -> ((), b)) y
    pair x pureUnit           ==>     -- similar
    pair (pair x y) z         ==>     pair x (pair y z)
    
    Run Code Online (Sandbox Code Playgroud)

    使用这些规则,我们可以重新排序pairs,fmap向外推s并消除pureUnits,因此最终可以将这种表达式转换为

    fmap pureFunction (x1 `pair` x2 `pair` ... `pair` xn)
    
    Run Code Online (Sandbox Code Playgroud)

    要么

    fmap pureFunction pureUnit
    
    Run Code Online (Sandbox Code Playgroud)

    实际上,我们可以先pair使用纯函数收集所有效果,然后使用纯函数修改结果值.

  • 使用时Monad,效果可以取决于先前monadic值的值.这使他们如此强大.


yat*_*975 6

已经给出的答案是优秀的,但有一个小的(ISH)一点我想明确地拼写出来,它与做的<*,<$*>.

其中一个例子是

do var1 <- parser1
   var2 <- parser2
   var3 <- parser3
   return $ foo var1 var2 var3
Run Code Online (Sandbox Code Playgroud)

也可以写成foo <$> parser1 <*> parser2 <*> parser3.

假设值与之var2无关foo- 例如,它只是一些分隔空格.那么foo接受这个空格只是为了忽略它也没有意义.在这种情况下foo应该有两个参数,而不是三个.使用do-notation,您可以将其写为:

do var1 <- parser1
   parser2
   var3 <- parser3
   return $ foo var1 var3
Run Code Online (Sandbox Code Playgroud)

如果你只想使用它来编写它<$>,<*>它应该像这些等效表达式之一:

(\x _ z -> foo x z) <$> parser1 <*> parser2 <*> parser3
(\x _ -> foo x) <$> parser1 <*> parser2 <*> parser3
(\x -> const (foo x)) <$> parser1 <*> parser2 <*> parser3
(const  . foo) <$> parser1 <*> parser2 <*> parser3
Run Code Online (Sandbox Code Playgroud)

但是,通过更多的论点来解决这个问题真是太棘手了!

但是,你也可以写foo <$> parser1 <* parser2 <*> parser3.你可以把foo它送入结果的语义功能parser1parser3而忽视结果parser2之间.缺席>意味着忽视.

如果你想忽略结果parser1但是使用其他两个结果,你可以类似地写foo <$ parser1 <*> parser2 <*> parser3,<$而不是使用<$>.

我从来没有找到太多用处*>,我通常会id <$ p1 <*> p2为解析器编写而忽略结果p1而只是解析p2; 你可以写这个,p1 *> p2但这会增加代码读者的认知负担.

我已经为解析器学习了这种思维方式,但后来又被推广到了Applicatives; 但我认为这个符号来自于uuparsing库 ; 至少我在10多年前在乌得勒支使用它.