Search code examples
haskellpointfreefunction-composition

Can't get point free notation to compile in Haskell


This is working

unique :: (a -> Bool) -> [a] -> Bool
unique p xs = 1 == length (filter p xs)

But now I want it in the form:

unique = (== 1) . length . filter

Error message:

Couldn't match expected type `[a] -> Bool' with actual type `Bool'
Expected type: b0 -> [a] -> Bool
  Actual type: b0 -> Bool
In the first argument of `(.)', namely `(== 1)'
In the expression: (== 1) . length . filter

Why is this not working?


Solution

  • This is because filter is a two argument function. You can get around this using the handy operator

    (.:) = (c -> d) -> (a -> b -> c) -> a -> b -> d
    (.:) = (.) . (.)
    
    -- Important to make it the same precedence as (.)
    infixr 9 .:
    
    unique = ((== 1) . length) .: filter
    

    If you look at the type of (length .) in GHCi, you'll get

    (length .) :: (a -> [b]) -> a -> Int
    

    This means that it takes a single argument function that returns a list. If we look at the type of filter:

    filter :: (a -> Bool) -> [a] -> [a]
    

    This can be rewritten to make it "single argument" as

    filter :: (a -> Bool) -> ([a] -> [a])
    

    And this quite clearly does not line up with a -> [b]! In particular, the compiler can't figure out how to make ([a] -> [a]) be the same as [b], since one is a function on lists, and the other is simply a list. So this is the source of the type error.


    Interestingly, the .: operator can be generalized to work on functors instead:

    (.:) :: (Functor f, Functor g) => (a -> b) -> f (g a) -> f (g b)
    (.:) = fmap fmap fmap
    -- Since the first `fmap` is for the (->) r functor, you can also write this
    -- as (.:) = fmap `fmap` fmap === fmap . fmap
    

    What is this good for? Say you have a Maybe [[Int]], and you wanted the sum of each sublist inside the Just, provided it exists:

    > let myData = Just [[3, 2, 1], [4], [5, 6]]
    > sum .: myData
    Just [6, 4, 11]
    > length .: myData
    Just [3, 1, 2]
    > sort .: myData
    Just [[1,2,3],[4],[5,6]]
    

    Or what if you had a [Maybe Int], and you wanted to increment each one:

    > let myData = [Just 1, Nothing, Just 3]
    > (+1) .: myData
    [Just 2,Nothing,Just 4]
    

    The possibilities go on and on. Basically, it lets you map a function inside two nested functors, and this sort of structure crops up pretty often. If you've ever had a list inside a Maybe, or tuples inside a list, or IO returning a string, or anything like that, you've come across a situation where you could use (.:) = fmap fmap fmap.