简体   繁体   English

应用仿函数更有趣

[英]More fun with applicative functors

Earlier I asked about translating monadic code to use only the applicative functor instance of Parsec. 之前我曾询问过将monadic代码翻译为仅使用Parsec的applicative functor实例。 Unfortunately I got several replies which answered the question I literally asked, but didn't really give me much insight. 不幸的是,我得到了几个回复,回答了我真正问过的问题,但并没有给我太多的了解。 So let me try this again... 那么让我再试一次......

Summarising my knowledge so far, an applicative functor is something which is somewhat more restricted than a monad. 总结我到目前为止的知识,一个应用函子比一个monad更受限制。 In the tradition of "less is more", restricting what the code can do increases the possibilities for crazy code manipulation. 在“少即是多”的传统中,限制代码可以做什么会增加疯狂代码操作的可能性。 Regardless, a lot of people seem to believe that using applicative instead of monad is a superior solution where it's possible. 无论如何,很多人似乎相信使用applicative而不是monad是一种可行的优越解决方案。

The Applicative class is defined in Control.Applicative , whose Haddock's listing helpfully separates the class methods and utility functions with a vast swathe of class instances between them, to make it hard to quickly see everything on screen at once. Applicative类在Control.Applicative定义,其Haddock的列表有助于将类方法和实用程序函数与它们之间的大量类实例分开,从而难以立即快速查看屏幕上的所有内容。 But the pertinent type signatures are 但相关的类型签名是

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

Makes perfect sense, right? 做得很完美,对吧?

Well, Functor already gives us fmap , which is basically <$> . 好吧, Functor已经给了我们fmap ,基本上是<$> Ie, given a function from x to y , we can map an fx to an fy . 即,给定从xy的函数,我们可以将fx映射到fy Applicative adds two essentially new elements. Applicative添加了两个基本上新的元素。 One is pure , which has roughly the same type as return (and several other operators in various category theory classes). 一个是pure ,它与return类型大致相同(以及各种类别理论类中的其他几个运算符)。 The other is <*> , which gives us the ability to take a container of functions and a container of inputs and produce a container of outputs. 另一个是<*> ,它使我们能够获取一个功能容器和一个输入容器,并产生一个输出容器。

Using the operators above, we can very neatly do something such as 使用上面的运算符,我们可以非常巧妙地做一些事情

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

This allows us to take an N-ary function and source its arguments from N functors in a way which generalises easily to any N. 这允许我们采用N-ary函数并从N个函子中以一种易于推广到任何N的方式来源它的参数。


This much I already understand. 这个我已经明白了。 There are two main things which I do not yet understand. 我还有两件不太明白的事情。

First, the functions *> , <* and <$ . 首先,函数*><*<$ From their types, <* = const , *> = flip const , and <$ could be something similar. 从它们的类型来看, <* = const*> = flip const<$可能是类似的东西。 Presumably this does not describe what these functions actually do though. 据推测,这并没有描述这些功能实际上做了什么。 (??!) (?!)

Second, when writing a Parsec parser, each parsable entity usually ends up looking something like this: 其次,在编写Parsec解析器时,每个可解析的实体通常最终看起来像这样:

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

Since an applicative functor does not allow us to bind intermediate results to variables in this way, I'm puzzled as to how to gather them up for the final stage. 由于应用程序仿函数不允许我们以这种方式将中间结果绑定到变量,所以我很困惑如何在最后阶段收集它们。 I haven't been able to wrap my mind around the idea fully enough in order to comprehend how to do this. 我无法完全理解这个想法,以便理解如何做到这一点。

The <* and *> functions are very simple: they work the same way as >> . <**>函数非常简单:它们的工作方式与>>相同。 The <* would work the same way as << except << does not exist. <*工作方式与<<<<不存在的方式相同。 Basically, given a *> b , you first "do" a , then you "do" b and return the result of b . 基本上,给出a *> b ,你先“做” a ,那么你“做” b ,返回的结果b For a <* b , you still first "do" a then "do" b , but you return the result of a . 对于a <* b ,你还是第一个“做” a则“做” b ,但是你返回的结果a (For appropriate meanings of "do", of course.) (当然,对于“do”的适当含义。)

The <$ function is just fmap const . <$函数只是fmap const So a <$ b is equal to fmap (const a) b . 所以a <$ b等于fmap (const a) b You just throw away the result of an "action" and return a constant value instead. 你只需丢弃“动作”的结果并返回一个常量值。 The Control.Monad function void , which has a type Functor f => fa -> f () could be written as () <$ . Control.Monad函数void ,其类型为Functor f => fa -> f () ,可写为() <$

These three functions are not fundamental to the definition of an applicative functor. 这三个函数对于applicative functor的定义并不重要。 ( <$ , in fact, works for any functor.) This, again, is just like >> for monads. <$ ,实际上适用于任何仿函数。)这对于monads来说就像>>一样。 I believe they're in the class to make it easier to optimize them for specific instances. 我相信他们在课堂上可以更轻松地针对特定情况对其进行优化。

When you use applicative functors, you do not "extract" the value from the functor. 当您使用applicative functor时,您不会从仿函数中“提取”该值。 In a monad, this is what >>= does, and what foo <- ... desugars to. 在monad中,这就是>>= do, foo <- ... desugars to what。 Instead, you pass the wrapped values into a function directly using <$> and <*> . 而是使用<$><*>直接将包装的值传递给函数。 So you could rewrite your example as: 所以你可以将你的例子重写为:

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

If you want intermediate variables, you could just use a let statement: 如果你想要中间变量,你可以使用let语句:

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

As you correctly surmised, pure is just another name for return . 正如你所推测的那样, pure只是return另一个名称。 So, to make the shared structure more obvious, we can rewrite this as: 因此,为了使共享结构更加明显,我们可以将其重写为:

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

I hope this clarifies things. 我希望这能澄清事情。

Now just a little note. 现在只是一点点说明。 People do recommend using applicative functor functions for parsing. 人们建议使用适用函子功能解析。 However, you should only use them if they make more sense! 但是,如果它们更有意义,你应该只使用它们! For sufficiently complex things, the monad version (especially with do-notation) can actually be clearer. 对于足够复杂的东西,monad版本(尤其是带有do-notation)实际上可以更清晰。 The reason people recommend this is that 人们推荐这个的原因是

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

is both shorter and more readable than 比起来更简短,更易读

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

Essentially, the f <$> a <*> b <*> c is essentially like lifted function application. 本质上, f <$> a <*> b <*> c基本上类似于提升函数应用程序。 You can imagine the <*> being a replacement for a space (eg function application) in the same way that fmap is a replacement for function application. 您可以将<*>想象为替换空间(例如函数应用程序),就像fmap替代函数应用程序一样。 This should also give you an intuitive notion of why we use <$> --it's like a lifted version of $ . 这也应该给你的,为什么我们使用一个直观的概念<$> --IT的喜欢的提升版本$

I can make a few remarks here, hopefully helpful. 我可以在这里发表一些评论,希望对你有所帮助。 This reflects my understanding which itself might be wrong. 这反映了我的理解本身可能是错误的。

pure is unusually named. pure是异乎寻常的命名。 Usually functions are named referring to what they produce, but in pure x it is x that is pure . 通常函数的命名参照他们生产什么,但在pure xx纯粹的 pure x produces an applicative functor which "carries" the pure x . pure x产生一个“运载”纯x的应用函子。 "Carries" of course is approximate. “携带”当然是近似的。 An example: pure 1 :: ZipList Int is a ZipList , carrying a pure Int value, 1 . 一个例子: pure 1 :: ZipList Int是一个ZipList ,带有一个纯Int值, 1

<*> , *> , and <* are not functions, but methods (this answers your first concern). <*>*><* 不是函数,而是方法 (这是您首先关注的问题)。 f in their types is not general (like it would be, for functions) but specific, as specified by a specific instance. f在它们的类型中不是通用的(就像它对于函数一样),而是具体的,由特定实例指定。 That's why they are indeed not just $ , flip const and const . 这就是为什么它们确实不仅仅是$flip constconst The specialized type f specifies the semantics of combination . 专用类型f指定组合的语义 In the usual applicative style programming, combination means application. 在通常的应用风格编程中,组合意味着应用。 But with functors, an additional dimension is present, represented by the "carrier" type f . 但是对于仿函数,存在另外的维度,由“载体”类型f In fx , there is a "contents", x , but there is also a "context", f . fx ,有一个“内容”, x ,但也有一个“上下文”, f

The "applicative functors" style sought to enable the "applicative style" programming, with effects. “applicative functors”风格试图通过效果实现“应用风格”编程。 Effects being represented by functors, carriers, providers of context; 由仿函数,载体,背景提供者代表的效果; "applicative" referring to the normal applicative style of functional application. “应用”指的是功能应用的正常应用方式。 Writing just fx to denote application was once a revolutionary idea . fx来表示应用程序曾经是一个革命性的想法 There was no need for additional syntax anymore, no (funcall fx) , no CALL statements, none of this extra stuff - combination was application ... Not so, with effects, seemingly - there was again that need for the special syntax, when programming with effects. 不再需要额外的语法,没有(funcall fx) ,没有CALL语句,没有这些额外的东西 - 组合应用程序 ......不是这样,有效果, 似乎 - 再次需要特殊语法,当用效果编程。 The slain beast reappeared again. 被杀的野兽再次出现了。

So came the Applicative Programming with Effects to again make the combination mean just application - in the special (perhaps effectful) context , if they were indeed in such context. 因此, 应用程序编程与效果再次使组合意味着应用 - 在特殊(可能有效)的上下文中 ,如果它们确实这样的上下文中。 So for a :: f (t -> r) and b :: ft , the (almost plain) combination a <*> b is an application of carried contents (or types t -> r and t ), in a given context (of type f ). 因此,对于a :: f (t -> r)b :: ft(几乎是普通的)组合a <*> b在给定的上下文中 的承载内容 (或类型t -> rt )的应用( f型)。

The main distinction from monads is, monads are non-linear . 与monad的主要区别在于, monad是非线性的 In

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

the computation bx depends on x , and cxy depends on both x and y . 计算bx取决于xcxy取决于xy The functions are nested : 这些函数是嵌套的

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

If b and c do not depend on the previous results ( x , y ), this can be made flat by making the computation stages return repackaged, compound data (this addresses your second concern): 如果bc 依赖于先前的结果( xy ),则可以通过使计算阶段返回重新打包的复合数据 (这解决您的第二个问题)来使其变

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

and this is essentially an applicative style ( b , c are fully known in advance, independent of the value x produced by a , etc.). 这实质上是一种应用性样式( bc预先充分已知的,独立于所述值的x通过产生a ,等等)。 So when your combinations create data that encompass all the information they need for further combinations, and there's no need for "outer variables" (ie all computations are already fully known, independent of any values produced by any of the previous stages), you can use this style of combination. 因此,当您的组合创建包含进一步组合所需的所有信息的数据时, 并且不需要“外部变量” (即所有计算已经完全已知,独立于任何前一阶段产生的任何值),您可以使用这种风格的组合。

But if your monadic chain has branches dependent on values of such "outer" variables (ie results of previous stages of monadic computation), then you can't make a linear chain out of it. 但是如果你的monadic链的分支依赖于这种“外部”变量的值(即monadic计算的前一阶段的结果),那么你就不能用它来形成一个线性链。 It is essentially monadic then. 基本上是 monadic。


As an illustration, the first example from that paper shows how the "monadic" function 作为一个例子,该论文的第一个例子显示了“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) }

can actually be coded in this "flat, linear" style as 实际上可以用这种“扁平,线性”的方式编码

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

There's no use here for the monad's ability to branch on previous results. 这里没有使用monad能够分支以前的结果。


a note on the excellent Petr Pudlák's answer : in my "terminology" here, his pair is combination without application . 过硬的音符切赫Pudlák的回答 :在我的“术语”在这里,他pair是没有应用 组合 It shows that the essence of what the Applictive Functors add to plain Functors, is the ability to combine. 它表明,Applictive Functors为简单Functor添加的内容的本质是结合的能力。 Application is then achieved by the good old fmap . 然后通过好的旧fmap实现应用程序。 This suggests combinatory functors as perhaps a better name ( update: in fact, "Monoidal Functors" is the name). 这表明组合仿函数可能是一个更好的名称( 更新:事实上,“Monoidal Functors”就是这个名字)。

You can view functors, applicatives and monads like this: They all carry a kind of "effect" and a "value". 你可以像这样查看仿函数,应用程序和monad:它们都带有一种“效果”和“价值”。 (Note that the terms "effect" and "value" are only approximations - there doesn't actually need to be any side effects or values - like in Identity or Const .) (请注意,术语“效果”和“值”只是近似值 - 实际上并不需要任何副作用或值 - 例如IdentityConst 。)

  • With Functor you can modify possible values inside using fmap , but you cannot do anything with effects inside. 使用Functor您可以使用fmap修改内部的可能值,但是您无法对内部的效果执行任何操作。
  • With Applicative , you can create a value without any effect with pure , and you can sequence effects and combine their values inside. 使用Applicative ,您可以创建一个没有任何pure效果的值,您可以对效果进行排序并将其值组合在一起。 But the effects and values are separate: When sequencing effects, an effect cannot depend on the value of a previous one. 但效果和值是分开的:在排序效果时,效果不能取决于前一个效果的值。 This is reflected in <* , <*> and *> : They sequence effects and combine their values, but you cannot examine the values inside in any way. 这反映在<*<*>*> :它们对效果进行排序并组合它们的值,但您无法以任何方式检查内部的值。

    You could define Applicative using this alternative set of functions: 您可以使用此替代功能集定义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) 

    (where pureUnit doesn't carry any effect) and define pure and <*> from them (and vice versa). (其中pureUnit不带任何效果)并从中定义pure<*> (反之亦然)。 Here pair sequences two effects and remembers the values of both of them. 这里pair两个效果进行排序,并记住它们的两个值。 This definition expresses the fact that Applicative is a monoidal functor . 该定义表达了Applicative是一个半体仿函数的事实。

    Now consider an arbitrary (finite) expression consisting of pair , fmap , pureUnit and some primitive applicative values. 现在考虑一个由pairfmappureUnit和一些原始应用值组成的任意(有限)表达式。 We have several rules we can use: 我们有几个可以使用的规则:

     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) 

    Using these rules, we can reorder pair s, push fmap s outwards and eliminate pureUnit s, so eventually such expression can be converted into 使用这些规则,我们可以重新排序pair ,向外推fmap并消除pureUnit s,所以最终这样的表达式可以转换成

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

    or 要么

     fmap pureFunction pureUnit 

    So indeed, we can first collect all effects together using pair and then modify the resulting value inside using a pure function. 确实,我们可以先使用pair收集所有效果,然后使用纯函数修改结果值。

  • With Monad , an effect can depend on the value of a previous monadic value. 对于Monad ,效果可以取决于先前monadic值的值。 This makes them so powerful. 这使他们如此强大。

The answers already given are excellent, but there's one small(ish) point I'd like to spell out explicitly, and it has to do with <* , <$ and *> . 已经给出的答案非常好,但是我想明确说明一个小的(ish)点,它与<*<$*>

One of the examples was 其中一个例子是

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

which can also be written as foo <$> parser1 <*> parser2 <*> parser3 . 也可以写成foo <$> parser1 <*> parser2 <*> parser3

Suppose that the value of var2 is irrelevant for foo - eg it's just some separating whitespace. 假设var2的值与foo无关 - 例如,它只是一些分隔空格。 Then it also doesn't make sense to have foo accept this whitespace only to ignore it. 然后让foo接受这个空格只是为了忽略它也没有意义。 In this case foo should have two parameters, not three. 在这种情况下, foo应该有两个参数,而不是三个。 Using do -notation, you can write this as: 使用do -notation,您可以将其写为:

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

If you wanted to write this using only <$> and <*> it should be something like one of these equivalent expressions: 如果您只想使用<$><*>来编写它,它应该类似于以下等效表达式之一:

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

But that's kind of tricky to get right with more arguments! 但是,通过更多的论点来解决这个问题真是太棘手了!

However, you can also write foo <$> parser1 <* parser2 <*> parser3 . 但是,您也可以编写foo <$> parser1 <* parser2 <*> parser3 You could call foo the semantic function which is fed the result of parser1 and parser3 while ignoring the result of parser2 in between. 您可以调用foo语义函数,该语义函数提供parser1parser3的结果,同时忽略parser2的结果。 The absence of > is meant to be indicative of the ignoring. 缺少>意味着忽略。

If you wanted to ignore the result of parser1 but use the other two results, you can similarly write foo <$ parser1 <*> parser2 <*> parser3 , using <$ instead of <$> . 如果你想忽略parser1的结果但是使用另外两个结果,你可以类似地使用<$而不是<$>来编写foo <$ parser1 <*> parser2 <*> parser3

I've never found much use for *> , I would normally write id <$ p1 <*> p2 for the parser that ignores the result of p1 and just parses with p2 ; 我从来没有发现很多用于*> ,我通常会为忽略p1结果的解析器写入id <$ p1 <*> p2 ,只是用p2解析; you could write this as p1 *> p2 but that increases the cognitive load for readers of the code. 您可以将其写为p1 *> p2但这会增加代码读者的认知负担。

I've learnt this way of thinking just for parsers, but it has later been generalised to Applicative s; 我已经为解析器学习了这种思维方式,但后来又将其推广到了Applicative ; but I think this notation comes from the uuparsing library ; 但我认为这个符号来自于uuparsing库 ; at least I used it at Utrecht 10+ years ago. 至少我在10多年前在乌得勒支使用它。

I'd like to add/reword a couple things to the very helpful existing answers: 我想在一些非常有用的现有答案中添加/改写一些内容:

Applicatives are "static". 申请人是“静态的”。 In pure f <*> a <*> b , b does not depend on a , and so can be analyzed statically . pure f <*> a <*> bb不依赖于a ,因此可以静态分析 This is what I was trying to show in my answer to your previous question (but I guess I failed -- sorry) -- that since there was actually no sequential dependence of parsers, there was no need for monads. 这是我试图在我对你之前的问题的回答中显示的内容(但我想我失败了 - 抱歉) - 因为实际上没有解析器的顺序依赖,所以不需要monad。

The key difference that monads bring to the table is (>>=) :: Monad m => ma -> (a -> mb) -> ma , or, alternatively, join :: Monad m => m (ma) . monad带来的关键区别是(>>=) :: Monad m => ma -> (a -> mb) -> ma ,或者, join :: Monad m => m (ma) Note that whenever you have x <- y inside do notation, you're using >>= . 需要注意的是,只要你有x <- ydo记号,你使用>>= These say that monads allow you to use a value "inside" a monad to produce a new monad, "dynamically". 这些说monad允许你使用monad里面的值来“动态地”产生一个新的monad。 This cannot be done with an Applicative. 申请人无法做到这一点。 Examples: 例子:

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

Lastly, Applicatives enable lifted function application as mentioned by @WillNess. 最后,Applicatives启用了@WillNess提到的提升功能应用程序。 To try to get an idea of what the "intermediate" results look like, you can look at the parallels between normal and lifted function application. 为了试图了解“中间”结果的样子,您可以查看正常和提升函数应用程序之间的相似之处。 Assuming add2 = (+) :: Int -> Int -> Int : 假设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]

Unfortunately, you can't meaningfully print the result of pure add2 <*> pure 3 for the same reason that you can't for add2 ... frustrating. 不幸的是,你不能有意义地打印pure add2 <*> pure 3的结果,原因与你不能为add2 ...令人沮丧的原因相同。 You may also want to look at the Identity and its typeclass instances to get a handle on Applicatives. 您可能还需要查看Identity及其类型类实例以获取Applicatives的句柄。

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

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