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?
The signature says,
and
(->)
is an instance ofArrow
type class. So, rewriting the signature specialized for(->)
type:in, infix form it will look like:
which simply means
a simple replication would be:
which would work the same way:
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:
Walking the type there, we've got
A more complex example where it's not all b:
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.
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 ofa b c
, to make the parallel with functions more apparent.It might be easier to skip some of the explanation of how arrows work and look at the type of
(negate &&& (+5))
:With
negate :: a -> a
and(+5) :: a -> a
, the function created by&&&
takes a value of typea
and returns a pair of values, both of which have typea
as well.When considering a function as an instance of
Arrow
,b
andc
are simply the names given to the argument and return types, respectively. That is, iff :: b -> c
andg :: 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)
.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-functionf
applied to some type-argumenta
to produce another type-functionf a
which is being applied to some type-argumentb
. 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 froma b c
to functions froma b c'
toa b (c, c')
with the added constraint thata
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
toc
and another fromb
toc'
, and combine them together into an arrow of that same sort, fromb
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 theArrow
typeclass, so something can only be anArrow
from one type to another type if there is some way to perform an analogous "parallel operation" of the two arrows.