Tacit programming
en.wikipedia.org
en.wikipedia.org
If the intermediary result makes no sense, and only the function composition makes sense, I'll create a new well named function that does the chaining, even if it's single use.
This is literally the only way I've seen multi-year projects be maintainable, and anything else eventually breaks down into uninteligible code.
Writing code for other developers in the long-run is empathy for yourself, since you'll eventually forget all the context you once had.
compose(foo, bar, baz)
Here compose is applied to three "points" (which happen to be functions).tacit doesn't mean functions are nullary.
From the Wikipedia article:
> Tacit programming is of theoretical interest, because the strict use of composition results in programs that are well adapted for equational reasoning.
Now this is bullshit. See my explanation, as equations are symmetric, and you can swap left and right hand side.
Maybe you should try to understand harder.
It has little to do with the idea of LeftSide = RightSide. In fact you don’t even need an “equation” in that sense, or any equality sign, to do equational reasoning.
Equational reasoning is when a program is evaluated (ie.: like when you simplify or factorize an equation in maths) by using substitution (or rewriting) rules: https://en.wikipedia.org/wiki/Rewriting
I recommend this book for you, you should read it. I have: https://www.cambridge.org/core/books/term-rewriting-and-all-...
I wouldn't think so, because you don't seem to know the first thing about equational reasoning, namely that it is ALWAYS ABOUT EQUATIONS.
As for making things clearer for everyone, well, this is code I work on solo (hobby), but I think providing example inputs as well as debugging macros can help a lot. Consider the following:
(defn parse-it [dependencies]
(pp->> dependencies
str
str/split-lines
(keep (|| re-matches #"- (\d+(?:\.\d+)?) -> \[((?:\d+(?:\.\d+)?(?:, ?)?)+)\]"))
(>>- (map-> (juxt-> second
(->> third (re-seq #"(?:\d+(?:\.\d+)?)")))))))
(parse-it (str "- 4 -> [1, 2, 3]\n"
"- 5 -> [4, 2]\n"
"- 6 -> [1, 2, 5]"))
;; The use of pp->> will lead to this getting printed
;; ->> dependencies : "- 4 -> [1, 2, 3]
;; - 5 -> [4, 2]
;; - 6 -> [1, 2, 5]"
;; str/split-lines : ["- 4 -> [1, 2, 3]"
;; "- 5 -> [4, 2]"
;; "- 6 -> [1, 2, 5]"]
;; (keep (|| re-matches #"- (\d+(?:\.\d+)... : (["- 4 -> [1, 2, 3]" "4" "1, 2, 3"]
;; ["- 5 -> [4, 2]" "5" "4, 2"]
;; ["- 6 -> [1, 2, 5]" "6" "1, 2, 5"])
;; (>>- (map-> (juxt-> second (->> third ... : (("4" ("1" "2" "3"))
;; ("5" ("4" "2"))
;; ("6" ("1" "2" "5")))
In the end I think it doesn't really bring a lot, but it's especially useful in making short piece of code more readable: (->> [1 2 3 4]
(map (when| odd? inc)))
;; vs
(->> [1 2 3 4]
(map (fn [x]
(if (odd? x)
(inc x)
x))))
;; result (2 2 4 4)
Or this: (-> k-or-ks (when-not-> coll? list)
(map-> ...do-something))
;;vs
(let [ks (if (coll? k-or-ks)
k-or-ks
(list k-or-ks))]
(map ...do-something
ks))
Or even this: (-> 1 (juxtm-> :incd inc :decd dec))
;; vs
(let [n 1]
{:incd (inc n)
:decd (dec n)})
Granted I have insane shits like this teleport arrow (very useful though): (-> '(1 2)
(•- (conj (-• first dec)))) ;; => (0 1 2)
Or this >-args "fletching" (-> {:a 1 :b 2}
(•- (-> (>-args (-> (/ (-> :a) (-> :b))))
(->> (assoc (-•) :result))))) ;; => {:a 1, :b 2, :result 1/2}
I actually write this kind of stuff in my code ahahaha (the right move is to implement assoc-> of course hahahaha). Now there are combinators I wrote I never use, like departializers, unappliers, argument shifters, etcSometimes, the repetition of encounters is just as meaningful as the people we meet.
https://en.wikipedia.org/wiki/Small-world_network https://www.quantamagazine.org/new-proof-shows-that-expander... https://telecom-paris.hal.science/hal-03814119/document
Consider McIlroy's famous 1-liner:
tr -cs A-Za-z '\n' |
tr A-Z a-z |
sort |
uniq -c |
sort -rn |
sed ${1}q
Adding intermediate variables like "newline_delimited", "lowercased", "sorted", etc... is just pure noise. It's the equivalent of a newb programmer putting a comment over each line of simple code explaining in English what that code does, despite it being clear already."There are only two hard things in Computer Science: cache invalidation and naming things"
One of the great benefits of pipelines and tacit programming is the ability not to name intermediate results, especially when those results speak for themselves, are not re-used, and have no significance in isolation.
Would you honestly expect someone to know by heart what "sort -rn" or "uniq -c" do? This forces the reader to know what all the arguments mean by anyone reading this code.
If you'd try to push this code, I wouldn't let it pass code review without a comment on each line (except plain "sort", that one's really obvious).
For people that program regularly in bash, I would. If it was a rare bash script in a code base where many team members didn't know bash well, comments would be appropriate. Even there, though, that's not the same as introducing superfluous intermediate variables.
The larger point here relates to "intended audience" or "what competencies may I assume the reader has?". This is a matter of art, not science, and highly context dependent.
Take an extreme version of the point you just made:
const x 4*2;
"Would you honestly expect someone to know by heart that `*` in JS means multiplication?"Clearly that is absurd, because that knowledge is an assumed competency.
What about `**` for power?
What about `^` for XOR?
What about knowing that `-~x` is equivalent to `x+1`?
Where exactly do you draw the line? You can "err on the side of over-commenting" but only so much, because taken to an extreme it hurts readability, and will annoy everyone.
Responsibilities are distributed. Is clarification the responsibility of the author or the reader? What can I assume "a reasonable reader" should know? The answer depends on many things. But the right answer isn't a blanket policy of assuming incompetence and explaining every detail with comments or intermediate variables.
I personally have not regretted much assuming incompetence in my future self on small things, but it's your choice to what standard you hold yourself against.
I don't fully agree. Even when each step is fully descriptive it greatly helps to see the intermediate result, and names can be used for that as well. It is true that some intermediate names are not required, partly because some steps are best understandable in conjuction with neighboring steps (e.g. `sort | uniq -c` is a very common idiom and splitting them would do much harm than good), but a healthy dose of names would help in general.
I would say that there are three major steps in this particular pipeline: normalization (`tr -cs A-Za-z '\n' | tr A-Z a-z`), frequency calculation (`sort | uniq -c`), and extraction of first ${1} largest entries (`sort -rn | sed ${1}q`). So it is reasonable to have two additional names between them. Or you can name each step with a function so that you don't need intermediate results to understand that (`norm-words | calc-freqs | keep-largest ${1}`).
> It's the equivalent of a newb programmer putting a comment over each line of simple code explaining in English what that code does, despite it being clear already.
That is more about repeating functional parts without describing any intention. Comments about intents and clarifications are absolutely fine. For example:
counter += 1; // increment the counter by one (bad)
counter += 1; // increment the global counter, don't need synchronization here (better)
g_counter.incr_without_sync(); // (even better, but not always possible)The Haskell culture in particular has a very strong idea of naming all the things that can have a good name, and none of the ones that can't. Point-free functions are about that last part, but you only touched the first part. (Personally, I think the culture is too radical, but you can't really argue against a principle like this.)
quad a b c = let d = b * b - 4 * a * c in ((-b + sqrt d) / 2 * a, (-b - sqrt d) / 2 * a)
to ghci> import Control.Monad
ghci> quad = ap (ap . ((.) .) . ap (ap . (liftM2 (,) .) . flip (flip . ((*) .) . flip flip 2 . ((/) .) . (. sqrt) . (+) . negate)) (flip (flip . ((*) .) . flip flip 2 . ((/) .) . (. sqrt) . (-) . negate))) (flip ((.) . (-) . join (*)) . (*) . (4 *))
ghci> quad 1 3 (-4)
(1.0,-4.0)
[0] https://pointfree.io/ foo = h . g . f
With the more verbose: foo x =
let
a = f x
b = g a
c = h b
in c
If a, b, and c have useful names that help you understand the code then the second function might be preferable- but in a lot of cases all the intermediate variables are just adding noise and making it harder to see what’s happening at a glance. The tacit example makes it very clear at a quick glance exactly what’s happening.My personal rule of thumb is that if you are passing combinators in as arguments to other combinators then you should probably stop, but straightforward chaining is usually okay.
I just hate reading the resulting code written by others. What information is expected to come in, and exactly what data passes from one step to the next, and in what position? Data type signatures only go so far.
Point-free means you have all that wiring in your head, without assistance from the notation.
x
|> f a
|> g b
…
(search for "pipeline debugging" on < https://devblogs.microsoft.com/dotnet/whats-new-in-fsharp-6/>).In my experience, these are more common than strict point-free style anyway.
I'd say that at least point-free prevents naming fatigue, but for the result to be nice to the reader, the naming and factorization is much more important than with explicit code, which has sort of much more "safeties".
On the specific question you ask, let's hear the creator of one of the major stack oriented languages, Forth, by its inventor when he had about 30 years of professional practice. He talks about code comments, which is the local solution that comes immediately to mind when thinking about the issues you pointed out:
So people who draw stack diagrams or pictures of things on the stack should immediately realize that they are doing something wrong. Even the little parameter pictures that are so popular. You know if you are defining a word and then you put in a comment showing what the stack effects are and it indicates F and x and y
F ( x - y )
I used to appreciate this back in the days when I let my stacks get too complicated, but no more. We don't need this kind of information. It should be obvious from the source code or be documented somewhere else.
Well, comment-less code isn't popular either, but we're not here to design the next Python anyway. By the way, Chuck Moore evolved his language until he determined that the remaining problems were "in the hardware". He did his own CAD tools with his language to design stack-oriented CPUs with some success, as some of them ended up in spacecrafts (notably Philae in 2014 [3]).
In my experience, when you want to keep the data flow as simple as possible (in Forth: keep "stack juggling" to a minimum), there's very often only one good order for parameters. That works very well for the writer, for whom the understanding of the problem helps with memorizing signatures and semantics. As a reader (of other's code), I have much less experience, but I guess that understanding the same things requires the readers to invest more time up front, in addition to the "accidental obfuscations" (poorly written code, unusual programming habits and conventions...).
[1] https://en.wikipedia.org/wiki/Decision_fatigue [2] https://www.ultratechnology.com/1xforth.htm [3] https://en.wikipedia.org/wiki/RTX2010
ChatGPT is quite good at coming up with names.
It can map a description of a thing to a name. And the descriptions can vary between developers and probably still end up with the same name. So if everyone relies on its naming-prowess then everyone ends up naming things consistently.
Aside: I wonder if it can name parameters from tacit-style...
Write this in non-tacit style:
sort input.txt | uniq -c | sort -rn > output.txt
sort input.txt > sorted.txt
uniq -c sorted.txt > counted.txt
sort -rn counted.txt > output.txt
Seems it can. Look how the output of `uniq` was `counted`.Maybe we should just let ChatGPT name everything.
Interesting to think what programming is you no longer have to name things. A lot of programming is grouping code and coming up with names for the groups.
What if we didn't worry about grouping or naming, and just dealt with the data, and let chatgpt provide names on-the-fly.
Functions could even be replaced by descriptions of the actions.
I wonder if you could let an LLM evolve a graphical operating system from just the hardware definition.
We went through ~70 years of trial and error for programming languages going from low-level to progressively higher-level, and kept all the baggage along the way.
How would an AI go about this...with all the knowledge of today? How would it reason about each step in evolving a language.
I'm guessing it would probably be a LISP :D
params[:id]
|> find_user
|> verify_user_has_access_to(params[:post_id])
|> etc
When I define the functions I'll use pattern matching on the first argument to each function so that you can either pass in a "user" or {:ok, user} or {:err, _}. If it matches the error pattern it'll just return the error unmodified. The errors have enough fidelity to make it clear where the pipeline failed. I'm not sure this is a super common pattern though but it worked pretty well for me.- (Result.map f) returns (Ok (f(a))) or (Err b)
- (Result.bind f) returns f(a) or (Err b)
)
One could then write
params.id
|> find_user
|> Result.bind (verify_user_has_access_to params.post_id)
|> etc
Reasons for that are 2-fold1. It de-clutters your functions (you do not need that match statement anymore)
2. It becomes evident
- which functions will simply pass down errors (bind or map) vs. which ones may handle them
- which functions may raise new errors (bind can, map can't)I completely agree. It fatigues me to read unnecessarily point-free programming. I have to translate it into a point-ful style in my head to understand it.
For example, you could take this piece of Haskell code and make it more point-free. I think it's readable at first, if redundant (you could remove the last parameter xs, for example).
-- map: apply the function f to each element of the list xs
map :: (a -> b) -> [a] -> [b]
map f xs = foldr (\x xs -> f x : xs) [] xs
Some people would prefer to write it like this: map :: (a -> b) -> [a] -> [b]
map f = foldr ((:) . f) []
That lambda takes more effort to parse and think about, though, at least for me. The first version was pretty readable. Now, when I read "((:) . f)" I'm thinking "okay, so the function f takes an argument, and passes the result to the (:) function, which normally takes two parameters; with one argument, it returns another function that takes one list parameter and returns it with the result of "f x" prepended to it." And to do this, I have to know implicitly how many arguments the function (:) takes in order to parse and understand it correctly (though in this case, it's obvious, because (:) is ubiquitous).Pointfree.io would take what I wrote and transform it into
map = flip foldr ([]) . ((:) .)
But I'm pretty sure no one would write that. It takes even more effort to correctly parse this.That said, I don't write any Haskell. I just used to try to, but found I didn't like it.
Is 'tacit' an old term or a newer rebranding?
(edit: https://en.wikipedia.org/wiki/Whitehead%27s_point-free_geome... might be earlier than the category stuff, but not sure if it used the 'point-free' nomenclature, nor if it has a direct lineage to the later category theory stuff )
I'd guess given that I associate point-free with Haskell, which came much later, that the APL nomenclature (of which I was ignorant) came first.
The wiki page explains it in the very first sentence: “… in which function definitions do not identify the arguments (or "points") on which they operate”.
Tacit Programming - https://news.ycombinator.com/item?id=28413195 - Sept 2021 (1 comment)
What’s the Point of Pointfree Programming? - https://news.ycombinator.com/item?id=25509468 - Dec 2020 (1 comment)
When Does Point-Free Notation Make Code More Readable? - https://news.ycombinator.com/item?id=15365214 - Sept 2017 (73 comments)
Programming in the Point-Free Style - https://news.ycombinator.com/item?id=14077863 - April 2017 (101 comments)
Point-Free style: What is it good for? - https://news.ycombinator.com/item?id=1175946 - March 2010 (22 comments)
dismal ← 10⊥(⌈/10⊥⍣¯1⊢)
This is the complete solution to addition in the framework of Dismal Arithmetic [1].The pivotal idea there was the inverse of a function, and "trains". Until that moment of insight, I was fiddling about with dfns, which looks janky in comparison.
dismal ← {10(⊤⍣¯1)⍵}∘{⌈/⍵}∘{10(⊥⍣¯1)⍵}⊢
⍣¯1 is APL for "inverse". Aaron Hsu helped me understand [2] what was going on.I feel like this kind of conceptual power is tacit programming at is finest. It blows my mind that this alien gobbledygook code still makes sense to me after not touching APL for years now. And I've only toyed around with the language.
[1] I was playing with the problem to compare Clojure and APL, just because. https://www.evalapply.org/posts/dismal-arithmetic-dyalog-apl...
[2] https://www.sacrideo.us/decoding-inverses/
(edit: fix typos, verbiage)
And what does that look like in C?
unsigned dismal(unsigned x, unsigned y) {
unsigned z;
for (z = 0; x || y; x /= 10, y /= 10)
z = z * 10 + (x % 10 > y % 10 ? x % 10 : y % 10);
return z;
}For dyadic functions, the incerse argument is always the right side one.
So negation is its own inverse.
So the inverse of 2+ is indeed subtraction of 2.
I was curious about this piece of unexplained cryptic code, so I did a little investigation to see what's going on. Unfortunately there aren't any revolutionary concepts here, just esoteric notation. I'll explain:
To do that "dismal addition" thing, you need to split a number into digits and build a new number using the largest digit at each position. dismal(123, 321) = 323.
APL gives you an operator to make a number out of a sequence of digits: that inverted T you see in OP's code. The left operand is the base. `inverted_T(10, [1, 2, 3]) = 123`.
It gives you another operator to split a quantity into a hierarchy of units. That's the Tee you see in their second snippet. The left operand is a sequence of radixes. An inch is 2.54 cm, a foot is 12 inches, a yard is 3 feet. To transform 130 cm into ft/yd/in, you'd do: `Tee([3, 12, 2.54], 130) = [1, 1, 3.18]`.
So, OP wanted to use this Tee operator to split a number into digits. The problem is, they don't know beforehand how many digits the number has! If it's 2 digits, they must do `Tee([10, 10], number)`. If it's 3, they must do `Tee([10, 10, 10], number)`. (Because `Tee([10, 10], 123) = [12, 3]`). So in the second snippet they tried to do some juggling to get the number of digits and use it in the Tee function (I guess).
What OP really needs is the inverse function of inverted_T. And wouldn't you know it, APL can give you the inverse of a built-in function or a sufficiently simple user function. How? Maybe an operator? No...
See that operator that looks like a puckered face? That operator applies the function to its left, as many times as the operand to its right, to whatever is to the right of the sideways T. BUT, if the right operand is negative, it applies the inverse of the left operand. Basically, the all-powerful "invert function" operation is hidden as a special case of another operator...
In sum, here's my interpretation of OP's code in pseudocode:
Using:
encode(base, seq) = <base> inverted_T <seq>
max(a, b) = a gamma b
reduce(fn, seq) = <fn> slash <seq>
superapply(fn, times, seq) = <fn> puckered <times> sideways_T <seq>
let dismal =
encode(10, reduce(max, superapply(encode(10), -1)))
So, dismal([123, 321, 111])
applies the inverse of `encode(10)` one time to each sequence item, giving: [[1, 2, 3], [3, 2, 1], [1, 1, 1]]
Reduces using max max(max([1, 2, 3], [3, 2, 1]), [1, 1, 1])
giving [3, 2, 3]
and encodes it in base 10, giving 323.So that's it. Nice standard library, awful syntax.
I think that OP's "epiphany" was finding a quirk in this esoteric language to counteract another quirk.
Anyway, having satisfied my curiosity, I'm going to promptly forget everything about this :)
I could see myself transforming a functional program into a procedual one as I try to understand it just so I can have all information just in front of me and not have to keep things in my head.
Like I get that a functional style helps with the mathematical correctness side of things. But I'm just one of those people who still hold "code is meant for humans" first.
On the contrary, if you assume that your functions are good abstractions then you shouldn’t need to know their implementation details in order to compose them. You can tell what it does by looking at just what you have in front of you.
If that’s not the case, then you’re not looking at a good example of this concept. And we all know we can find bad examples of any idea or paradigm— what is useful is the good examples.
> On the contrary, if you assume that your functions are good abstractions then you shouldn’t need to know their implementation details in order to compose them. You can tell what it does by looking at just what you have in front of you.
In maintenance, you often cannot assume that your functions are good abstractions. One of them is doing something wrong, or at least something that needs changed. Which one? You have to go into the implementation details in order to find where you have to begin to work.
That's not "bad examples" of FP. That's just how software maintenance is. And maintenance is going to happen to FP programs too...
While this is as true in FP as it is anywhere, my experience is that it’s rarely true of the kind of small pure functions that tend to be composed like this. When someone is using a chain of functions like this, they usually are good abstractions that don’t change, and are short enough to be trivially correct.
(
f andThen
g
)(x)
vs
a = f(x);
g(a);_andThen_ is of course just compose with the arguments flipped such that you can compose from left to right instead of right to left.
In FP languages, and languages that use FP combinators, it is usually more efficient to use composed functions with combinators that do iteration and copying like _map(list, f):List_, because the iteration and copying happens during each map application:
map(map(l, f), g)
and map(l, f andThen g)
produce equivalent results, but the second is faster and has fewer allocations.For determining the arguments to f and g and map, typed fp languages usually have ide features which can show you the inferred type of each expression, or you can jump to the definition and see it, usually by hovering or key chord while the cursor is over it.
map[A, B](
fa: List[A],
f: A => B
): List[B]
and etc.This allows you to read things easily and avoid cluttering the code with types where they can be easily inferred.
Point-free style is important to make the usage of such combinators acceptable but experienced devs do extract and name a composition when it becomes difficult to understand. Of course, the treatment of functions as effect-free black box transformers for equational reasoning also makes these refactoring extractions to variables safe to do.
You get a feeling for what is too much over time, and settle on when to use point free style and when not to.
On efficiency - good compilers can often identify nested/chained map applications and rewrite them as a single map application during compilation, but map fusion isn't guaranteed by all compilers or all _map_ instances. The evaluation strategy (lazy/eager) of the language and/data structure also plays a role in whether or not map fusion with point-free style is more efficient or optimisable.
Really like the idea and apparently one of my favorite features of UNIX is the pipe facility, and it's actually a form of tacit programming. This makes a lot of sense since the OS itself is already full of ready made functions that can be exposed through the API [2].
It's also interesting to note that most of the modern programming languages for examples Python, Ruby, Perl, and JavaScript are missing or have awkward support of this feature.
[1] Pipelines Support Vectorized, Point-Free, and Imperative Style:
https://www.oilshell.org/blog/2017/01/15.html
[2] The Linux Programming Interface:
For context, see this discussion: https://stackoverflow.com/questions/5671271/what-are-advanta...
I really enjoy trying to think through programming problems in a dataflow style like this. It often turns the problem inside out in an interesting way. I did about half of this year's Advent of Code problems in such a system and it was a blast.
A key step is that programming languages which use the style builds a vocabulary of commonly used operations and make these into general knowledge for programmers using the language. Thus, succinctness is obtained and you can compress hundreds of lines into a few. Flip side is a steeper learning curve, which has to be balanced against.
In Rust there is the “builder pattern”[0] where the builder isn’t mentioned directly:
ByValueBuilder::new()
.with_favorite_number(42)
.with_favorite_programming_language("Rust")
.build()
In OO land they are called “fluent interfaces”[1], commonly used when building SQL queries while only mentioning the final query at the end: query = translations
.Where(t => t.Key.Contains("a"))
.OrderBy(t => t.Value.Length)
.Select(t => t.Value.ToUpper());
Especially in the query builder example, since it is essentially building up an AST, the type of the top-level object can change in each call, but since it isn’t named it also doesn’t need to be typed, so there’s no issue with variable shadowing etc.[0] https://blog.logrocket.com/build-rust-api-builder-pattern/ [1] https://en.m.wikipedia.org/wiki/Fluent_interface
A prime example of non-builder fluent interfaces are containers in Rust.
Both builders and fluent interfaces exist pretty much in all languages that can support chaining methods on some sort of object. This includes OO languages as well as totally-not-OO languages like Rust that just stop short of calling their classes "classes."
[0] https://en.wikipedia.org/wiki/Builder_pattern
Both builders and fluent interfaces have little to do with point-free style. X.foo().bar().baz() is not any more point-free than baz(bar(foo(x))).
Whether GP's second example counts as a fluent interface is a bit iffy just because of SQL keywords and how it works in general, but possibly a good way to think of fluent interfaces is madlibs: "Find a ___(noun) that is ___(adjective)". Instead of creating a function with complicated arguments like "find(noun, adjective)" or "find(Noun(n, adjective))", you'd mimic the sentence structure with something along the lines of "collection.find(noun).thatIs(adjective)" - the chained methods interact with each other to flexibly specify complicated arguments. The ".thatIs()" could for example be completely omitted or specified multiple times to further restrict what "noun" to return. The query builder is iffy because while it looks like one on the surface, it's actually plain method chaining where each call modifies and returns the original object without the context that's passed to the next call in a fluent interface.
Method chaining is the simple syntactic pattern that enables both of these other patterns. An umbrella term they both fall under.
When I write JS (not TS), I prefer as much as possible to use destructuring in every function definition. Named keyword arguments.
Try to change the name of something as little as possible even as it gets passed around. It’s more powerful than a type system in some ways. Not always doable, but just try it sometime; it’s fun.
I call it nominative programming.
function greet(greeting, name) {
return `${greeting} ${name}`
}
const greetWithGreetingHello = greet('hello')
greetWithGreetingHello('sally')
And this should work with named arguments too. function greet({greeting, name}) {
...
}
const greetWithGreetingHello = greet({greeting: 'hello'})
greetWithGreetingHello({name: 'sally'})
With named params, curried functions can be read more naturally too.greetWithGreetingHelloAndWith({name: 'sally'}).
"and with name sally"
A javascript object is like an S-expression if you squint. The subtree nesting is messier.
x
|> f a
|> g b
…
… where everything after the first |> is essentially in point-free style.You'd do something like:
(save (transform (fetch))) ;; calls fetch, then transform, then save
(-> (fetch)
(transform)
(save))
Not that the non-threading version was hard to read, but once the function names start to be a bit longer and involve arguments, the threading version tends to be a lot easier to read.It’s fun in the sense that solving a puzzle is fun, but I avoid it for anything I need to maintain long-term.
But it’s good practice for understanding combinators which is useful for some kinds of problems.
Some good practice problems can be found on Advent of Code, Codewars, and the Perl Weekly Challenge.
(defn blackbird [f g] (fn [x y] (f (g x y))))
or (def blackbird (comp comp comp))I often use point-free style myself (pipelines), but I'd never pretend it's good practice. Pipelines are a quick-and-dirty hack for when you're too lazy to use a better programming paradigm. They're "write only", in that it's quicker to write one from scratch than to understand an existing one.
"Tacit programming" as a concept sounds like typical mathematicians' obscurantism, like their love of single-character variable names and implicit operations.