Search code examples
haskellrecursionlambdafoldpartial-application

Haskell recursive function example with foldr


I've taken up learning Haskell again, after a short hiatus and I am currently trying to get a better understanding of how recursion and lambda expressions work in Haskell.

In this: YouTube video, there is a function example that puzzles me far more than it probably should, in terms of how it actually works:

firstThat :: (a -> Bool) -> a -> [a] -> a
firstThat f = foldr (\x acc -> if f x then x else acc)

For the sake of clarity and since it wasn't immediately obvious to me, I'll give an example of applying this function to some arguments:

firstThat (>10) 2000 [10,20,30,40] --returns 20, but would return 2000, if none of the values in the list were greater than 10

Please correct me, if my assumptions are wrong.

It seems firstThat takes three arguments:

  1. a function that takes one arguments and returns a Boolean value. Since the > operator is actually an infix function, the first argument in the example above seems the result of a partial application to the > function – is this correct?
  2. an unspecified value of the same type expected as the missing argument to the function provided as the first argument
  3. a list of values of the aforementioned type

But the actual function firstThat seems to be defined differently from its type declaration, with just one argument. Since foldr normally takes three arguments I gathered there is some kind of partial application happening. The lambda expression provided as an argument to foldr seem to be missing its arguments too.

So, how exactly does this function work? I apologize if I am being too dense or fail to see the forest for the trees, but I just cannot wrap my head around it, which is frustrating.

Any helpful explanation or example(s) would be greatly appreciated.

Thanks!


Solution

  • But the actual function firstThat seems to be defined differently from its type declaration, with just one argument. Since foldr normally takes three arguments I gathered there is some kind of partial application happening.

    You are right. However, there is a nicer way of putting it than talking about "missing arguments" -- one that doesn't lead you into asking where they have gone. Here are two ways in which the arguments are not missing.

    Firstly, consider this function:

    add :: Num a => a -> a -> a
    add x y = x + y
    

    As you may know, we can also define it like this:

    add :: Num a => a -> a -> a
    add = (+)
    

    That works because Haskell functions are values like any other. We can simply define a value, add, as being equal to another value, (+), which just happens to be a function. There is no special syntax required to declare a function. The upshot is that writing arguments explicitly is (almost) never necessary; the main reason why we do so because it often makes code more readable (for instance, I could define firstThat without writing the f parameter explicitly, but I won't do so because the result is rather hideous).

    Secondly, whenever you see a function type with three arguments...

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

    ... you can also read it like this...

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

    ... that is, a function of one argument that produces a function of two arguments. That works for all functions of more than one argument. The key takeaway is that, at heart, all Haskell functions take just one argument. That is why partial application works. So on seeing...

    firstThat :: (a -> Bool) -> a -> [a] -> a
    firstThat f = foldr (\x acc -> if f x then x else acc)
    

    ... you can accurately say that you have written explicitly all parameters that firstThat takes -- that is, only one :)


    The lambda expression provided as an argument to foldr seem to be missing its arguments too.

    Not really. foldr (when restricted to lists) is...

    foldr :: (a -> b -> b) -> b -> [a] -> b
    

    ... and so the function passed to it takes two arguments (feel free to add air quotes around "two", given the discussion above). The lambda was written as...

    \x acc -> if f x then x else acc
    

    ... with two explicit arguments, x and acc.