The opinion that a point-free style is "beautiful code" seems pretty esotheric to me.
The opinion that a point-free style is "beautiful code" seems pretty esotheric to me.
tldw; 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.
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.
(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.
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
hehThe 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.
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.
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.
(Twisted paraphrase of a somewhat famous quote. See http://quoteinvestigator.com/2015/09/09/like-sort/ for more.)
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.