Point-free notation

Hi,
I’ve been learning Haskell for about two months now, and I am just now trying to understand and fully grasp how point-free notation works. I am only doing basic exercises for the time being as my knowledge is still lacking and I have found myself struggling with the order the arguments are being used.

Let’s use the function f as an example. My exercise tells me to write the following function f point-free without using any lambdas, list comprehensions, enumerations, where, or ler clauses.
f x y = 3 - x / y.

I have come up with multiple different ways of solving this, but I do not know which one/ ones are correct.

Here you have my step-by-step thinking:

  1. f x y = 3 - x / y = (-) 3 $ (/) x y = (-) 3 . (/) x y - Here is where I get particularily confused. In what order do x and y “get applied” when using point-free? To my knowledge, it follows as bellow:

  2. f = \x → \y → (-) 3 . (/) x y - Meaning y “gets applied” first and x after, so I should use flip.

  3. f = (-) 3 . flip (/) = (3 - ) . flip (/) - This to me seems like the correct answer and is the “most logical” to come up with, however the answer sheet I am given says the following:

f = (/).(3-)
{- THIS ANSWER IS WRONG! THE CORRECT ONE BELOW: -}
f = (.) ((3-) .) (/)

What are the steps to conclude the correct answer that is given? I think I understand that the one above seems wrong as when I apply y first and then x I get (3-y)/x, but here I “disprove” my assumption from before. Now I apply y to the function (3-) and then apply x to the function (/) (3-y). So once again, what is the correct order to apply the arguments? Is the rule of thumb “Apply until the function doesn’t take any more arguments”, which would explain why flip takes both x and y?

I truly appreciate all the help I can get to solve this mystery.

// Robotamski

1 Like

Here is my way of making it point free. There are several “correct” versions I would say, but the last line is the one your answer sheet says is correct.

I implicitly insert $ rather than parentheses.

f x y = 3 - x / y
{- prefix / -}
f x y = 3 - ((/) x y)
{- prefix - -}
f x y = (-) 3 $ (/) x y
{- eta reduce -}
f x = (-) 3 . (/) x
{- prefix . -}
f x = (.) ((-) 3) $ (/) x
{- eta reduce -}
f = ((.) ((-) 3)) . (/)
{- prefix . -}
f = ((.) ((-) 3)) . (/)
{- prefix . -}
f = (.) ((.) ((-) 3)) (/)
{- to `(-) 3` to `(3-)` -}
f = (.) ((.) (3-)) (/)
{- infix . -}
f = (.) ((3-) .) (/)
2 Likes

Thanks a lot for the solution. Makes it easy to follow all the steps!
I am, however, wondering if it even is necessary to go from step 6, f = ((.) ((-) 3)) . (/) to the last one, as it is already made point-free? Seems like multiple extra steps for it to be “a little more readable”

Defintely not! I only included the extra steps to turn the expression into the one your answer sheet states as the correct one.

If I had to write this function point-free I would probably choose: ((3-) .) . (/), although, for readability’s sake, we really should introduce at least one variable: f x = (3-) . (x/)

1 Like

another approach is to notice the pattern occurring here. you’re trying to apply 2 arguments to a function (i.e. (/)) then pass it’s result to another function (i.e. (3 -)). so we can pull that structure out into a helper:

f ... g = \x y -> f (g x y)
f = (3 -) ... (/)

try and write out the type of (...)! you also don’t have to stop at double composing – you can keep going. this is actually a family of functions and closely related are fmap . fmap, traverse . traverse, etc.. play around with them!

edit: use points to avoid connotations

3 Likes

Btw, while these point-free exercises can be fun puzzles when someone is learning Haskell, please don’t ever write code like this in an actual program :wink:

4 Likes

Same, or fmap (3 -) . (/). The job of composition (.) here is to “map under” the input, so it’s being used as (b -> c) -> ((a ->) b) -> ((a ->) c), which is fmap @(a ->) @b @c. Though aside I think sadly you still have to write (->) a instead of (a ->).

Another idiom for this is curry ((3 -) . uncurry (/)) to locally group values into a tuple when you want to work with them together in a pipeline. The combinators in Control.Arrow are very handy for that kind of thing.

And the idea is name code, not data — so don’t forget to name code! Pointfree style is way more readable and reusable when you take advantage of the fact that composition lets you easily cut & paste functions out of a pipeline into small definitions with meaningful names. If you just inline everything, you miss a lot of the benefits.

Everything in moderation! Often the definition can be clearer than a name.

addThree :: Int -> Int
addThree = (+ 3)

My advice to anyone who wrote the above would be to get more familiar with the expression (+ 3), so that they don’t feel compelled to give it a new name. (+ 3) is already perfectly descriptive when you’re fluent! (It’s understandable not to be fluent yet when you’re learning—but named minidefinitions like this can be like training wheels that ultimately slow you down on your way to fluency.)

As a slightly more advanced example, one of my favorite point-free tricks is turning this:

\x -> someFn x + anotherFn x

into this:

liftA2 (+) someFn anotherFn

Like (+ 3), liftA2 (+) is already totally descriptive of what it does and doesn’t need a name—once you’ve reached enlightenment about Applicative ((->) a), that is! Don’t deny yourself the enlightenment by avoiding thinking about it! The absence of clarity is a temporary state in the reader, not an immutable quality of the code.

(Everyone has their limits, though. I don’t particularly enjoy playing tricks with too many nested (.)s. Probably there are seasoned Haskellers here who would turn up their nose at liftA2 (+). This is just my opinion.)

3 Likes

Yes, that’s essentially just the same name twice. Extracting into a named definition adds information when the name is non-trivially related to the definition, for example:

adjustToFitWomble :: Int -> Int
adjustToFitWomble = (+ 3)

Then we’ve asserted a property of the size of a womble, and a womble domain expert can check by looking at this definition whether we have implemented it correctly.

4 Likes

Agreed—that’s name-your-CAFs as an extension of name-your-constants.

(In this specific case, I’d argue naming your constants is probably better if you have womble domain experts to whom the number 3 is significant.)

wombleMargin :: Int
wombleMargin = 3

(And then you’re back to (+ wombleMargin) not needing its own name.)

1 Like

Not that it is of any relevance, but this operator gained some prominence due to its resemblance of certain mammalian body parts, as well as due to its usefulness.
Apparently there is even a theorem of the same name.

that’s kind of gross, tbh

True. I mentioned it only because once you know that name, it is more likely that you will remember the pattern when you need it.

1 Like

I’d really prefer this conversation ended. I’ll write the operator with points going forward to avoid this discussion.

1 Like

(c -> d) -> (a -> b -> c) -> a -> b -> d is the blackbird combinator of Raymond Smullyan fame Data.Aviary.Birds

blackbird :: (c -> d) -> (a -> b -> c) -> a -> b -> d
blackbird f g x y = f (g x y)

More useful is the on combinator from Data.Function which applies a binary operation to two arguments after passing each through a unary operation

1 Like