What is the difference between '.' and '<<<' when performing function composition?

148 views Asked by At

I'm trying to perform function composition in Haskell, and I'm not sure which operator is the correct one to use.

The docs contain these two type signatures:

(.) :: (b -> c) -> (a -> b) -> a -> c 
(<<<) :: Category cat => cat b c -> cat a b -> cat a c 

Clearly the difference between these two options is the presence/absence of Category cat, but what does this annotation signify, and how should I use the information to choose one operator over the other?

I also noticed a third variant on the above two signatures when comparing two other operators:

(>>) :: forall a b. m a -> m b -> m b 
(>>>) :: Category cat => cat a b -> cat b c -> cat a c

What does the forall annotation mean—is >> for use in a third scenario?

2

There are 2 answers

3
chepner On

First, you should recognize that (.) is defined in the Prelude in a function-specific form

(.) :: (b -> c) -> (a -> b) -> a -> c 

as well as a more generic function provided by the Category class:

class Category cat where
    -- | the identity morphism
    id :: cat a a

    -- | morphism composition
    (.) :: cat b c -> cat a b -> cat a c

The (->) instance of Category makes clear that the two are the same for functions:

instance Category (->) where
    id = GHC.Base.id
    (.) = (GHC.Base..)

The definition of (<<<) makes clear that it is just a synonym of (.)

-- | Right-to-left composition
(<<<) :: Category cat => cat b c -> cat a b -> cat a c
(<<<) = (.)

intended for symmetry with the (>>>) operator. You can write either f >>> g or g <<< f, whichever makes more sense for your particular use.


(>>) is an entirely different operator (at least, as long as you aren't delving too much into the theory of monads). (>>) is a version of (>>=) that ignores the result of the first operand, using it only for its effect.

x >> y == x >>= (\_ -> y)
3
leftaroundabout On

A purely syntactic difference, that despite being superficial may actually be the most common use case, is that <<< has lower precedence than .:

infixr 9 Control.Category..
infixr 1 Control.Category.<<<

This is similar to the difference between plain function application f x (which binds tighter than any infix, so it's basically infixl 10) and use of the $ operator like f $ x, which has lowest precedence infixr 0 $. That means, you can opt for which one requires less parentheses in an expression. <<< is handy when you want to compose functions that are themselves defined by some infix expression; this often happens when working with lenses.

More theoretically interesting is that the Category version works not only with functions, but also with morphisms from other well, categories. A simple example is the category of coercions: if you have e.g. a list of newtype-wrapped values and you want to get at the underlying representations, it would be inefficient to map over the list – this would create a copy of the entire list, which however contains the exact same runtime information. A coercion allows you to use the original list all the time, but without bypassing the type system – the compiler will at each point keep track in which “view” of the list the elements have what type. Coercions aren't actually functions – they are always no-ops at runtime – but they can be composed just like functions (e.g. coerce from Product Int to Int, then coerce from Int to Sum Int).

For other examples, Haskellers will usually refer to Kleisli categories. These contain functions of the form a -> m b, where m is a monad. Although you cannot directly compose e.g. readFile :: FilePath -> IO String with firstFileInDirectory :: FilePath -> IO FilePath, because there's a mismatch between FilePath and IO FilePath, you can Kleisli compose them:

import Control.Monad
main = writeFile "firstfileContents.txt" <=< readFile <=< firstFileInDirectory
            $ "src-directory/"

and the same thing can be written

import Control.Arrow
main = runKleisli ( Kleisli (writeFile "firstfileContents.txt")
                 <<< Kleisli readFile
                 <<< Kleisli firstFileInDirectory
            ) $ "src-directory/"

What's the point? Well, it allows you to abstract over different categories and thus have code that will work both with pure functions and with IO functions. But frankly, I think Kleisli does a bad job at motivating the use of other categories: anything you can write with Kleisli arrows is typically more readable when written with standard monadic do notation, or else just with the =<< or <=< operators. That still allows you to abstract over computations that may be pure or impure, simply by choosing different monads (IO, ST or simply Identity).
There are apparently some specialist parsers that are Arrows but can't be written as monads, but they haven't really caught on – it seems the advantages do not balance the less intuitive style.

In maths, there are many more interesting categories, but unfortunately these tend not to be expressible as Categoryes because not every Haskell type can be an object. An example I like to give is the category of linear mappings, whose objects are only Haskell types that represent vector spaces, such as Double or (Double, Double) or InfiniteSequence Double. The linear mappings are essentially matrices, but with not only type-checked domain- and codomain dimension but also the option to represent different spaces with particular meaning, e.g. preventing adding a position vector to a gravity-field vector. And because the vectors don't need to be literally represented by arrays of numbers, you can have optimised representations for every application, e.g. for compressed image data on which you want to do machine learning.

Linear mappings are not a Category instance, but they are an instance of constrained category.