Home > Enterprise >  Is it possible to eta (or otherwise) reduce arguments if I depend on them?
Is it possible to eta (or otherwise) reduce arguments if I depend on them?

Time:07-01

I was solving a super simple problem and came out with this code. I feel like there should be a way to eliminate arguments x and y in areaOrPerimeter. But I can't find a way to do it. Is it possible to get rid of them somehow? I have a strong feeling that it should be possible.

areaOrPerimeter :: Double -> Double -> Double
areaOrPerimeter x y
  | x == y = area x y 
  | otherwise = perimeter x y
  

area :: Num a => a -> a -> a
area = (*)

perimeter :: Num a => a -> a -> a
perimeter x y = 2 * (x   y)

CodePudding user response:

If you want to have a chance of doing that in a non-obnoxious way, you should first group together the arguments meaningfully to avoid having to deal with multiple parameters:

data RectangleSize a = RectangleSize {width, height :: a}

Then we can make the functions and the x==y check more meaningful too:

area :: Num a => RectangleSize a -> a
area (RectangleSize w h) = w*h

perimeter :: Num a => RectangleSize a -> a
perimeter (RectangleSize w h) = 2*(w h)

isSquare :: Eq a => RectangleSize a -> Bool
isSquare (RectangleSize w h) = w==h

That leaves us with the already more promising

areaOrPerimeter :: RectangleSize Double -> Double
areaOrPerimeter r
  | isSquare r  = area r 
  | otherwise   = perimeter r

To make that point-free, you still need to copy around the r three times. This can be done (whether it should is another matter) with the Applicative instance of functions, namely \x -> f x (g x) can be written as f<*>g. In this case, you want to make a decision, so you require a first-class if; typically that's written

if' :: Bool -> a -> a -> a
if' True x _ = x
if' False _ y = y

Now you can upgrade isSquare to a switching function by composing with if':

    if' . isSquare :: Eq a => RectangleSize a -> b -> b -> b

And now we can use the applicative instance to distribute the rectangle to all components:

areaOrPerimeter = if'.isSquare <*> area <*> perimeter

Actually not too shabby IMO.

CodePudding user response:

To chain two single parameter functions f and g, . is pretty forward. To chain g :: c -> d and f :: a -> b -> c, use (g .) . f. That way, you can write perimeter as

perimeter = ((2*) .) . ( )

one parameter can be removed with <*>, do that twice for two parameters:

import Control.Applicative(liftA2)
import Data.Bool(bool)

areaOrPerimeter :: Double -> Double -> Double
areaOrPerimeter = bool <<$>> (*) <<*>> ((2*) .) . ( ) <<*>> (==)
   where infixl 4 <<*>>
         (<<*>>) = liftA2 (<*>)
         infixl 5 <<$>>
         (<<$>>) = fmap . fmap

Writing point-free is a nice exercise, but don't expect to understand this in 6 months from now.

  • Related