【问题标题】:More fun with applicative functors应用函子更有趣
【发布时间】:2013-02-19 00:15:39
【问题描述】:

Earlier 我询问了关于将一元代码翻译为仅使用 Parsec 的应用函子实例的问题。不幸的是,我收到了几个答复,回答了我真正提出的问题,但并没有真正给我太多的见解。所以让我再试一次......

总结我到目前为止的知识,应用函子是比单子更受限制的东西。在“少即是多”的传统中,限制代码可以做的事情增加了疯狂代码操作的可能性。无论如何,很多人似乎都相信,在可能的情况下,使用 applicative 代替 monad 是一种更好的解决方案。

Applicative 类在 Control.Applicative 中定义,其 Haddock 的列表有助于将类方法和实用程序函数与它们之间的大量类实例分开,从而难以同时快速查看屏幕上的所有内容。但相关的类型签名是

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

很有道理,对吧?

好吧,Functor 已经给了我们fmap,基本上就是&lt;$&gt;。即,给定从xy 的函数,我们可以将f x 映射到f yApplicative 添加了两个本质上新的元素。一个是pure,它的类型与return(以及各种范畴论类中的其他几个运算符)大致相同。另一个是&lt;*&gt;,它使我们能够获取一个函数容器和一个输入容器,并产生一个输出容器。

使用上面的操作符,我们可以非常巧妙地做一些事情,比如

foo <$> abc <*> def <*> ghi

这使我们可以采用 N 元函数并从 N 个函子中获取其参数,这种方式可以轻松推广到任何 N。


这点我已经明白了。主要有两件事我了解。

首先,函数*&gt;&lt;*&lt;$。从它们的类型来看,&lt;* = const*&gt; = flip const&lt;$ 可能是类似的。大概这描述了这些函数实际上做了什么。 (??!)

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

entity = do
  var1 <- parser1
  var2 <- parser2
  var3 <- parser3
  ...
  return $ foo var1 var2 var3...

由于应用函子不允许我们以这种方式将中间结果绑定到变量,我对如何为最后阶段收集它们感到困惑。为了理解如何做到这一点,我一直无法完全围绕这个想法。

【问题讨论】:

  • &lt;$&gt;&lt;$ 并非特定于 Applicative。他们可以在任何Functor 上工作。
  • “从他们的类型来看,&lt;* = const*&gt; = flip const&lt;$ 可能是类似的”(旁注,多年后) f x -&gt; g y -&gt; f xf x -&gt; g y -&gt; g y 等类型就是这种情况。

标签: haskell monads applicative


【解决方案1】:

&lt;**&gt; 函数非常简单:它们的工作方式与&gt;&gt; 相同。 &lt;* 的工作方式与 &lt;&lt; 相同,但 &lt;&lt; 不存在。基本上,给定a *&gt; b,您首先“执行”a,然后您“执行”b 并返回b 的结果。对于a &lt;* b,您仍然首先“执行”a,然后“执行”b,但您返回a 的结果。 (当然,对于“do”的适当含义。)

&lt;$ 函数就是 fmap const。所以a &lt;$ b 等于fmap (const a) b。您只需丢弃“动作”的结果并返回一个常量值。类型为Functor f =&gt; f a -&gt; f ()Control.Monad函数void可以写成() &lt;$

这三个函数不是应用函子定义的基础。 (&lt;$,事实上,适用于任何函子。)这又一次,就像 &gt;&gt; 用于 monads。我相信他们在课堂上可以更轻松地针对特定实例优化它们。

当您使用应用函子时,您不会从函子中“提取”值。在 monad 中,这就是 &gt;&gt;= 所做的,以及 foo &lt;- ... 去糖的目的。相反,您可以使用&lt;$&gt;&lt;*&gt; 将包装后的值直接传递给函数。因此,您可以将示例重写为:

foo <$> parser1 <*> parser2 <*> parser3 ...

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

let var1 = parser1
    var2 = parser2
    var3 = parser3 in
foo <$> var1 <*> var2 <*> var3

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

pure foo <*> parser1 <*> parser2 <*> parser3

我希望这可以澄清一些事情。

现在只是一个小提示。人们确实建议使用应用函子函数进行解析。但是,只有在它们更有意义时才应该使用它们!对于足够复杂的事情,monad 版本(尤其是 do-notation)实际上可以更清晰。人们推荐这个的原因是

foo <$> parser1 <*> parser2 <*> parser3

更短更易读
do var1 <- parser1
   var2 <- parser2
   var3 <- parser3
   return $ foo var1 var2 var3

本质上,f &lt;$&gt; a &lt;*&gt; b &lt;*&gt; c 本质上类似于提升函数应用程序。您可以想象&lt;*&gt; 是空间(例如函数应用程序)的替代品,就像fmap 是函数应用程序的替代品一样。这也应该让您直观地了解我们为什么使用&lt;$&gt;——它就像$ 的升级版。

【讨论】:

  • 其实(&lt;$)(&lt;$&gt;)是在Data.Functor中定义的,只是从Control.Applicative模块重新导出的:)
  • 因此,关键的见解似乎是,与其运行解析器,将它们的结果绑定到名称,然后在最后使用这些名称,我们可以简单地执行foo &lt;$&gt; parser1 &lt;*&gt; parser2 &lt;*&gt; parser3。加上来自 yatima 的提示,我想这给了我所有我需要知道的。
【解决方案2】:

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

pure 名称异常。通常函数的命名是指它们产生的内容,但在pure x 中,xpure x 产生一个应用函子,它“携带”纯 x。 “携带”当然是近似的。一个例子:pure 1 :: ZipList Int 是一个 ZipList,带有一个纯 Int 值,1

&lt;*&gt;*&gt;&lt;* 不是函数,而是方法(这回答了您的第一个问题)。 f 在它们的类型中不是通用的(就像函数那样),而是特定的,由特定实例指定。这就是为什么他们确实不是只是$flip constconst。专用类型f 指定组合的语义。在通常的应用风格编程中,组合意味着应用。但是对于函子,存在一个额外的维度,由“载体”类型f 表示。在f x 中有一个“内容”x,但也有一个“上下文”f

“applicative functors”风格试图启用“applicative style”编程,并带有效果。由函子、载体、上下文提供者代表的效果; “applicative”指的是函数式应用的正常应用风格。 只写f x 来表示应用程序曾经是一个革命性的想法。不再需要额外的语法,没有(funcall f x),没有CALL 语句,没有这些额外的东西 - combinationapplication...不是这样,有效果,看似 - 在使用效果进行编程时,再次需要特殊语法。被杀的野兽再次出现。

所以Applicative Programming with Effects 再次使组合意味着只是应用 - 在特殊(可能有效)上下文中,如果它们确实是这样的上下文中。所以对于a :: f (t -&gt; r)b :: f t(几乎是普通的) 组合a &lt;*&gt; b承载内容的应用(或类型t -&gt; rt) , 在给定的上下文中f 类型)。

与单子的主要区别在于,单子是非线性的。在

do {  x        <-  a
   ;     y     <-  b x
   ;        z  <-  c x y
   ;               return 
     (x, y, z) }

计算 b x 取决于 x,而 c x y 取决于 xy。函数是嵌套的

a >>= (\x ->  b x  >>= (\y ->  c x y  >>= (\z ->  .... )))

如果bc 依赖于先前的结果(xy),则可以通过使计算阶段返回重新打包的复合数据(这解决了您的第二个问题):

a  >>= (\x       ->  b  >>= (\y-> return (x,y)))       -- `b  ` sic
   >>= (\(x,y)   ->  c  >>= (\z-> return (x,y,z)))     -- `c  `
   >>= (\(x,y,z) ->  ..... )

而这本质上是一种应用风格(bc 是完全已知的,独立于a 产生的值x 等)。因此,当您的组合创建包含进一步组合所需的所有信息的数据时,并且不需要“外部变量”(即所有计算都是完全已知的,独立于任何前面的阶段),你可以使用这种风格的组合。

但如果你的单子链有分支依赖于这些“外部”变量的值(即单子计算的前一阶段的结果),那么你就不能用它制作一个线性链。那么它本质上是一元的。


作为说明,该论文中的第一个示例显示了“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) }

实际上可以用这种“扁平、线性”的方式编码为

sequence :: (Applicative f) => [f a] -> f [a]
sequence []       = pure []
sequence (c : cs) = pure (:) <*> c <*> sequence cs
                  --     (:)     x     xs

这里没有用 monad 对先前结果进行分支的能力。


关于出色的Petr Pudlák's answer 的注释:在我的“术语”中,他的pair组合,没有应用程序。它表明 Applictive Functor 添加到普通 Functor 的本质是组合的能力。然后通过好旧的fmap实现应用程序。这表明 combinatory functors 可能是一个更好的名称(update: 事实上,“Monoidal Functors”就是这个名称)。

【讨论】:

  • pure x 产生一个没有副作用的动作并返回x,即结果是一个“纯动作”。
  • @melpomene 动作不纯,很有效。
  • 那它的作用是什么?
  • 不,它不是这样工作的。效果不在类型中;它们在(某些)值中,并非所有值都有影响。 Monad/Applicative 法则确保了这一点(例如 return a &gt;&gt;= k == k apure f &lt;*&gt; pure x = pure (f x))。
  • @melpomene 我刚刚遇到this related answer by Conor。我想我并不反对。
【解决方案3】:

您可以这样查看函子、应用程序和单子:它们都带有一种“效果”和“值”。 (请注意,术语“效果”和“价值”只是近似值——实际上并不需要任何副作用或价值——就像在 IdentityConst 中一样。)

  • 使用Functor,您可以使用fmap 修改内部可能的值,但您不能对内部的效果进行任何操作。
  • 使用Applicative,您可以使用pure 创建一个没有任何效果的值,并且您可以对效果进行排序并在其中组合它们的值。但是效果和值是分开的:当对效果排序时,一个效果不能依赖于前一个效果的值。这反映在&lt;*&lt;*&gt;*&gt;:它们对效果进行排序并组合它们的值,但您无法以任何方式检查其中的值。

    您可以使用这组替代函数定义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)
    

    (其中pureUnit 没有任何效果) 并从中定义pure&lt;*&gt;(反之亦然)。这里pair 对两个效果进行排序并记住它们的值。这个定义表达了Applicative是一个monoidal functor的事实。

    现在考虑一个由pairfmappureUnit 和一些原始应用值组成的任意(有限)表达式。我们有几个可以使用的规则:

    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)
    

    利用这些规则,我们可以将pairs重新排序,将fmaps向外推,并消除pureUnits,这样最终这样的表达式就可以转换成

    fmap pureFunction (x1 `pair` x2 `pair` ... `pair` xn)
    

    fmap pureFunction pureUnit
    

    确实,我们可以首先使用pair 将所有效果收集在一起,然后使用纯函数修改内部的结果值。

  • 使用Monad,效果可以依赖于先前的一元值的值。这使它们如此强大。

【讨论】:

  • 我仍然认为你 接受 pure 的纯粹价值观,并创造“携带”它们的有效事物。 pure 1 :: ZipList Int 不是纯值;这是一个由纯1 制成的ZipList。我想。同样,在您的 fmap pureFunction pureUnit 中,函数是纯函数,但 pureUnit 不是。
  • 对上述评论的更正:在您的fmap pureFunction pureUnit 中相当于pure pureFunction &lt;*&gt; pureUnit 它是pureFunction 是一个纯值; pure pureFunction 不是。
  • @WillNess 你说得对,我选的名字有点坏。 pureUnit 并不是真正的纯粹,它是一个没有效果的应用值。
【解决方案4】:

已经给出的答案非常好,但有一点我想明确说明,它与&lt;*&lt;$*&gt; 有关。

其中一个例子是

do var1 <- parser1
   var2 <- parser2
   var3 <- parser3
   return $ foo var1 var2 var3

也可以写成foo &lt;$&gt; parser1 &lt;*&gt; parser2 &lt;*&gt; parser3

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

do var1 <- parser1
   parser2
   var3 <- parser3
   return $ foo var1 var3

如果您只想使用 &lt;$&gt;&lt;*&gt; 编写此代码,则它应该类似于以下等效表达式之一:

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

但要正确处理更多参数有点棘手!

不过,你也可以写foo &lt;$&gt; parser1 &lt;* parser2 &lt;*&gt; parser3。您可以调用foo 语义函数,该函数由parser1parser3 的结果提供,同时忽略parser2 之间的结果。 &gt; 的缺失表示忽略。

如果您想忽略parser1 的结果但使用其他两个结果,您可以类似地写foo &lt;$ parser1 &lt;*&gt; parser2 &lt;*&gt; parser3,使用&lt;$ 而不是&lt;$&gt;

我从来没有发现*&gt; 有多大用处,我通常会为忽略p1 的结果并仅使用p2 解析的解析器编写id &lt;$ p1 &lt;*&gt; p2;您可以将其写为p1 *&gt; p2,但这会增加代码读者的认知负担。

我已经为解析器学习了这种思维方式,但后来被推广到Applicatives;但我认为这个符号来自the uuparsing library;至少我 10 多年前在 Utrecht 使用过它。

【讨论】:

【解决方案5】:

我想在非常有用的现有答案中添加/改写几件事:

应用程序是“静态的”。在pure f &lt;*&gt; a &lt;*&gt; b 中,b 不依赖于a,因此可以是analyzed statically。这就是我试图在my answer to your previous question 中展示的内容(但我想我失败了——抱歉)——因为实际上没有解析器的顺序依赖,所以不需要 monad。

monad 带来的主要区别是(&gt;&gt;=) :: Monad m =&gt; m a -&gt; (a -&gt; m b) -&gt; m a,或者join :: Monad m =&gt; m (m a)。请注意,每当您在 do 符号中包含 x &lt;- y 时,您使用的是 &gt;&gt;=。这些说单子允许您使用单子“内部”的值来“动态地”生成新的单子。这不能通过 Applicative 完成。例子:

-- parse two in a row of the same character
char             >>= \c1 ->
char             >>= \c2 ->
guard (c1 == c2) >>
return c1

-- parse a digit followed by a number of chars equal to that digit
--   assuming: 1) `digit`s value is an Int,
--             2) there's a `manyN` combinator
-- examples:  "3abcdef"  -> Just {rest: "def", chars: "abc"}
--            "14abcdef" -> Nothing
digit        >>= \d -> 
manyN d char 
-- note how the value from the first parser is pumped into 
--   creating the second parser

-- creating 'half' of a cartesian product
[1 .. 10] >>= \x ->
[1 .. x]  >>= \y ->
return (x, y)

最后,Applicatives 启用了 @WillNess 提到的提升功能应用程序。 要尝试了解“中间”结果是什么样的,您可以查看普通函数应用程序和提升函数应用程序之间的相似之处。假设add2 = (+) :: Int -&gt; Int -&gt; Int

-- normal function application
add2 :: Int -> Int -> Int
add2 3 :: Int -> Int
(add2 3) 4 :: Int

-- lifted function application
pure add2 :: [] (Int -> Int -> Int)
pure add2 <*> pure 3 :: [] (Int -> Int)
pure add2 <*> pure 3 <*> pure 4 :: [] Int

-- more useful example
[(+1), (*2)]
[(+1), (*2)] <*> [1 .. 5]
[(+1), (*2)] <*> [1 .. 5] <*> [3 .. 8]

不幸的是,您无法有意义地打印pure add2 &lt;*&gt; pure 3 的结果,原因与add2 无法打印的原因相同……令人沮丧。您可能还想查看 Identity 及其类型类实例以了解 Applicatives。

【讨论】:

  • 是的,这是 applicative 和 monad 之间的关键区别。 Monad 允许您运行任意图灵完备的代码来决定下一个解析器 [或其他] 应该是什么,从而破坏任何静态分析的可能性。 Applicative 通过更具限制性,允许您分析并可能优化整个解析器。就像我说的,“少即是多”。这实际上就是为什么我首先对应用程序感兴趣......
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2014-11-01
  • 2015-01-02
  • 2010-09-26
  • 1970-01-01
  • 1970-01-01
  • 2014-11-27
  • 1970-01-01
相关资源
最近更新 更多