应用仿函数更有趣

时间:2013-03-04 19:29:44

标签: haskell monads applicative

Earlier我问过翻译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的函数,我们可以将f x映射到f yApplicative添加了两个基本上新的元素。一个是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...

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

5 个答案:

答案 0 :(得分:26)

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

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

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

当您使用applicative functors时,您不会从仿函数中“提取”该值。在monad中,这就是>>=所做的事情,以及foo <- ...所遇到的事情。相反,您可以使用<$><*>直接将包装的值传递给函数。所以你可以将你的例子重写为:

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

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

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

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

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

我希望这能澄清事情。

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

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

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

基本上,f <$> a <*> b <*> c基本上就像提升功能应用程序一样。您可以将<*>想象为空间(例如函数应用程序)的替换,其方式与fmap替换函数应用程序的方式相同。这也应该让您直观地了解我们使用<$>的原因 - 它就像$的升级版本。

答案 1 :(得分:11)

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

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

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

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

所以来了Applicative Programming with Effects再次使组合意味着应用 - 在特殊的(可能有效的)上下文中,如果它们确实这样的上下文中。因此,对于a :: f (t -> r)b :: f t(几乎是普通的)组合a <*> b承载内容的应用(或类型{{ 1}}和t -> r),在给定的上下文中(类型为t)。

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

f

计算do { x <- a ; y <- b x ; z <- c x y ; return (x, y, z) } 取决于b xx取决于c x yx。这些函数是嵌套

y

如果a >>= (\x -> b x >>= (\y -> c x y >>= (\z -> .... ))) b 不依赖于之前的结果(cx),则可以通过使计算阶段返回重新打包,复合数据(这解决了您的第二个问题)来平面

y

这实际上是一种应用风格(a >>= (\x -> b >>= (\y-> return (x,y))) -- `b ` sic >>= (\(x,y) -> c >>= (\z-> return (x,y,z))) -- `c ` >>= (\(x,y,z) -> ..... ) b提前完全知道,与c等产生的价值x无关。因此,当您的组合创建包含进一步组合所需的所有信息的数据时,并且不需要“外部变量”(即所有计算都已完全已知,独立于任何前几个阶段),你可以使用这种风格的组合。

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


作为一个例子,该论文的第一个例子显示了“monadic”函数

a

实际上可以用“扁平,线性”样式编码为

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

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


关于优秀Petr Pudlák's answer的说明:在我的“术语”中,他的sequence :: (Applicative f) => [f a] -> f [a] sequence [] = pure [] sequence (c : cs) = pure (:) <*> c <*> sequence cs -- (:) x xs 组合,没有应用。它表明,Applictive Functors为简单Functor添加的内容的本质是结合的能力。然后通过好的旧pair来实现应用。这表明组合仿函数可能是一个更好的名称(更新:实际上,“Monoidal Functors”就是这个名字)。

答案 2 :(得分:8)

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

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

    现在考虑一个由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)
    

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

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

    fmap pureFunction pureUnit
    

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

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

答案 3 :(得分:6)

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

其中一个例子是

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

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

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

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代替<$来编写<$>

我从未发现*>有太多用处,我通常会为解析器编写id <$ p1 <*> p2,忽略p1的结果并只用p2解析;你可以把它写成p1 *> p2,但这会增加代码读者的认知负担。

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

答案 4 :(得分:3)

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

申请人是“静态的”。在pure f <*> a <*> b中,b不依赖于a,因此可以analyzed statically。这就是我试图在my answer to your previous question中展示的内容(但我想我失败了 - 抱歉) - 由于解析器实际上没有顺序依赖,所以不需要monad。

monad带来的主要区别是(>>=) :: Monad m => m a -> (a -> m b) -> m a,或者join :: Monad m => m (m a)。请注意,只要您在x <- y符号内do,就会使用>>=。这些说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的句柄。