Programming in the Point-Free Style
eiriktsarpalis.wordpress.com
eiriktsarpalis.wordpress.com
In order to be able to read it, you have to know by heart the signature of the standard operators, and then you have to learn on the go the signature of the operators of the particular program your are studying.
It doesn't fly well if the author doesn't follow certain "usability" rules:
- name things thoughtfully and properly,
- no more than 3 arguments for your functions,
- keep the complexity of the functions as low as possible.
This is good advice in any language, but I'd say that with concatenative languages at least, deviations are likely to have more acute negative consequences on maintainability.
I'd probably extend that to things like straightforward partial application as well, but I strongly agree with your implied point that once some new function isn't a relatively simple derivation from some existing function(s), point-free style tends to become a liability. It's cute doing things with weird combinations of operators like (.) and ($), but it's also the Haskell version of writing C code full of one-character identifiers and obscure macros.
Unfortunately, while that tells you how many operands there are, it doesn't help you find them in the surrounding syntax.
In Forth, for instance, the operands of a + could be a pair of immediately preceding constants like 1 2 +. These could be far away in a contrived way: 1 2 [... snip stuff that produces side effects and keeps stack balanced ...] +. Or just due to expression complexity: [30 word expr] [27 word expr] +: the left operand of + is the result of the 30 word expr, which is 27 words away from the + symbol. Or is that 26? 29?
Whenever the literary German dives into a sentence, that is the last you are going to see of him till he emerges on the other side of his Atlantic with his verb in his mouth. - Mark Twain
This is obviously poorly written. "1 2 + [stuff]" is the sane way to write it. Or "2 [stuff] 1 +" if [stuff] takes one parameter and outputs one result.
> Or just due to expression complexity: [30 word expr] [27 word expr] +
Post that on comp.lang.forth, and people will certainly tell you "don't do that" or "why do you that" if it's less trivial than a factoring issue.
In similar situations some people have this trick of separating sub-expressions with double spaces. But that's ignoring the writing on the wall.
What I learned from Forth is that it's true that it has its shortcomings, but I have my own shortcomings too. As Forth is a DIY language, you have to make this distinction in order to improve language/programmer pair.
It's only obvious in the example form which I have. If such a thing occurs in real code, the stuff won't look like [stuff]; it will not stand out.
"Poorly written" happens.
This seems particularly critical. This style works well in Haskell where it's extremely common to have short functions.
I've found that Haskell code often has two kinds of functions: 1-2 line functions (or function cases) that do actual work and computation (often combining other such functions), and 50+ line monadic functions that implement simple sequential routines with very simple control flow (calling other functions for any non-trivial computation). Point-free style works quite well for many of the former.
There's probably a bigger picture problem and fix in that kind of situation, but it is something I think about.
When you write small, single-purpose functions, you can make them fairly self-documenting with the name alone. If you have to look up a one-line function, it probably doesn't have a very clear function or name.
> Worse, people start writing the same functions over and over again.
That one I've definitely run into.
> There's probably a bigger picture problem and fix in that kind of situation, but it is something I think about.
Mechanisms to search functions by type ("find me a function that takes an X and returns a Y") help quite a bit.
For instance, the lazy readTVar, modifyTVar, etc. functions have strict analogs called readTVar' and so on. mapM, foldM, replicateM, and traverse return a monadic value, while mapM_, foldM_, replicateM_, traverse_ and friends discard the return value (giving an m () instead of an m a).
Sometimes, although with languages like Haskell there can be a related problem that accurate names for these small functions wind up being almost as long, or even longer, than their definitions.
It is hard for me to believe that this is a piece of style advice that anyone writing serious software would follow.
If you need to do something wherein the basic task requires 8 arguments' worth of information (which happens A LOT) then trying to factor that into 3-argument pieces is going to give you something Byzantine that is probably also buggy (and it could get extremely heinous, in a way determined by the data dependencies internal to the procedure you are factoring). And if you somehow succeed at all this, congratulations, you just did a bunch of engineering that did not improve the functionality of your software in any way. (In fact it probably made the software take longer to compile).
If doing a certain job needs 8 pieces of information, it needs 8 pieces of information. It doesn't help anyone to try to break that up.
Similarly with this:
keep the complexity of the functions as low as possible
Not really. If you are just factoring some block of complexity in to 4 blocks of less-complexity, well, now you have the same amount of complexity as the original code, plus the complexity of the call graph, and the fact that the person who comes along to read the code will not be able to clearly see the control flow.
There definitely are many cases when factoring a procedure into simpler things is beneficial. But to claim that it's a good idea all the time, or even half the time, is I think mistaken.
(b) This has performance implications, not least because of the ABI. And depending on what language you are using, they can be quite severe (good luck if you are using one of those languages that always puts classes on the heap).
Then you get early warning that you need to be paying attention, rather than silently corrupting your data by swapping some coordinates around.
> And what if your point is 8-dimensional, anyway, what does that constructor look like?
You don't want an 8-dimensional point literal that takes 8 arguments directly, that's never going to be readable. You might want to use a builder. More likely you want to load it from a data file or something on those lines rather than constructing it directly. Where are you even getting these 8-dimensional points from?
> This has performance implications, not least because of the ABI. And depending on what language you are using, they can be quite severe (good luck if you are using one of those languages that always puts classes on the heap).
In Haskell (or indeed in C) it doesn't necessarily have performance implications; an 8-element structure may have exactly the same runtime representation as those 8 elements being passed distinctly.
(In my experience performance concerns are always overblown in any case. If you have actual performance constraints then profile; if you don't, don't worry about it. Too often performance is used as an excuse)
No, a real-world case is that I have a giant program that uses my own geometric primitives, and now I want to start heavily using a library. I know they are just fricking 3D points or quaternions or whatever. Yet because of some weird ideology you want to increase the amount of gruntwork I have to do, and make my life much less pleasant.
You don't want an 8-dimensional point literal that takes 8 arguments directly, that's never going to be readable. You might want to use a builder. More likely you want to load it from a data file or something on those lines rather than constructing it directly. Where are you even getting these 8-dimensional points from?
WHAT ARE YOU TALKING ABOUT
In Haskell (or indeed in C) it doesn't necessarily have performance implications; an 8-element structure may have exactly the same runtime representation as those 8 elements being passed distinctly.
Wow, okay, this conversation is over.
> Wow, okay, this conversation is over.
I'm confused as to why this was a conversation ender.
An 8 element structure using newtypes in haskell would have the same runtime.
For more info, see:
https://skillsmatter.com/skillscasts/5296-safe-zero-cost-coe...
If you want to talk about Haskell, fine ... I don't know anything about Haskell, though, and I am interested in high-performance programming, which is an area where Haskell cannot currently play (nor can any GC'd language). Making claims about how the performance of an operation in a slow language doesn't get any slower under certain circumstances isn't that interesting to me.
If I've misunderstood what's going on with C then by all means tell me what does happen. To the best of my knowledge, if we're talking about 8 same-typed coordinates then there's no alignment/struct-packing issue, so whether you have 8 named doubles (say) or a struct with 8 double members (or indeed an array of 8 doubles), the in-memory representation on the stack is going to be the same; function calling convention is platform-specific but could easily have the same behaviour for the struct vs the named doubles (an array would be different in that case since you can't pass them by value). If that's wrong then talk constructively about what does happen.
> you want to load your 8-dimensional points from a file, where else could you possibly be getting them?
My point wasn't that you want to load them from a file, it's that it would be rare to want to have a point literal directly in your source. But why let that stop you leaping to a good ad hominem?
The opinion that a point-free style is "beautiful code" seems pretty esotheric to me.
https://github.com/tibbe/haskell-style-guide/blob/master/has...
Avoid over-using point-free style. For example, this is hard to read:
-- Bad:
f = (g .) . h
hehtldw; point-free style can lead to readable, generic code -- but any dogmatic adherence to a particular style can have the opposite effect. Use it when it makes your code more readable, avoid it otherwise.
That's probably the most useful approach to point-free programming.
Rigid adherence to rules, styles, paradigms or whatever will generally result in at least some corner cases where adherence leads to worse code.
(Twisted paraphrase of a somewhat famous quote. See http://quoteinvestigator.com/2015/09/09/like-sort/ for more.)
The only way i could really cope was lots of classes. leave the current instance in place while i write a whole new instance with the new approach. Attempting to change an existing instance, like i'd do with a more verbose language, seemed very difficult if not impossible for me.
(fmap . fmap . fmap) toUpper
is a function that goes three levels down and applies f, eg on a 'Maybe (Int, [Char]))' and I find this style easier to read than: fmap (\i_string -> fmap (\string -> fmap (\c -> toUpper c)))
But chaining 'fmap's in this way might be a special idiom that people can get used to, and not say anything about using point-free style in novel situations. The nice thing is that you can mix 'fmap' and eg 'traverse' like this. f (Just (i, cs)) = Just (i, map toUpper cs)
f x = x
Especially because I find the instances that apply fmap to the second item of a two-item tuple confusing.But in general I don't mind that kind of use of '.'; I was talking more about any expression like "g . (f .)" or similarly inscrutable curries.
Yes, some of the instances can be confusing. Adding type signatures afterwards usually clears it up. (The types will tell the reader that for tuples, fmap operates on the second element.)
Yes, "g . (f .)" might be a bit harder to read. Though when I first discovered functor-composition by myself, I was just playing around with exactly that kind of point-less nonsense for fun.
That's because they don't operate on much more than strings.
With sufficient type information reasoning is easy enough that even the compiler can verify correctness.
> The opinion that a point-free style is "beautiful code" seems pretty esotheric to me.
In some way one could say that tacit programming is the interface-oriented programming of the FP-world.
I just had an example where I used tacit programming and had much cleaner code: I had to talk to PostgreSQL through "some" API. For that I need to connect, authenticate and potentially configure the connection. The toplevel function looks like this (in CL):
(defun connect (open authenticate configure)
;;; omitting the (check-type ..)s on the functions here for brevity
(funcall (compose configure authenticate open)))
There is of course a wrapper that creates the 3 functions for you for the regular use cases, but if you want to implement a connection through IP-over-carrier-pigeon or authentication based on DNA samples, you can do so without having to touch the library - just pass your own open/authenticate/configure functions.And the benefits are just the same you have in OOP when using interfaces properly: Decoupling of logic and implementation.
I think we have a different opinion on "easy to reason about". With easy to reason about I mean the time it takes me to understand a function or part of a program. I can write the most cryptic function with cryptic types that is 100% correct but nobody understands.
> And the benefits are just the same you have in OOP when using interfaces properly: Decoupling of logic and implementation.
No, there are no real benefits as it's only a syntactic difference.
Generally, a point-free style should only be "easier to understand" when the point you decide not to mention is completely irrelevant and non-meaningful to understanding the function, but that's not always the case in my opinion.
> No, there are no real benefits as it's only a syntactic difference.
Yes, but an implementation in brainfuck can be derived by pure syntax transformation as well and thus too is "only a syntactic difference".
I personally find it much easier to reason about 6 consecutive lines of code that enforce 4 interfaces by type annotations (the 2 lines quoted plus the 4 lines of type declarations I omitted) than to read through the equivalent in java, which is at least 12 lines in 4 files and requires an abstraction very far away from underlying mathematical principles (the equivalent of my code in Java would require factories of factories and I'd argue that if one considers that easier to read, one has spent too much time in OOP-land).
As others have pointed out, one should definitely not get carried away with it and always evaluate carefully whether it helps clarity. But when the protocol is as simple as "connect, authenticate, configure" then I don't see any reason why I should not write it exactly like this:
(compose configure authenticate connect) connect authenticate configure configure(authenticate(connect(open())));
in a C-style language. Those functions might be returning and accepting all kinds of complex objects or tuples, but you can use purely local reasoning and just treat that expression as a pipeline of black boxes, as long as the inputs and outputs match up.Static types help a lot with this (and I’ve actually been working on a statically typed concatenative language for a while) but even basic arity checking (as in Factor) is enough to make it a very minor issue.
To me, the point is not that you should avoid local variables entirely. Rather, it’s that most code doesn’t actually need local variables to be perfectly readable, because the most common dataflow patterns in regular business logic can be captured by simple composition of functions.
1. Escaping parenthesis hell: sometimes you can escape an unreasonable amount of parenthesis nesting by using (some) point-free programming.
2. The same thing that you'd use cascades in Smalltalk for: repeated applications of operations to the same object without having to name the object over and over again. (Obviously, in a functional language, it may not strictly be the "same" object, but conceptually, it's the same pattern.)
3. Combinator libraries (mentioned by the article itself). Here, the functions being composed are really meant to be (mostly) opaque entities and the fact that you're using a form of function composition is an implementation detail.
Why would I want to? Parenthesis make precedence explicit.
That's the whole point that justifies not going for S-Expression syntax for every programming language in the first place.
I use this to modify the parameters of the composite geometric transformations of fractals. This allows me to generate animations for any kind of fractals without having to reimplement the animation code for each fractal : https://scientific-coder.github.io/Playground/2017-03-20-fra... (cf stepify multi-method). This would be useful for web servers in Clojure as we could have our cake and eat it too : while currently one has to pick either easy with ring handlers that are simple function compositions and vectors of interceptors for pedestal that can be modified at runtime.
It feels to me like there's this weird disconnect between people who talk about beautiful code and those who don't. For me, the aesthetic criteria that would lead me to call a piece of code beautiful are things like clarity, ease of understanding and simplicity. Writing code in a certain way might even elucidate the structure of the problem for the reader.
Now, concision is definitely something that matters to me aesthetically, but if it comes at the cost of clarity, it just isn't worth it. I would consider very concise but complex and inscrutable point free code to be ugly.
It definitely is my opinion that much of the time point free code is beautiful by these criteria. It's also often the case that I find point free code ugly. The thing that makes it difficult is that whether code is readable is heavily subjective. It very much depends on the familiarity of the reader with the functions and combinators and constructs that are in use. And of course people differ on the level of fluency in the language and language constructs that should be expected of the reader.
def foo(whatever: Int) = quxxl(bar(whatever))
val foo = bar andThen quxxl
Naming "whatever" just gets in the way of making it clear what foo actually does.> The point of this exercise is to illustrate how the lambda calculus with pattern matching (or set theory for that matter) is superior to the point-free style in terms of expressive efficiency.
ELI5: What's the connection between your comment and JadeNB's?
So the commentariate has decided to talk about point free programming in general instead of engaging the article's very narrow and unsubstantiated point.
Function defined here, stored in xform_listed_quote_f global:
http://www.kylheku.com/cgit/txr/tree/eval.c?id=6ca6be767f8ac...
Used as a mapping function here:
http://www.kylheku.com/cgit/txr/tree/eval.c?id=6ca6be767f8ac...
I think perhaps Maths people need to seek more consensus on terminology.
All the other terms here (abstract functions, origin, destination, argument type, return type) appear to be the author's attempt to make the subject understandable to programmers.
In Graph Theory the terminology is Node and Vertex.
Arrow, Source and Target are from Category Theory.
In CT you usually are dealing with morphisms, but not always and that intuition can get in your way. For this reason, arrow is sometimes used instead to indicate something a little more abstract than morhism.
If only they could follow the example of programmers, who never have two different words for identical (or, worse, near-identical) concepts!
- Morphism (Domain, Codomain)
- Arrow (Source, Target)
The reason for the last one is that you may find "Arrows" that aren't functions. (Such as an order relationship in the Poset category)
"Point-Free or Die: Tacit Programming in Haskell and Beyond" by Amar Shah https://www.youtube.com/watch?v=seVSlKazsNk
But why? If a point-free style is more readable, then by all means use it, but if you need an automated tool to do the translation for you, then that seems unlikely. If it's an efficiency question, then your compiler can, and (given the trickiness of GHC in particular) probably will, do anything that an external automated tool can.
If it is purely an intellectual exercise, to see how else the code could theoretically be written, then I totally see the point. (I am a mathematician, after all!)
On the other hand, if it is code that you actually use in production, then surely the old dictum that I will misquote, for want of looking it up, as "if you write the cleverest code possible, then you are not clever enough to debug it", applies all the more so to say "if you aren't clever enough to write your own code, then you certainly aren't clever enough to debug it"! (Not you personally, I mean; just a comment on programmer-facing auto-generated code.)
(((.) . (,)) .) f x g y
(((.) . (,)) . f) x g y
(((.) . (,)) (f x)) g y
((.) ((,) (f x))) g y
((.) (f x,)) g y
((f x,) . g) y
(f x, g y)This example is totally absurd, since you’d just write “(f x, g y)” and call it a day, but you can extract a fully point-free version that’s somewhat readable.
f = pairwise $ curry $ apply *** apply
where
pairwise = fcurry fcurry
fcurry = fmap curry
apply = uncurry ($)And this is a tool to do just that.
I find the pointfree style is usually easier to read but the pointful style is often easier to write. So I tend to write pointful first and then translate into pointfree. An automated tool can help with that.
Some code can seem extremely elegant, but mostly it's just clever, and an exercise in showing off how smart you are and how much you can keep in your head.
And with all due respect to Joe Condon: Eschew clever code.
When reading (eg) nested for loops with conditions, I have to push a ton of context variables into my head, and then evolve them over iterations if I'm doing a close read. That's a lot of ask of my head.
But in the point-free style, each compositional unit tends to be small enough to be "obviously no bugs" vs. "no obvious bugs".
Not really what I'd expect from a HN link with so many upvotes. Did no one else experience this?
As with most languages it comes down to the programmer.