首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >应用函子更有趣

应用函子更有趣
EN

Stack Overflow用户
提问于 2013-03-04 19:29:43
回答 5查看 2.6K关注 0票数 19

早些时候,我问过关于将一元代码翻译成只使用Parsec的应用程序实例的问题。不幸的是,我得到了几个答复,回答了我字面上提出的问题,但并没有给我太多的洞察力。让我再试一次..。

概括我迄今为止的知识,应用函子比单子更受限制。在“越少越多”的传统中,限制代码可以做的事情增加了疯狂的代码操作的可能性。不管怎么说,很多人似乎相信,在可能的情况下,使用应用程序而不是单播是一种更好的解决方案。

Applicative类是在Control.Applicative中定义的,它的清单将类方法和实用程序函数与它们之间的大量类实例分离开来,使得很难立即看到屏幕上的所有内容。但是相关的类型签名是

代码语言:javascript
复制
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大致相同(以及不同类别理论类中的其他几个运算符)。另一个是<*>,它使我们能够接受一个函数容器和一个输入容器,并生成一个输出容器。

使用上面的操作符,我们可以非常整洁地做一些事情,例如

代码语言:javascript
复制
foo <$> abc <*> def <*> ghi

这使得我们能够接受一个N元函数,并以一种很容易推广到任意N的方式从N函子中获取它的论点。

这点我已经明白了。有两件事我还不明白。

首先是函数*><*<$。从它们的类型来看,<* = const*> = flip const<$可能是类似的。不过,这大概并没有描述这些函数的实际作用。(??!)

其次,在编写Parsec解析器时,每个可解析实体通常看起来如下所示:

代码语言:javascript
复制
entity = do
  var1 <- parser1
  var2 <- parser2
  var3 <- parser3
  ...
  return $ foo var1 var2 var3...

由于应用函子不允许我们以这种方式将中间结果绑定到变量,所以我不知道如何将它们收集到最后阶段。为了理解如何做到这一点,我还没能充分地思考这个想法。

EN

回答 5

Stack Overflow用户

回答已采纳

发布于 2013-03-04 19:52:49

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

<$函数就是fmap const。所以a <$ b等于fmap (const a) b。您只需丢弃一个“操作”的结果,然后返回一个常量值。Control.Monad函数void (具有Functor f => f a -> f ()类型)可以编写为() <$

这三个函数对于应用函子的定义并不是基本的。(事实上,<$适用于任何函子。)同样,这就像用于monads的>>一样。我相信它们在类中是为了更容易地为特定实例优化它们。

当您使用应用函式时,您不会从函子中“提取”这个值。在单曲中,这是>>=所做的,也是foo <- ...所做的。相反,您可以直接使用<$><*>将包装好的值传递给函数。因此,您可以将您的示例重写为:

代码语言:javascript
复制
foo <$> parser1 <*> parser2 <*> parser3 ...

如果需要中间变量,只需使用let语句:

代码语言:javascript
复制
let var1 = parser1
    var2 = parser2
    var3 = parser3 in
foo <$> var1 <*> var2 <*> var3

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

代码语言:javascript
复制
pure foo <*> parser1 <*> parser2 <*> parser3

我希望这能澄清一些事情。

现在只是一张小纸条。人们建议使用应用函式函数进行解析。但是,只有当它们更有意义时,你才应该使用它们!对于足够复杂的事情,单一版本(特别是使用do符号)实际上可以更清楚一些。人们建议这样做的原因是

代码语言:javascript
复制
foo <$> parser1 <*> parser2 <*> parser3

既短又易读

代码语言:javascript
复制
do var1 <- parser1
   var2 <- parser2
   var3 <- parser3
   return $ foo var1 var2 var3

本质上,f <$> a <*> b <*> c本质上类似于提升函数应用程序。您可以想象<*>是空间(例如函数应用程序)的替代品,就像fmap是函数应用程序的替代品一样。这也应该给你一个直观的概念,为什么我们使用<$>--它就像一个提升版的$

票数 26
EN

Stack Overflow用户

发布于 2013-03-04 21:10:09

我可以在这里说几句话,希望能有所帮助。这反映了我的理解,这本身可能是错误的。

pure的名字非同寻常。函数通常是根据它们产生的内容命名的,但在pure x中,纯粹的是xpure x生成一个“携带”纯x的应用函子。“随身携带”当然是近似的。例如:pure 1 :: ZipList Int是一个ZipList,包含一个纯Int1

<*>*><*不是函数,而是方法(这是您首先关心的问题)。在它们的类型中,f不是通用的(对于函数也是如此),而是特定的,正如特定实例所指定的那样。这就是为什么他们是,而不是,只有$flip constconst。专用类型f指定组合的语义。在通常的应用程序设计中,组合意味着应用。但是对于函子,存在一个附加维数,由“载体”类型f表示。在f x中,有一个"contents",x,但是还有一个"context",f

“应用函子”样式试图启用“应用程序风格”编程,并具有效果。效果由函子、载体、语境提供者表示;“应用”指的是功能应用的正常应用风格。用f x 来表示应用程序曾经是一个革命性的想法。不再需要额外的语法,没有(funcall f x),没有CALL语句,没有任何额外的东西-组合是应用程序.不是这样的,对于效果,似乎-在使用效果进行编程时,再次需要特殊的语法。被杀的野兽又出现了。

因此,有效果的应用程序设计再次使组合意味着公正的应用-在特殊(也许是有效的)上下文中,如果他们确实在这样的上下文中。因此,对于a :: f (t -> r)b :: f t,(几乎普通的)组合a <*> b是--一种在给定上下文(类型为f)中的承载内容(或类型t -> rt)、的应用程序。

与单模的主要区别是,单模是非线性.在……里面

代码语言:javascript
复制
do {  x        <-  a
   ;     y     <-  b x
   ;        z  <-  c x y
   ;               return 
     (x, y, z) }

计算b x依赖于xc x y依赖于xy。函数是嵌套的。

代码语言:javascript
复制
a >>= (\x ->  b x  >>= (\y ->  c x y  >>= (\z ->  .... )))

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

代码语言:javascript
复制
a  >>= (\x       ->  b  >>= (\y-> return (x,y)))       -- `b  ` sic
   >>= (\(x,y)   ->  c  >>= (\z-> return (x,y,z)))     -- `c  `
   >>= (\(x,y,z) ->  ..... )

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

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

作为一个例子,本文的第一个例子展示了“一元”函数是如何实现的。

代码语言:javascript
复制
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) }

实际上,可以将这种“平面,线性”样式编码为

代码语言:javascript
复制
sequence :: (Applicative f) => [f a] -> f [a]
sequence []       = pure []
sequence (c : cs) = pure (:) <*> c <*> sequence cs
                  --     (:)     x     xs

在这里,monad的能力在以前的结果上是没有用的。

关于优秀的Petr Pudlák的回答的一个注意事项:用我这里的“术语”,他的pair是没有应用程序的组合。这说明了应用函子对普通函子所增加的本质是组合能力。然后,由好的旧fmap实现应用程序。这意味着组合函子可能是一个更好的名称(更新:实际上,“单形函子”是名称)。

票数 13
EN

Stack Overflow用户

发布于 2013-03-04 21:53:02

你可以这样看待函子、应用程序和单数:它们都带有一种“效果”和“价值”。(请注意,术语"effect“和"value”只是近似的--实际上不需要有任何副作用或值--比如在IdentityConst中)。

  • 使用Functor,您可以使用fmap修改内部可能的值,但不能在内部进行任何效果操作。
  • 使用Applicative,您可以使用pure创建一个没有任何影响的值,并且可以对效果进行排序并将其值组合到内部。但是效果和值是分开的:当排序效果时,效果不能取决于前一个效果的值。这反映在<*<*>*>中:它们对效果进行排序并组合它们的值,但您不能以任何方式检查内部的值。 您可以使用这组可选的函数定义Applicative: (a -> b) -> (f a -> f b) pureUnit ::f ()对:f a -> f b -> f (a,b) -甚至具有更有暗示性的类型(f a,f b) -> f (a,b) (其中pureUnit没有任何效果),并从它们定义pure<*> (反之亦然)。在这里,pair对两个效应进行排序,并记住它们的值。这个定义表达了Applicative是一个单面函子的事实。 现在考虑一个由pairfmappureUnit和一些原始应用值组成的任意(有限)表达式。我们有几条可以使用的规则: fmap f.==> fmap (f .(fmap F x) y ==> fmap ((a,b) -> (f a,b)) ((a,b) -> (f a,b)) ((a,b)->(f,a,b))对((a,b)->(f,a,b))((a,b)->(f,a,b))对((a,b)->(f,a,b))(对x)对x( 使用这些规则,我们可以重新排序pair,向外推fmap并消除pureUnit,因此最终可以将这些表达式转换为 pureFunction (x1 pair x2 pair . pair xn) 或 fmap pureFunction pureUnit 因此,我们首先可以使用pair收集所有效果,然后使用纯函数修改内部的结果值。
  • 使用Monad,效果可以取决于先前一元值的值。这使他们如此强大。
票数 8
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/15209516

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档