How to make sense of the Haskell type signature fo

2020-02-10 17:54发布

I'm trying to wrap my head around how Haskell's Control.Arrow's &&& works, but fear I'm on the verge of losing my way.

Specifically, I'm confused (as a beginner) by how to makes sense of its behavior from the type signature

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

in

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

or even just

(negate &&& (+5)) 5

for example, the first argument is "missing" b and c, while the second is missing just c', and the result looks to me like (c, c'), not a b (c, c').

Can someone walk me through how &&& works, in the context of its type?

4条回答
姐就是有狂的资本
2楼-- · 2020-02-10 18:21

The signature says,

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

and (->) is an instance of Arrow type class. So, rewriting the signature specialized for (->) type:

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

in, infix form it will look like:

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

which simply means

(&&&) :: (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

a simple replication would be:

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

which would work the same way:

\> (negate &:& (+5)) <$> [1, 2, 3]
[(-1,6),(-2,7),(-3,8)]
查看更多
爱情/是我丢掉的垃圾
3楼-- · 2020-02-10 18:25

I always think of &&& as a split and apply operation. You've got an Arrow value, and you're going to apply two functions (sorry, arrows, but it works with functions and makes explanations easier) to it, and keep both results, thus splitting the stream.

Dead simple example:

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

Walking the type there, we've got

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

A more complex example where it's not all b:

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

So in plain english: &&& takes two functions, and combines them into a single function that takes it input, applies both functions to it, and returns the result pair.

But it's defined on arrows, not functions. Yet it works exactly the same: it takes two arrows, and combines them into a single arrow that takes its input, applies both arrows to it, and returns the result pair.

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

Addendum: part of what seems to confuse you is whether or not the a type still appears in the type signatures. The rule of thumb here is that it works the same as for when you see the -> in function types: it shows as long as it's not applied.

I recall reading some Arrow literature that wrote arrows as b ~> c (note the tilde not dash) instead of a b c, to make the parallel with functions more apparent.

查看更多
闹够了就滚
4楼-- · 2020-02-10 18:44

It might be easier to skip some of the explanation of how arrows work and look at the type of (negate &&& (+5)):

> :t (negate &&& (+5))
(negate &&& (+5)) :: Num a => a -> (a, a)

With negate :: a -> a and (+5) :: a -> a, the function created by &&& takes a value of type a and returns a pair of values, both of which have type a as well.

When considering a function as an instance of Arrow, b and c are simply the names given to the argument and return types, respectively. That is, if f :: b -> c and g :: b -> c' are two functions that take arguments of the same type but return values of possibly different types, then ( f &&& g ) :: b -> (c, c'), meaning the new function takes a single value of the common input type, then returns a pair consisting of the return values of each of the original functions, or ( f &&& g ) x = (f x, g x).

查看更多
家丑人穷心不美
5楼-- · 2020-02-10 18:48

You've got a bunch of great answers; I just wanted to add how to understand this thing by only looking at the syntax.

The rules for Haskell's type language are the same as Haskell's normal language: f a b = (f a) b is some type-function f applied to some type-argument a to produce another type-function f a which is being applied to some type-argument b. Then there is one operator (and some compiler flags allow you to enable others) -> which denotes the type of functions which expect the first type as input and return the second type as output.

So the expression (&&&) :: (Arrow a) => a b c -> a b c' -> a b (c, c') means "The type of functions from a b c to functions from a b c' to a b (c, c') with the added constraint that a is a member of the Arrow typeclass.

There are lots of different sorts of arrows; this says "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')."

Intuitively, we'd say that it composes the two arrows "in parallel" so that they both act on the same input, produce their different outputs, and then we "join" those outputs with our inhomogeneous-pair data structure (,).

For the special case where the arrow is (->), the function arrow, this is clearly (f &&& g) = \b -> (f b, g b). This operator (&&&) is a part of the definition of the Arrow typeclass, so something can only be an Arrow from one type to another type if there is some way to perform an analogous "parallel operation" of the two arrows.

查看更多
登录 后发表回答