2

我正在尝试在 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注释是什么意思——>>用于第三种情况?

4

2 回答 2

6

首先,您应该认识到(.)在 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清楚地表明两者对于函数是相同的:

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

的定义(<<<)清楚地表明它只是(.)

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

旨在与(>>>)操作员对称。您可以编写f >>> gg <<< f,以对您的特定用途更有意义。


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

x >> y == x >>= (\_ -> y)
于 2018-08-27T22:50:56.417 回答
3

一个纯粹的句法差异,尽管它实际上可能是最常见的用例,但它的<<<优先级低于.

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

这类似于普通函数应用程序f x(它比任何中缀绑定更紧密,所以基本上是infixl 10)和使用具有最低优先级的$运算符 like之间的区别。这意味着,您可以选择表达式中需要较少括号的表达式。当您想要编写本身由某些中缀表达式定义的函数时,它会很方便;这在使用镜头时经常发生。f $ xinfixr 0 $<<<

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

对于其他示例,Haskellers 通常会引用Kleisli类别。这些包含形式的函数a -> m b,其中m是 monad。虽然你不能直接组成例如readFile :: FilePath -> IO Stringwith firstFileInDirectory :: FilePath -> IO FilePath,因为FilePathand之间存在不匹配IO 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符号来写,或者只用=<<or<=<操作符写,通常都更易读。这仍然IO允许您通过选择不同的单子(ST或简单地)来抽象可能是纯的或不纯的计算Identity
显然有一些专业的解析器是Arrows但不能写成 monads,但它们还没有真正流行起来——似乎优势并不能平衡不太直观的风格。

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

线性映射不是Category实例,但它们是受约束类别的实例。

于 2018-08-28T12:31:37.687 回答