繁体   English   中英

应用仿函数更有趣

[英]More fun with applicative functors

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

总结我到目前为止的知识,一个应用函子比一个monad更受限制。 在“少即是多”的传统中,限制代码可以做什么会增加疯狂代码操作的可能性。 无论如何,很多人似乎相信使用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 ,基本上是<$> 即,给定从xy的函数,我们可以将fx映射到fy Applicative添加了两个基本上新的元素。 一个是pure ,它与return类型大致相同(以及各种类别理论类中的其他几个运算符)。 另一个是<*> ,它使我们能够获取一个功能容器和一个输入容器,并产生一个输出容器。

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

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

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


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

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

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

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

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

<**>函数非常简单:它们的工作方式与>>相同。 <*工作方式与<<<<不存在的方式相同。 基本上,给出a *> b ,你先“做” a ,那么你“做” b ,返回的结果b 对于a <* b ,你还是第一个“做” a则“做” b ,但是你返回的结果a (当然,对于“do”的适当含义。)

<$函数只是fmap const 所以a <$ b等于fmap (const a) b 你只需丢弃“动作”的结果并返回一个常量值。 Control.Monad函数void ,其类型为Functor f => fa -> f () ,可写为() <$

这三个函数对于applicative functor的定义并不重要。 <$ ,实际上适用于任何仿函数。)这对于monads来说就像>>一样。 我相信他们在课堂上可以更轻松地针对特定情况对其进行优化。

当您使用applicative functor时,您不会从仿函数中“提取”该值。 在monad中,这就是>>= do, foo <- ... desugars to what。 而是使用<$><*>直接将包装的值传递给函数。 所以你可以将你的例子重写为:

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 <$> a <*> b <*> c基本上类似于提升函数应用程序。 您可以将<*>想象为替换空间(例如函数应用程序),就像fmap替代函数应用程序一样。 这也应该给你的,为什么我们使用一个直观的概念<$> --IT的喜欢的提升版本$

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

pure是异乎寻常的命名。 通常函数的命名参照他们生产什么,但在pure xx纯粹的 pure x产生一个“运载”纯x的应用函子。 “携带”当然是近似的。 一个例子: pure 1 :: ZipList Int是一个ZipList ,带有一个纯Int值, 1

<*>*><* 不是函数,而是方法 (这是您首先关注的问题)。 f在它们的类型中不是通用的(就像它对于函数一样),而是具体的,由特定实例指定。 这就是为什么它们确实不仅仅是$flip constconst 专用类型f指定组合的语义 在通常的应用风格编程中,组合意味着应用。 但是对于仿函数,存在另外的维度,由“载体”类型f fx ,有一个“内容”, x ,但也有一个“上下文”, f

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

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

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

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

计算bx取决于xcxy取决于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预先充分已知的,独立于所述值的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) }

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

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

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


过硬的音符切赫Pudlák的回答 :在我的“术语”在这里,他pair是没有应用 组合 它表明,Applictive Functors为简单Functor添加的内容的本质是结合的能力。 然后通过好的旧fmap实现应用程序。 这表明组合仿函数可能是一个更好的名称( 更新:事实上,“Monoidal Functors”就是这个名字)。

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

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

    您可以使用此替代功能集定义Applicative

     fmap :: (a -> b) -> (fa -> fb) pureUnit :: f () pair :: fa -> fb -> f (a, b) -- or even with a more suggestive type (fa, fb) -> f (a, b) 

    (其中pureUnit不带任何效果)并从中定义pure<*> (反之亦然)。 这里pair两个效果进行排序,并记住它们的两个值。 该定义表达了Applicative是一个半体仿函数的事实。

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

     fmap f . fmap g ==> fmap (f . g) pair (fmap fx) y ==> fmap (\\(a,b) -> (fa, b)) (pair xy) pair x (fmap fy) ==> -- similar pair pureUnit y ==> fmap (\\b -> ((), b)) y pair x pureUnit ==> -- similar pair (pair xy) z ==> pair x (pair yz) 

    使用这些规则,我们可以重新排序pair ,向外推fmap并消除pureUnit s,所以最终这样的表达式可以转换成

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

    要么

     fmap pureFunction pureUnit 

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

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

已经给出的答案非常好,但是我想明确说明一个小的(ish)点,它与<*<$*>

其中一个例子是

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

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

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

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

如果您只想使用<$><*>来编写它,它应该类似于以下等效表达式之一:

(\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 <$> parser1 <* parser2 <*> parser3 您可以调用foo语义函数,该语义函数提供parser1parser3的结果,同时忽略parser2的结果。 缺少>意味着忽略。

如果你想忽略parser1的结果但是使用另外两个结果,你可以类似地使用<$而不是<$>来编写foo <$ parser1 <*> parser2 <*> parser3

我从来没有发现很多用于*> ,我通常会为忽略p1结果的解析器写入id <$ p1 <*> p2 ,只是用p2解析; 您可以将其写为p1 *> p2但这会增加代码读者的认知负担。

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

我想在一些非常有用的现有答案中添加/改写一些内容:

申请人是“静态的”。 pure f <*> a <*> bb不依赖于a ,因此可以静态分析 这是我试图在我对你之前的问题的回答中显示的内容(但我想我失败了 - 抱歉) - 因为实际上没有解析器的顺序依赖,所以不需要monad。

monad带来的关键区别是(>>=) :: Monad m => ma -> (a -> mb) -> ma ,或者, join :: Monad m => m (ma) 需要注意的是,只要你有x <- ydo记号,你使用>>= 这些说monad允许你使用monad里面的值来“动态地”产生一个新的monad。 申请人无法做到这一点。 例子:

-- 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 -> Int -> 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 <*> pure 3的结果,原因与你不能为add2 ...令人沮丧的原因相同。 您可能还需要查看Identity及其类型类实例以获取Applicatives的句柄。

暂无
暂无

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

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