我正在尝试在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
注释是什么意思– >>
用于第三种情况?
答案 0 :(得分: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
(->)
的{{1}}实例清楚地表明,这两个函数是相同的:
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 >>> g
,以您的特定用途为准。
g <<< f
是一个完全不同的运算符(至少,只要您对单子理论不做过多研究即可)。 (>>)
是(>>)
的一个版本,它忽略第一个操作数的结果,仅将其用于效果。
(>>=)
答案 1 :(得分:3)
纯粹的语法差异(尽管肤浅可能实际上是最常见的用例)是<<<
的优先级低于.
:
infixr 9 Control.Category..
infixr 1 Control.Category.<<<
这类似于普通函数应用程序f x
(绑定比任何中缀都紧,因此基本上是infixl 10
)与使用$
运算符(如{{1})之间的区别},其优先级最低f $ x
。这意味着,您可以选择在表达式中哪一个需要较少的括号。如果您想编写由一些infix表达式定义的函数,infixr 0 $
会很方便;在使用lenses时经常会发生这种情况。
从理论上讲更有趣的是,<<<
版本不仅适用于函数,而且适用于其他类别的态射。一个简单的示例是category of coercions:如果您有Category
包装的值的列表,而您想获取基础表示,则newtype
在列表上的效率会很低–这会创建整个列表的副本,但其中包含精确的相同的运行时信息。强制使您可以始终使用原始列表,而无需绕过类型系统-编译器将在每个点上跟踪元素在列表的“视图”中具有哪种类型。强制实际上不是函数,它们在运行时始终是无操作的,但是它们可以像函数一样组成(例如,从Product Int
强制为map
,然后从Int
强制为{ {3}}。
对于其他示例,Haskellers通常将引用Sum Int
类别。它们包含格式为Int
的函数,其中a -> m b
是monad。虽然您不能直接撰写例如m
与readFile :: FilePath -> IO String
,因为firstFileInDirectory :: FilePath -> IO FilePath
和FilePath
之间不匹配,因此您可以 Kleisli编写它们:
IO FilePath
可以写同样的东西
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
单引号或用{ do
或=<<
运算符。通过 still ,您只需选择不同的monad(<=<
,IO
或简单地ST
)就可以抽象出纯净或不纯净的计算。
显然,有些专家解析器是Identity
,但不能写成monad,但它们并没有真正流行起来-似乎优点是无法平衡不太直观的样式。
在数学中,还有更多有趣的类别,但不幸的是,由于并非每种Haskell类型都可以是一个对象,这些类别往往无法表示为Arrows
。我想举一个例子,Kleisli
,其对象只是代表矢量空间的Haskell类型,例如Category
或Double
或(Double, Double)
。线性映射本质上是矩阵,但不仅具有类型检查的域和共域维,而且还具有表示具有特殊含义的不同空间的选项,例如防止将位置矢量添加到重力场矢量。而且由于向量不需要直接由数字数组表示,因此您可以为每个应用程序优化表示,例如用于要对其进行机器学习的压缩图像数据。
线性映射不是InfiniteSequence Double
实例,但它们是category of linear mappings的实例。