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:
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:
f = \x → \y → (-) 3 . (/) x y - Meaning y “gets applied” first and x after, so I should use flip.
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.
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-) .) (/)
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/)
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!
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.)
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.
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.