13

I am trying to convert the following Haskell code to point free style, to no avail.

bar f g xs = filter f (map g xs )

I'm new to Haskell and any help would be great.

Will Ness
  • 70,110
  • 9
  • 98
  • 181
Lee
  • 139
  • 8
  • 6
    Beware that in some cases (like this) the point free version can be much less readable than the original code. Unless you are aiming for obfuscated code, use point free style with some care. – chi Apr 13 '15 at 07:08

3 Answers3

49

Converting to pointfree style can be done entirely mechanically, though it's hard without being comfortable with the fundamentals of Haskell syntax like left-associative function application and x + y being the same as (+) x y. I will assume you are comfortable with Haskell syntax; if not, I suggest going through the first few chapters of LYAH first.

You need the following combinators, which are in the standard library. I have also given their standard names from combinator calculus.

id :: a -> a                                   -- I
const :: a -> b -> a                           -- K
(.) :: (b -> c) -> (a -> b) -> (a -> c)        -- B
flip :: (a -> b -> c) -> (b -> a -> c)         -- C
(<*>) :: (a -> b -> c) -> (a -> b) -> (a -> c) -- S

Work with one parameter at a time. Move parameters on the left to lambdas on the right, e.g.

f x y = Z

becomes

f = \x -> \y -> Z

I like to do this one argument at a time rather than all at once, it just looks cleaner.

Then eliminate the lambda you just created according to the following rules. I will use lowercase letters for literal variables, uppercase letters to denote more complex expressions.

  1. If you have \x -> x, replace with id
  2. If you have \x -> A, where A is any expression in which x does not occur, replace with const A
  3. If you have \x -> A x, where x does not occur in A, replace with A. This is known as "eta contraction".
  4. If you have \x -> A B, then
    1. If x occurs in both A and B, replace with (\x -> A) <*> (\x -> B).
    2. If x occurs in just A, replace with flip (\x -> A) B
    3. If x occurs in just B, replace with A . (\x -> B),
    4. If x does not occur in either A or B, well, there's another rule we should have used already.

And then work inward, eliminating the lambdas that you created. Lets work with this example:

f x y z = foo z (bar x y)
-- Move parameter to lambda:
f x y = \z -> foo z (bar x y)
-- Remember that application is left-associative, so this is the same as
f x y = \z -> (foo z) (bar x y)
-- z appears on the left and not on the right, use flip
f x y = flip (\z -> foo z) (bar x y)
-- Use rule (3) 
f x y = flip foo (bar x y)

-- Next parameter
f x = \y -> flip foo (bar x y)
-- Application is left-associative
f x = \y -> (flip foo) (bar x y)
-- y occurs on the right but not the left, use (.)
f x = flip foo . (\y -> bar x y)
-- Use rule 3
f x = flip foo . bar x

-- Next parameter
f = \x -> flip foo . bar x
-- We need to rewrite this operator into normal application style
f = \x -> (.) (flip foo) (bar x)
-- Application is left-associative
f = \x -> ((.) (flip foo)) (bar x)
-- x appears on the right but not the left, use (.)
f = ((.) (flip foo)) . (\x -> bar x)
-- use rule (3)
f = ((.) (flip foo)) . bar
-- Redundant parentheses
f = (.) (flip foo) . bar

There you go, now try it on yours! There is not really any cleverness involved in deciding which rule to use: use any rule that applies and you will make progress.

luqui
  • 59,485
  • 12
  • 145
  • 204
  • 12
    This is the first time I've seen anybody post the actual *algorithm* for doing this. I knew there must be one, but I didn't know precisely what it is... – MathematicalOrchid Apr 13 '15 at 08:29
  • 1
    I've seen several textbooks that struggled to present the conversion as clearly as you have done! (just a side note, the last expression is usually seen as `(flip foo .) . bar`). – Will Ness Jul 03 '15 at 22:48
  • 4
    For reference, I'd like to add that this is (mostly) [converting from lambda calculus into SK calculus](https://en.wikipedia.org/wiki/Combinatory_logic#Conversion_of_a_lambda_term_to_an_equivalent_combinatorial_term) (just in case someone wants to google more about it). – phipsgabler Feb 23 '16 at 14:20
  • "use any rule that applies" if more than one were to apply, it'd introduce nondeterminism and the "cleverness" of making the "best" choice. for instance you've excluded **W**xy = xyy here. – Will Ness Jun 17 '20 at 09:11
8

Both of the existing answers don't really answer your specific question in a way that's elucidating: one is "here are the rules, work it out for yourself" and the other is "here is the answer, no information about how the rules generate it."

The first three steps are really easy and consist in removing a common x from something of the form h x = f (g x) by writing h = f . g. Essentially it's saying "if you can write the thing in the form a $ b $ c $ ... $ y $ z and you want to remove the z, change all the dollars to dots, a . b . c . ... . y:

bar f g xs = filter f (map g xs)
           = filter f $ (map g xs)
           = filter f $ map g $ xs -- because a $ b $ c == a $ (b $ c).
bar f g    = filter f . map g
           = (filter f .) (map g)
           = (filter f .) $ map $ g
bar f      = (filter f .) . map

So this last f is the only tricky part, and it's tricky because the f is not at the "end" of the expression. But looking at it, we see that this is a function section (. map) applied to the rest of the expression:

bar f = (.) (filter f) . map
bar f = (. map) $ (.) $ filter $ f
bar   = (. map) . (.) . filter

and that's how you reduce an expression when you don't have complicated things like f x x and the like appearing in it. In general there is a function flip f x y = f y x which "flips arguments"; you can always use that to move the f to the other side. Here we have flip (.) map . (.) . filter if you include the explicit flip call.

Will Ness
  • 70,110
  • 9
  • 98
  • 181
CR Drost
  • 9,637
  • 1
  • 25
  • 36
6

I asked lambdabot, a robot who hangs out on various Haskell IRC channels, to automatically work out the point-free equivalent. The command is @pl (pointless).

10:41 <frase> @pl bar f g xs = filter f (map g xs )
10:41 <lambdabot> bar = (. map) . (.) . filter

The point free version of bar is:

bar = (. map) . (.) . filter

This is arguably less comprehensible than the original (non-point-free) code. Use your good judgement when deciding whether to use point-free style on a case-by-case basis.

Finally, if you don't care for IRC there are web-based point-free converters such as pointfree.io, the pointfree command line program, and other tools.

dfeuer
  • 48,079
  • 5
  • 63
  • 167
frasertweedale
  • 5,424
  • 3
  • 26
  • 38
  • 4
    If you don't want to fire up an IRC client, I wrote a web service that does these conversions for you. It's called Blunt: https://blunt.herokuapp.com/#input=bar%20f%20g%20xs%20%3D%20filter%20f%20(map%20g%20xs) – Taylor Fausak Apr 13 '15 at 04:49
  • 3
    You can also install the command line version using `cabal install pointfree`. If you then give it its own ghci definition you could use it from ghci :) https://hackage.haskell.org/package/pointfree – Jxek Apr 13 '15 at 07:51
  • @TaylorFausak, is Blunt dead, or just moved? – dfeuer Jun 16 '20 at 22:13
  • @dfeuer it's dead. – Taylor Fausak Jun 17 '20 at 23:50