73

I know that in some languages (Haskell?) the striving is to achieve point-free style, or to never explicitly refer to function arguments by name. This is a very difficult concept for me to master, but it might help me to understand what the advantages (or maybe even disadvantages) of that style are. Can anyone explain?

s952163
  • 6,276
  • 4
  • 23
  • 47
Eli Schneider
  • 4,903
  • 3
  • 28
  • 50

4 Answers4

79

The point-free style is considered by some author as the ultimate functional programming style. To put things simply, a function of type t1 -> t2 describes a transformation from one element of type t1 into another element of type t2. The idea is that "pointful" functions (written using variables) emphasize elements (when you write \x -> ... x ..., you're describing what's happening to the element x), while "point-free" functions (expressed without using variables) emphasize the transformation itself, as a composition of simpler transforms. Advocates of the point-free style argue that transformations should indeed be the central concept, and that the pointful notation, while easy to use, distracts us from this noble ideal.

Point-free functional programming has been available for a very long time. It was already known by logicians which have studied combinatory logic since the seminal work by Moses Schönfinkel in 1924, and has been the basis for the first study on what would become ML type inference by Robert Feys and Haskell Curry in the 1950s.

The idea to build functions from an expressive set of basic combinators is very appealing and has been applied in various domains, such as the array-manipulation languages derived from APL, or the parser combinator libraries such as Haskell's Parsec. A notable advocate of point-free programming is John Backus. In his 1978 speech "Can Programming Be Liberated From the Von Neumann Style ?", he wrote:

The lambda expression (with its substitution rules) is capable of defining all possible computable functions of all possible types and of any number of arguments. This freedom and power has its disadvantages as well as its obvious advantages. It is analogous to the power of unrestricted control statements in conventional languages: with unrestricted freedom comes chaos. If one constantly invents new combining forms to suit the occasion, as one can in the lambda calculus, one will not become familiar with the style or useful properties of the few combining forms that are adequate for all purposes. Just as structured programming eschews many control statements to obtain programs with simpler structure, better properties, and uniform methods for understanding their behavior, so functional programming eschews the lambda expression, substitution, and multiple function types. It thereby achieves programs built with familiar functional forms with known useful properties. These programs are so structured that their behavior can often be understood and proven by mechanical use of algebraic techniques similar to those used in solving high school algebra problems.

So here they are. The main advantage of point-free programming are that they force a structured combinator style which makes equational reasoning natural. Equational reasoning has been particularly advertised by the proponents of the "Squiggol" movement (see [1] [2]), and indeed use a fair share of point-free combinators and computation/rewriting/reasoning rules.

Finally, one cause for the popularity of point-free programming among Haskellites is its relation to category theory. In category theory, morphisms (which could be seen as "transformations between objects") are the basic object of study and computation. While partial results allow reasoning in specific categories to be performed in a pointful style, the common way to build, examine and manipulate arrows is still the point-free style, and other syntaxes such as string diagrams also exhibit this "pointfreeness". There are rather tight links between the people advocating "algebra of programming" methods and users of categories in programming (for example the authors of the banana paper [2] are/were hardcore categorists).

You may be interested in the Pointfree page of the Haskell wiki.

The downside of pointfree style is rather obvious: it can be a real pain to read. The reason why we still love to use variables, despite the numerous horrors of shadowing, alpha-equivalence etc., is that it's a notation that's just so natural to read and think about. The general idea is that a complex function (in a referentially transparent language) is like a complex plumbing system: the inputs are the parameters, they get into some pipes, are applied to inner functions, duplicated (\x -> (x,x)) or forgotten (\x -> (), pipe leading nowhere), etc. And the variable notation is nicely implicit about all that machinery: you give a name to the input, and names on the outputs (or auxiliary computations), but you don't have to describe all the plumbing plan, where the small pipes will go not to be a hindrance for the bigger ones, etc. The amount of plumbing inside something as short as \(f,x,y) -> ((x,y), f x y) is amazing. You may follow each variable individually, or read each intermediate plumbing node, but you never have to see the whole machinery together. When you use a point-free style, all the plumbing is explicit, you have to write everything down, and look at it afterwards, and sometimes it's just plain ugly.

PS: this plumbing vision is closely related to the stack programming languages, which are probably the least pointful programming languages (barely) in use. I would recommend trying to do some programming in them just to get of feeling of it (as I would recommend logic programming). See Factor, Cat or the venerable Forth.

gasche
  • 31,259
  • 3
  • 78
  • 100
  • " When you use a point-free style, it's all explicit," → Don't you mean _pointful_ here? Alternatively: _implicit_? – Magne Dec 09 '20 at 13:05
  • I think the sentence as-is is correct. In point-free style, you have to be very explicit about the flow of value from inputs to outputs in the function, while the pointful style relies on names to avoid this. For example there is no mark that `x` and `y` are duplicated in the right-hand-side, they just appear twice. If you try to implement this function in point-free style, you are going to see how much more explicit you have to be about it. – gasche Dec 10 '20 at 15:15
  • I'm still a bit confused by that whole paragraph, since you earlier wrote `The idea is that "pointful" functions (written using explicit variables)`.. – Magne Dec 10 '20 at 20:04
  • 1
    Yes: when you have variables, variables are explicit, but then the data-flow plumbing is implicit. In pointfree styles, there are no variables, but the plumbing has to be made explicit. (Edit: I removed the "explicit" in the formulation you cited to avoid the confusion, thanks.) – gasche Dec 12 '20 at 08:04
  • Thanks for clarifying. Is it correct to understand that the second-to-last paragraph starts by mentioning _point-free_-style, but then mainly talks about _pointful_-style, before ending with describing _point-free_-style? If so, then I would perhaps make the context switches clearer (e.g. `The general idea ...` of what?), or split the paragraph up. To avoid confusion. – Magne Dec 12 '20 at 13:25
  • For now I prefer to leave the paragraph as it is. – gasche Dec 13 '20 at 13:16
59

I believe the purpose is to be succinct and to express pipelined computations as a composition of functions rather than thinking of threading arguments through. Simple example (in F#) - given:

let sum = List.sum
let sqr = List.map (fun x -> x * x)

Used like:

> sum [3;4;5]
12
> sqr [3;4;5]
[9;16;25]

We could express a "sum of squares" function as:

let sumsqr x = sum (sqr x)

And use like:

> sumsqr [3;4;5]
50

Or we could define it by piping x through:

let sumsqr x = x |> sqr |> sum

Written this way, it's obvious that x is being passed in only to be "threaded" through a sequence of functions. Direct composition looks much nicer:

let sumsqr = sqr >> sum

This is more concise and it's a different way of thinking of what we're doing; composing functions rather than imagining the process of arguments flowing through. We're not describing how sumsqr works. We're describing what it is.

PS: An interesting way to get your head around composition is to try programming in a concatenative language such as Forth, Joy, Factor, etc. These can be thought of as being nothing but composition (Forth : sumsqr sqr sum ;) in which the space between words is the composition operator.

PPS: Perhaps others could comment on the performance differences. It seems to me that composition may reduce GC pressure by making it more obvious to the compiler that there is no need to produce intermediate values as in pipelining; helping make the so-called "deforestation" problem more tractable.

AshleyF
  • 3,682
  • 1
  • 25
  • 23
  • 12
    The part about improved compilation is not true at all. In most languages, point-free style will actually decrease performances. Haskell relies heavily on optimizations precisely because it's the only way to make the cost of these things bearable. At best, those combinators are inlined away and you get an equivalent pointful version. – gasche Apr 15 '11 at 22:31
  • 2
    What I meant by "deforestation" reducing GC pressure is that the compiler could avoid allocating intermediate values (e.g. the list from `sqr`) when it's clear that it's merely being passed to `sum` to construct the result; taking function composition as a _hint_ to do it. `List.sum` is really `List.fold (+) 0` or `List.fold (fun s x -> s + x)`. Composing with the map is: `List.map (fun x -> x * x) >> List.fold (fun s x -> s + x)` or could be fused into one: `List.fold (fun s x -> s + x * x) 0`, avoiding allocations. See: https://link.springer.com/content/pdf/10.1007/3-540-19027-9_23.pdf – AshleyF Mar 19 '18 at 19:20
10

While I'm attracted to the point-free concept and used it for some things, and agree with all the positives said before, I found these things with it as negative (some are detailed above):

  1. The shorter notation reduces redundancy; in a heavily structured composition (ramda.js style, or point-free in Haskell, or whatever concatenative language) the code reading is more complex than linearly scanning through a bunch of const bindings and using a symbol highlighter to see which binding goes into what other downstream calculation. Besides the tree vs linear structure, the loss of descriptive symbol names makes the function hard to intuitively grasp. Of course both the tree structure and the loss of named bindings also have a lot of positives as well, for example, functions will feel more general - not bound to some application domain via the chosen symbol names - and the tree structure is semantically present even if bindings are laid out, and can be comprehended sequentially (lisp let/let* style).

  2. Point-free is simplest when just piping through or composing a series of functions, as this also results in a linear structure that we humans find easy to follow. However, threading some interim calculation through multiple recipients is tedious. There are all kinds of wrapping into tuples, lensing and other painstaking mechanisms go into just making some calculation accessible, that would otherwise be just the multiple use of some value binding. Of course the repeated part can be extracted out as a separate function and maybe it's a good idea anyway, but there are also arguments for some non-short functions and even if it's extracted, its arguments will have to be somehow threaded through both applications, and then there may be a need for memoizing the function to not actually repeat the calculation. One will use a lot of converge, lens, memoize, useWidth etc.

  3. JavaScript specific: harder to casually debug. With a linear flow of let bindings, it's easy to add a breakpoint wherever. With the point-free style, even if a breakpoint is somehow added, the value flow is hard to read, eg. you can't just query or hover over some variable in the dev console. Also, as point-free is not native in JS, library functions of ramda.js or similar will obscure the stack quite a bit, especially with the obligate currying.

  4. Code brittleness, especially on nontrivial size systems and in production. If a new piece of requirement comes in, then the above disadvantages get into play (eg. harder to read the code for the next maintainer who may be yourself a few weeks down the line, and also harder to trace the dataflow for inspection). But most importantly, even something seemingly small and innocent new requirement can necessitate a whole different structuring of the code. It may be argued that it's a good thing in that it'll be a crystal clear representation of the new thing, but rewriting large swaths of point-free code is very time consuming and then we haven't mentioned testing. So it feels that the looser, less structured, lexical assignment based coding can be more quickly repurposed. Especially if the coding is exploratory, and in the domain of human data with weird conventions (time etc.) that can rarely be captured 100% accurately and there may always be an upcoming request for handling something more accurately or more to the needs of the customer, whichever method leads to faster pivoting matters a lot.

Robert Monfera
  • 1,980
  • 1
  • 22
  • 16
  • 3
    In regards to point #3, `const tap = x => (console.log(x), x);` will spare you much, much pain (not entirely painless though). – Jared Smith Oct 18 '17 at 16:34
  • 2
    everybody resorts to using tap esp. with observables, but it's something you need to add and then remove, while in a series of `const` bindings you just click on the line in dev tools - but ofc the big price is it's not point-free – Robert Monfera Oct 19 '17 at 15:57
  • Then put the call on it's own line and use a preprocessor directive or some other build step to remove it for non-dev builds. That's clunky enough that I wouldn't call it a "solved problem", but it's not terribly difficult, I'd be willing to bet that my JS codebase is littered with commented-out calls to `tap`. – Jared Smith Oct 19 '17 at 16:41
  • 1
    This is a truly great and informative answer, with points not often talked about. – Magne Dec 09 '20 at 13:46
-2

To the pointfree variant, the concatenative programming language, i have to write:
I had a little experience with Joy. Joy is a very simple and beautiful concept with lists. When converting a problem into a Joy function, you have to split your brain into a part for the stack plumbing work and a part for the solution in the Joy syntax. The stack is always handled from the back. Since the composition is contained in Joy, there is no computing time for a composition combiner.

fpstefan
  • 1
  • 1