Skip to content

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.


  1. 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 of

    i $
    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.