More Functions for Functors
Before moving on to Foldable
containers, let's look at some convenient
functions to work with functors. We have already seen (<$)
as part of the
Functor
class definition. We also have ($>)
, (<$>)
, (<&>)
, and void
.
<$>
(<$>)
is an infix version of fmap
:
(<$>) :: Functor f => (a -> b) -> f a -> f b
(<$>) = fmap
So, instead of writing fmap f xs
, we can write f <$> xs
. If you're
comfortable with the function application operator ($)
, then it feels natural
to use (<$>)
to map a function over a functor.
<&>
(<&>)
is a flipped version of <$>
(or fmap
):
(<&>) :: Functor f => f a -> (a -> b) -> f b
(<&>) = flip fmap
So, to apply a function f
to every element in a container xs
, we can also
write xs <&> f
. Sometimes, this backwards way of writing function application
makes our code clearer. In particular, when building a long pipeline of
functions,
input <&> f
<&> g
<&> h
<&> i
may be easier to read than
i <$>
h <$>
g <$>
f <$> input
because the former lets us read the functions we apply in order top down, almost as in an imperative program.1
flip
is a useful helper function that takes a two-argument function and
converts it into a function whose arguments are swapped:
flip :: (a -> b -> c) -> (b -> a -> c)
flip f y x = f x y
$>
We also have a flipped version of (<$)
, called ($>)
. Remember, y <$ xs
replaces every value in xs
with y
. xs $> y
does the same.
($>) :: Functor f => f a -> b -> f b
($>) = flip (<$)
void
Finally, we have void
:
void :: Functor f => f a -> f ()
void xs = () <$ xs
It's a simple function, but there is still a lot to unpack here because we
haven't discussed ()
yet. Consider a function in C that we call only for its
side effects; it doesn't return anything useful. Such a function returns void
in C. void
represents the absence of a value in C. In Haskell, we can't have
functions that don't return anything. We can't even have side effects, but we
will discuss how to implement common side effects in a purely functional manner
in the next chapter. The tool to do this is monads, which are functors with some
extra properties. If f
is a functor that models side effects, then it makes
sense to build functions that perform some side effects and return nothing
useful. Since any such function still needs to return something, we need a type
that represents "nothing useful." ()
is this type. Haskell programmers call it
"void" because a function that returns ()
is the equivalent of a C function
that does not return anything. The type ()
has exactly one value, which
happens to also be called ()
. So, in pseudo-Haskell, we have
data () = ()
Now assume we have a computation x
with side effects that returns a value of
type a
. If f
is the functor we use to model the side effects, this
computation has type f a
. We may call x
as the last step in a computation
y
that is not meant to return anything useful; the type of y
is f ()
. The
way Haskell treats such sequencing of computations is that the return value of
y
is the same as the return value of its last step, the return value of x
.
Now we have a type mismatch. y
is supposed to return ()
but x
returns a
.
void
ignores the return value of x
and replaces it with ()
. So we can use
void x
instead of x
as the last step of y
to eliminate the type mismatch
between the return values of x
and y
.
All of this will make more sense in the next chapter. Returning to the view of
functors as containers for now, we can also view void xs
as replacing every
value in xs
with ()
. That's what the definition says: void xs = () <$ xs
,
and y <$ xs
replaces every value in xs
with y
. If xs
is a tree, for
example, then we can think about void xs
as xs
with all its values erased;
we're keeping only the shape of xs
and replace every value with ()
, which
carries no useful information. Such a transformation could be useful when
writing code that manipulates trees not so much as containers but as
graph-theoretic entities in their own right.
-
If you import the
Data.Function
module, then you also gain access to an operator(&) :: a -> (a -> b) -> b x & f = f x
Just as
(<&>)
is the flipped version of(<$>)
,(&)
is the flipped version of($)
. So, once again, instead ofi $ h $ g $ f $ input
we can write
input & f & g & h & i
making the sequence of functions we apply read like a pipeline from top to bottom. ↩