Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How does the point-free expression ((*) .) . (*) work in Haskell?

I'm learning about Haskell and came across this concise, but weird, definition for a function that multiplies three numbers:

volume :: Float -> Float -> Float -> Float
volume = ((*) .) . (*)

I can see from the type signature and by testing it in GHCi that it correctly calculates the product of three numbers, essentially \l w h -> l * w * h.

What I Understand

  • I know that . is the function composition operator, where (f . g) x is equivalent to f (g x).

  • I understand that partial application works like (*) 2, which returns a new function that multiplies its argument by 2.

Where I'm Stuck

My confusion comes from the ((*) .) part of the expression. It looks like the composition operator . is being partially applied, which is bending my brain a little.

I tried to break it down by applying arguments one by one:

  1. volume l w h = ((*) .) . (*) l w h

  2. This becomes ((*) .) ((* l)) w h because of how . works.

  3. Let multByL = (* l). The expression is now ((*) .) multByL w h.

This is where I get lost. I'm not sure how to evaluate ((*) .) multByL. How does a partially applied composition operator work here? What does it return and how does it then interact with the remaining arguments w and h? I'm not very familiar with the operation order. Maybe area = multByL w will be calculated first? But even in this way, I still get confused while encountering ((*) .) area h. There is only one function, shouldn't composition operator work with two functions?

My Questions

  1. What exactly is the function ((*) .)? What is its type, and what does it do?

  2. Is there a more intuitive way to "read" this expression from left to right to understand its meaning?

Any help in demystifying this clever piece of code would be greatly appreciated!
Thank you!!!

like image 554
zahiko Avatar asked Oct 30 '25 18:10

zahiko


1 Answers

A quick comment: (*) l becomes (l*), not (*l). For most number types, this distinction doesn't matter.

Generally, an op section has the following semantics:

  • (op) means \x y -> x op y
  • (e op) means \x -> e op x
  • (op e) means \x -> x op e

So let's start from your expression:

((*) .) (l*) w h
= { expand the `(e .)` section with `e = (*)` }
(\x -> (*) . x) (l*) w h
= { beta reduce }
((*) . (l*)) w h
= { definition of `.` }
(*) ((l*) w) h
= { expand the `(e *)` section with `e = l` }
(*) ((\x -> l*x) w) h
= { beta reduce }
(*) (l*w) h
= { expand the `(*)` section }
(\x y -> x*y) (l*w) h
= { beta reduce }
(l*w)*h

Actually, to be fully precise, the semantics of the two sections (op) and (e op) should not be the lambda expressions I showed, but their eta reduction. But it's pretty rare for that difference to matter, so if you didn't understand that, feel free to ignore it -- by the time it's important, you'll likely know it already.

like image 142
Daniel Wagner Avatar answered Nov 02 '25 12:11

Daniel Wagner



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!