如何理解 Control.Arrow 的“&&&”运算符的 Haskell 类型签名

How to make sense of the Haskell type signature for Control.Arrow's '&&&' operator

我正在努力思考 Haskell 的 Control.Arrow&&& 是如何工作的,但担心我正处于迷路的边缘。

具体来说,我(作为初学者)对如何从类型签名中理解它的行为感到困惑

(&&&) :: a b c -> a b c' -> a b (c, c')

import Control.Arrow
(negate &&& (+5)) <$> [1,2,3]

甚至只是

(negate &&& (+5)) 5

例如,第一个参数是 "missing" bc,而第二个参数只缺少 c',结果在我看来是 (c, c'),不是 a b (c, c').

谁能告诉我 &&& 在其类型的上下文中是如何工作的?

我一直认为 &&& 是拆分和应用操作。您有一个箭头值,您将对其应用两个函数(抱歉,箭头,但它与函数一起使用并使解释更容易),并保留两个结果,从而拆分流。

简单的例子:

λ> (succ &&& pred) 42
(43,41)

在那里走类型,我们有

succ &&& pred :: Arrow a, Enum b => a b (b,b)

一个更复杂的例子,其中不全是 b:

show &&& (== 42) :: Arrow a, Show b, Eq b, Num b => a b (String,Bool)

所以用简单的英语来说:&&& 接受两个函数,并将它们组合成一个函数,该函数接受输入,将两个函数应用于它,returns 结果对。

但它是在箭头上定义的,而不是函数。然而它的工作原理完全相同:它需要两个箭头,并将它们组合成一个接受输入的箭头,将两个箭头应用于它,然后 returns 结果对。

arrowOne :: Arrow a => a b c
arrowTwo :: Arrow a => a b c'
arrowOne &&& arrowTwo :: Arrow a => a b (c,c')

附录:部分让您感到困惑的是 a 类型是否仍然出现在类型签名中。这里的经验法则是,它的工作原理与您在函数类型中看到 -> 时的效果相同:只要未应用它就会显示。

我记得读过一些 Arrow 文献,将箭头写成 b ~> c(注意波浪号而不是破折号)而不是 a b c,以使与函数的平行更明显。

签名说,

(&&&) :: Arrow a => a b c -> a b c' -> a b (c, c')

(->) is an instance of Arrow type class. 所以,重写专门用于 (->) 类型的签名:

(&&&) :: ((->) b c) -> ((->) b c') -> ((->) b (c, c'))

在中缀形式中,它看起来像:

(b -> c) -> (b -> c') -> (b -> (c, c'))

这就是

(&&&) :: (b -> c)   -- given a function from type `b` to type `c`
      -> (b -> c')  -- and another function from type `b` to type `c'`
      -> (b -> (c, c')) -- returns a function which combines the result of
                        -- first and second function into a tuple

一个简单的复制是:

(&:&) :: ((->) b c) -> ((->) b c') -> ((->) b (c, c'))
f &:& g = \x -> (f x, g x)

这将以相同的方式工作:

\> (negate &:& (+5)) <$> [1, 2, 3]
[(-1,6),(-2,7),(-3,8)]

跳过一些关于箭头工作原理的解释并查看 (negate &&& (+5)):

的类型可能更容易
> :t (negate &&& (+5))
(negate &&& (+5)) :: Num a => a -> (a, a)

对于negate :: a -> a(+5) :: a -> a&&&创建的函数取一个a类型的值,return是一对值,都是也有类型a

当将函数视为 Arrow 的实例时,bc 只是分别为参数和 return 类型指定的名称。也就是说,如果 f :: b -> cg :: b -> c' 是两个接受相同类型参数但 return 可能不同类型值的函数,则 ( f &&& g ) :: b -> (c, c'),这意味着新函数接受公共输入类型的单个值,然后 returns 由每个原始函数的 return 值组成的一对,或 ( f &&& g ) x = (f x, g x).

你有很多很好的答案;我只是想通过只看语法.

来补充如何理解这个东西

Haskell 类型语言的规则与 Haskell 的普通语言相同:f a b = (f a) b 是某种类型函数 f 应用于某种类型-argument a 生成另一个类型函数 f a,它被应用于某些类型参数 b。然后是一个运算符(一些编译器标志允许您启用其他运算符)-> 表示期望第一种类型作为输入的函数类型,return 第二种类型作为输出。

所以表达式 (&&&) :: (Arrow a) => a b c -> a b c' -> a b (c, c') 的意思是“从 a b ca b c'a b (c, c') 的函数类型,加上 a 是Arrow 类型类的成员。

有很多不同种类的箭头;这表示 "I take two arrows of the same sort, one from b to c and another from b to c', and combine them together into an arrow of that same sort, from b to (c, c')."

直觉上,我们会说它由两个箭头 "in parallel" 组成,因此它们都作用于相同的输入,产生不同的输出,然后我们 "join" 这些输出与我们的非齐次-pair数据结构(,).

对于箭头为(->)的特殊情况,函数箭头,这显然是(f &&& g) = \b -> (f b, g b)。这个运算符 (&&&)Arrow 类型类定义的一部分,所以如果有某种方法可以执行类似的 "parallel operation" 两个箭头。