【问题标题】:What is the difference between '.' and '<<<' when performing function composition?'.' 和有什么区别?和 '<<<' 执行功能组合时?
【发布时间】:2018-08-27 22:41:09
【问题描述】:

我正在尝试在 Haskell 中执行函数组合,但我不确定使用哪个运算符是正确的。

文档包含这两种类型签名:

(.) :: (b -> c) -> (a -> b) -> a -> c 
(<<<) :: Category cat => cat b c -> cat a b -> cat a c 

显然这两个选项之间的区别在于Category cat 的存在/不存在,但是这个注释意味着什么,我应该如何使用这些信息来选择一个运算符而不是另一个?

在比较其他两个运算符时,我还注意到上述两个签名的第三种变体:

(>>) :: forall a b. m a -> m b -> m b 
(>>>) :: Category cat => cat a b -> cat b c -> cat a c

forall 注释是什么意思——&gt;&gt; 是否用于第三种情况?

【问题讨论】:

  • 功能没有区别。 &lt;&lt;&lt; 只是泛化的,就像 fmapmap

标签: haskell hindley-milner category-abstractions


【解决方案1】:

首先,您应该认识到 (.) 在 Prelude 中以特定功能的形式定义

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

以及Category 类提供的更通用的函数:

class Category cat where
    -- | the identity morphism
    id :: cat a a

    -- | morphism composition
    (.) :: cat b c -> cat a b -> cat a c

Category(-&gt;) 实例清楚地表明两者在功能上是相同的:

instance Category (->) where
    id = GHC.Base.id
    (.) = (GHC.Base..)

(&lt;&lt;&lt;) 的定义明确表明它只是(.) 的同义词

-- | Right-to-left composition
(<<<) :: Category cat => cat b c -> cat a b -> cat a c
(<<<) = (.)

旨在与(&gt;&gt;&gt;) 运算符对称。您可以写f &gt;&gt;&gt; gg &lt;&lt;&lt; f,以更适合您的特定用途为准。


(&gt;&gt;) 是一个完全不同的运算符(至少,只要您没有深入研究单子理论)。 (&gt;&gt;)(&gt;&gt;=) 的一个版本,它忽略第一个操作数的结果,仅将其用于其效果。

x >> y == x >>= (\_ -> y)

【讨论】:

  • 历史记录:如果不是Control.Arrow 的传承,(&lt;&lt;&lt;) 可能不会存在。
  • 您可能想提及与&gt;=&gt;&lt;=&lt;Kleisli 的连接,这间接弥合了&gt;&gt;&gt;&gt;&gt; 之间的差距。
  • @dfeuer 我欢迎对这种影响进行编辑(或单独的答案)。我认为我不能公正地讨论这个话题。
【解决方案2】:

一个纯粹的语法差异,尽管表面上可能实际上是最常见的用例,但&lt;&lt;&lt; 的优先级低于.

infixr 9 Control.Category..
infixr 1 Control.Category.<<<

这类似于普通函数应用程序f x(它比任何中缀绑定更紧密,所以它基本上是infixl 10)和使用$ 运算符(如f $ x)之间的区别,它的优先级最低@987654335 @。这意味着,您可以选择表达式中需要较少括号的表达式。 &lt;&lt;&lt; 当你想组合由一些中缀表达式定义的函数时很方便;这在使用 lenses 时经常发生。

更有趣的是,Category 版本不仅适用于函数,还适用于其他井的态射,类别。一个简单的例子是category of coercions:如果你有例如newtype-wrapped 值的列表,并且您想要获取底层表示,在列表上使用map 将是低效的——这将创建整个列表的副本,但其中包含完全相同的运行时信息。强制允许您一直使用原始列表,但不会绕过类型系统——编译器将在每个点跟踪列表的哪个“视图”中的元素具有什么类型。强制并不是真正的函数——它们在运行时总是无操作——但它们可以像函数一样组合(例如,从Product Int 强制转换为Int,然后从Int 强制转换为Sum Int)。

对于其他示例,Haskellers 通常会引用Kleisli 类别。它们包含a -&gt; m b 形式的函数,其中m 是一个monad。虽然你不能直接作曲,例如readFile :: FilePath -&gt; IO StringfirstFileInDirectory :: FilePath -&gt; IO FilePath,因为 FilePathIO FilePath 不匹配,您可以Kleisli 撰写它们:

import Control.Monad
main = writeFile "firstfileContents.txt" <=< readFile <=< firstFileInDirectory
            $ "src-directory/"

同样的东西也可以写

import Control.Arrow
main = runKleisli ( Kleisli (writeFile "firstfileContents.txt")
                 <<< Kleisli readFile
                 <<< Kleisli firstFileInDirectory
            ) $ "src-directory/"

有什么意义?好吧,它允许您对不同的类别进行抽象,从而使代码既可以用于纯函数,也可以用于IO 函数。但坦率地说,我认为Kleisli 在激励使用其他类别方面做得不好:任何你可以用 Kleisli 箭头写的东西通常在使用标准单子 do 表示法或仅使用 =&lt;&lt;&lt;=&lt; 运营商。 仍然允许您通过选择不同的 monad(IOST 或简单的 Identity)来抽象可能是纯的或不纯的计算。
显然有一些专业的解析器是 Arrows,但不能写成 monad,但它们并没有真正流行起来——似乎优势并不能平衡不太直观的风格。

在数学中,还有很多有趣的类别,但不幸的是,这些类别往往不能用Categoryes 来表达,因为并不是每个 Haskell 类型都可以是一个对象。我想举一个例子是category of linear mappings,它的对象只是代表向量空间的Haskell类型,例如Double(Double, Double)InfiniteSequence Double。线性映射本质上是矩阵,但不仅具有类型检查的域和共域维度,而且还具有表示具有特定含义的不同空间的选项,例如防止将位置矢量添加到重力场矢量。而且由于向量不需要用数字数组字面表示,因此您可以为每个应用程序优化表示,例如用于您想要进行机器学习的压缩图像数据。

线性映射不是Category 的实例,而是constrained category 的实例。

【讨论】:

  • 粗略地说,Category 是 Atkey 风格的索引应用函子,就像 MonoidApplicativeCategory 作为对类型对齐结构上折叠结果的约束非常自然地出现,就像 Monoid 对统一结构上的折叠所做的那样。
  • 仅仅因为如果你做了 X 那么 Y 会自然而然地出现,并不意味着 X 一定是很好地说明了为什么 Y 很有趣。
  • 确实如此。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-12-02
  • 1970-01-01
  • 2012-02-17
  • 2012-01-30
  • 1970-01-01
相关资源
最近更新 更多