【问题标题】:Haskell function composition operator of type (c→d) → (a→b→c) → (a→b→d)(c→d) → (a→b→c) → (a→b→d) 类型的 Haskell 函数组合算子
【发布时间】:2011-04-28 15:29:19
【问题描述】:

普通函数组合的类型

(.) :: (b -> c) -> (a -> b) -> a -> c

我认为这应该概括为以下类型:

(.) :: (c -> d) -> (a -> b -> c) -> a -> b -> d

一个具体的例子:计算差异平方。我们可以写diffsq a b = (a - b) ^ 2,但感觉我应该能够编写(-)(^2) 来写diffsq = (^2) . (-) 之类的东西。

我当然不能。我可以做的一件事是使用一个元组而不是(-) 的两个参数,通过使用uncurry 对其进行转换,但这不一样。

有可能做我想做的事吗?如果不是,我有什么误解让我认为它应该是可能的?


注意:这实际上已经被问到here,但没有给出答案(我怀疑必须存在)。

【问题讨论】:

  • 您拥有的组合子是blackbird :: (c -> d) -> (a -> b -> c) -> a -> -> b -> d。您可以将其视为将post-transformer 应用于常规函数应用程序。
  • 很好的问题,因为它突出了数学和 Haskell 中的函数组合之间的细微差别,这可能会令人困惑。对于这个例子,我更喜欢使用 uncurry (-) 的解决方案,因为它比替代方案更简单,并且指向潜在问题。

标签: haskell function-composition pointfree


【解决方案1】:

我的首选实现是

fmap . fmap :: (Functor f, Functor f1) => (a -> b) -> f (f1 a) -> f (f1 b)

如果只是因为它很容易记住。

分别将 f 和 f1 实例化为 (->) c(->) d 时,您会得到类型

(a -> b) -> (c -> d -> a) -> c -> d -> b

这是

的类型
(.) . (.) ::  (b -> c) -> (a -> a1 -> b) -> a -> a1 -> c

但是fmap . fmap 版本更容易说出来,它可以推广到其他仿函数。

有时这写成fmap fmap fmap,但写成fmap . fmap 它可以更容易地扩展以允许更多参数。

fmap . fmap . fmap 
:: (Functor f, Functor g, Functor h) => (a -> b) -> f (g (h a)) -> f (g (h b))

fmap . fmap . fmap . fmap 
:: (Functor f, Functor g, Functor h, Functor i) => (a -> b) -> f (g (h (i a))) -> f (g (h (i b))

等等

一般fmap与自己组成n次可以用到fmapn个层次!

由于函数形成Functor,这为n个参数提供了管道。

有关详细信息,请参阅 Conal Elliott 的 Semantic Editor Combinators

【讨论】:

  • 此外,您还可以混入其他语义组合器,例如firstsecond。例如:fmap.first.fmap.second :: (Functor g, Functor f) => (b -> c) -> f (g (d1, b), d) -> f (g (d1, c), d)。这些组合可以很容易地编写和阅读,因为它们提供了一条从正在编辑的整体值的类型到实际修改的子值的路径。
  • IIRC,有组合子.:定义为.: = (.).(.)
  • 非常正确。我确实发现 n 次组合 fmapresult 组合子更加直观和有用,因为 .: 方法在没有线性数量的实现的情况下无法泛化。
  • 这个答案很有启发性。我不知道(.) 只不过是fmap 专门用于函子(->) c
【解决方案2】:

误解是你认为a -> b -> c 类型的函数是两个返回类型为c 的参数的函数,而实际上它是一个返回类型为b -> c 的参数的函数,因为函数类型关联到右边(即它与a -> (b -> c) 相同。这使得无法使用标准函数组合运算符。

要了解原因,请尝试将 (y -> z) -> (x -> y) -> (x -> z) 类型的运算符 (.) 应用于两个函数 g :: c -> df :: a -> (b -> c)。这意味着我们必须将yc 以及b -> c 统一起来。这没有多大意义。 y 怎么可能既是 c 又是返回 c 的函数?那必须是无限类型。所以这行不通。

仅仅因为我们不能使用标准的组合运算符,它并不能阻止我们定义自己的。

 compose2 :: (c -> d) -> (a -> b -> c) -> a -> b -> d
 compose2 g f x y = g (f x y)

 diffsq = (^2) `compose2` (-)

通常最好避免在这种情况下使用无点样式,而直接使用

 diffsq a b = (a-b)^2

【讨论】:

  • 您可以在不定义自己的构图的情况下获得“中途”:sumSq a = (^2) . (+a)。通过命名和应用第一个参数,我们构造了一个 b -> c 类型的函数,用于常规合成。
  • 确实可以,但它很快就会变得混乱并且在这种情况下“感觉不对”,因为它在函数可交换时以不同方式处理两个参数。
  • 很好的解释,谢谢。出于某种原因,写compose2 让我脑筋急转弯。我学习。
  • compose2 = g f x y = g (f x y): 第一个等号必须去掉。
  • 我喜欢(g .) . f 的无点风格,但我承认它看起来有点与((^2) .) . (-) 的部分混淆
【解决方案3】:

我不知道执行此操作的标准库函数,但实现它的无点模式是组合组合函数:

(.) . (.) :: (b -> c) -> (a -> a1 -> b) -> a -> a1 -> c

【讨论】:

  • 虽然这是一个可爱的技巧,但我不建议编写那种代码。
  • 我想let (f .. g) a b = f (g a b),但.. 是其他东西的语法。 ... 看起来不太对。
  • @hammar 是的。我也不会。但它确实对(.) 如何与只有一个参数的函数交互提供了不同的见解。
  • 使用撰写撰写撰写。很有趣。
  • 我喜欢它。不仅仅是一个可爱的技巧,我认为它非常有用。虽然我看到这可能不属于“最终”代码,但它是一个很好的教育练习,我认为我从中获得了一些见识。
【解决方案4】:

我本来打算在评论中写这个,但是有点长,而且它来自于mightybyte和hammar。

我建议我们围绕运算符进行标准化,例如 .* 对应 compose2.** 对应 compose3。使用mightybyte的定义:

(.*) :: (c -> d) -> (a -> b -> c) -> (a -> b -> d)
(.*) = (.) . (.)

(.**) :: (d -> e) -> (a -> b -> c -> d) -> (a -> b -> c -> e)
(.**) = (.) . (.*)

diffsq :: (Num a) => a -> a -> a
diffsq = (^2) .* (-)

modminus :: (Integral a) => a -> a -> a -> a
modminus n = (`mod` n) .* (-)

diffsqmod :: (Integral a) => a -> a -> a -> a
diffsqmod = (^2) .** modminus

是的,modminusdiffsqmod 是非常随机且毫无价值的函数,但它们很快并且显示了重点。请注意,通过在另一个 compose 函数中组合来定义下一个级别是多么容易(类似于 Edward 提到的链接 fmaps)。

(.***) = (.) . (.**)

实际上,从compose12 开始,写函数名比写操作符要短

f .*********** g
f `compose12` g

虽然计算星号很累,所以我们可能想在 4 或 5 停止约定。


[edit] 另一个随机的想法,我们可以使用 .: 代表 compose2,.:. 代表 compose3,.:: 代表 compose4,.::. 代表 compose5,.::: 代表 compose6,让点数(之后第一个)直观地标记要向下钻取的参数数量。不过我觉得我更喜欢星星。

【讨论】:

  • 我在处理 Data.Aviary 时想到了这个问题。我的结论是,给组合运算符 ASCII 名称是对命名空间的浪费,ASCII 名称应该为额外的数学运算符(如 Conal Elliott 的向量空间)“保留”。使用反引号中的文本名称排版具有数学倾向的代码是可怕的,因此数学运算符应该首先选择名称空间。如果您无法为组合运算符想出一个好的文本名称,那么您最好使用有针对性的代码。
  • @stephen Function composition 是一种数学运算,尽管我承认当函数有多个参数时它会变得模糊。不过,我真的无法想象.* 将如何用作数学运算符,并且像这样的组合函数几乎总是以中缀样式调用。
  • 如果 compose2 是 .: 并且 compose3 是 .:. 那么 compose 必须是 ..,但它不是!无法想象的不公平!
【解决方案5】:

正如Maxcomment 中指出的那样:

diffsq = ((^ 2) .) . (-)

您可以将f . g 视为将一个参数应用于g,然后将结果传递给f(f .) . g 将两个参数应用于g,然后将结果传递给f((f .) .) . g 将三个参数应用于g,依此类推。

\f g -> (f .) . g :: (c -> d) -> (a -> b -> c) -> a -> b -> d

如果我们用某个函数f :: c -> d 左切组合运算符(左侧带有f 的部分应用程序),我们得到:

(f .) :: (b -> c) -> b -> d

所以我们有了这个新函数,它需要来自b -> c 的函数,但我们的ga -> b -> c,或者等效地是a -> (b -> c)。我们需要先申请a,然后才能得到我们需要的东西。好吧,让我们再迭代一次:

((f .) .) :: (a -> b -> c) -> a -> b -> d

【讨论】:

    【解决方案6】:

    我认为这是实现您想要的一种优雅的方式。 Functor 类型类提供了一种将函数“推送”到容器中的方法,因此您可以使用 fmap 将其应用于每个元素。您可以将函数a -> b 视为bs 的容器,其中每个元素由a 的元素索引。所以很自然的做这个实例:

    instance Functor ((->) a) where
      fmap f g = f . g
    

    (我想你可以通过import找到一个合适的库来获得它,但我不记得是哪个了。)

    现在fg 的通常组合只是fmap

    o1 :: (c -> d) -> (b -> c) -> (b -> d)
    f `o1` g = fmap f g
    

    a -> b -> c 类型的函数是c 类型元素的容器容器。所以我们只需要将函数f 向下推两次。给你:

    o2 :: (c -> d) -> (a -> (b -> c)) -> a -> (b -> d)
    f `o2` g = fmap (fmap f) g
    

    在实践中,您可能会发现您不需要o1o2,只需fmap。如果你能找到我忘记位置的图书馆,你可能会发现你可以直接使用fmap而不用写 任何额外的代码。

    【讨论】:

    • @Conal 是的,但我试图使代码尽可能清晰。
    • user207442:在这种情况下,您可能会特别喜欢我上面建议的语义编辑器组合器。他们澄清了一般情况,而不是堆积在专门的专业上,一旦人们了解正在发生的事情,他们就会非常清楚和有启发性。另一方面,对于特殊情况,您的 o2 更简单。
    • Semantic editor combinators,Conal Elliott 的一篇非常有启发性的文章。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-01-27
    • 1970-01-01
    • 1970-01-01
    • 2018-09-14
    • 1970-01-01
    • 2020-12-25
    • 2011-06-13
    相关资源
    最近更新 更多