Common combinators in JavaScript
gist.github.com
gist.github.com
What use is a line like
const S = f => g => x => f(x)(g(x));
for anyone except someone who already groks what the S combinator does and is for?Why do these combinators have single-letter names anyway? Does that help anyone? Even Haskell, heaven on earth for single-letter guess-what-i-mean coders, has actual names for half of these - how is knowing that "flip" is also "the C combinator" helpful in any way?
Now, I'm not trying to diss this page in particular, and I very much am not trying to diss functional programming. I like functional programming. But I think that when hype shifted from OOP to FP, we threw half the baby out with the bathwater (ok, maybe a quarter baby). Naming is important and FP does not have any feature in particular that makes "I" a better name than "identity".
I think programming in general, and FP in particular, will become a better place if we all grow up a bit and stop pretending we're too smart to need good naming.
It's a fun POC to actually represent it in code, not a serious proposal for production grade software.
In this case, I'd say it's not about being concise, but rather matching the mathematical notation that people are used to seeing when studying these combinators.
>> I think programming in general, and FP in particular, will become a better place if we all grow up a bit and stop pretending we're too smart to need good naming.
I with you on this, but I think this is a bad example.
And while we are at it, documentation matters as well. The 'good code is the best documentation' attitude[ * ] might be valid if your methods consist of 3 lines and you use 6 methods in 5 modules. But anything bigger will become hard to maintain in the long run.
[ * ]: I encountered this especially in the haskell and erlang world.
Oh my word. If it's code on the internet someone will use it. Especially functional programmers who have somehow landed themselves in a JavaScript position. I've seen much stranger code pulled off the internet and used in production applications by professionals.
That's why it's important to generally publish the best code possible whenever we can.
Or Haskell programmers who have somehow landed themselves in any position. I've seen Haskell implemented in Erlang :)
The comment makes poster look like they haven’t read anything CompSci related.
Even ++ is frowned upon for being unreadable/easily missed and/or placed in the wrong spot.
It’s algebra but with functions.
If you read any computer science books they’ll often use this form. It’s a holdover from mathematical notation.
How does this not apply to any and all identifiers and symbols and functions?
What use is a given function until you read its signature, implementation, and docstring?
You could use this to condemn all abstraction. After all, even a function you take for granted comes at the expense of indirection.
It just so happens that we as craftsmen make trade-offs. Imagine a pattern that shows up so much that you can extract it into a function like a combinator. For instance, look at parser combinators. Yes, you'd have to credentialize in some patterns, but once again, that's what we do every day as we work on what amount to machinas of abstraction.
I'm sure you'd get a kick out of Co-dfns [0]. But once you learn to "speak" APL programming it makes a lot more sense. I'd highly recommend, if you have the time, to listen to the author explaining the code for the compiler [1].
[0] https://github.com/Co-dfns/Co-dfns/blob/master/codfns.pdf
[1] https://www.youtube.com/watch?v=gcUWTa16Jc0 / HN discussion: https://news.ycombinator.com/item?id=13638086
That said, Math is very contextual while development has many varying scope levels. Single letters identifiers are normally not a good choice in a global context because of collisions, however good names they may be.
We are in a field where backgrounds vary widely.
A little "what am I looking at and why do I care" would be useful on this page.
I'm more likely to write
const geed = g(x);
const effed = f(x);
const S = effed(geed);
I have no idea when I would actually write a structure like this though. Can someone please fill those names in with something like `buildComparator` or `Math.max.apply` or something, anything, that remotely makes sense? some_function(f, g, x) {
var effed = f(x);
var geed = g(x);
var S = effed(geed);
// some other logic
}
Not the S combinator, but consider being able to pass a comparator function to your sort function in C. Or passing an arbitrary predicate to `filter` or an arbitrary binary function/operator to `fold`. The S combinator itself may not be that useful, but higher order functions in general are very useful.You'll also see their utility if you use tacit programming (programming without variables). Again, maybe not the explicit combinators listed here, but the mode of operating is the same. Tacit programming is easily explored through J, APL (less accessible due to the character set), the various MLs and Haskell.
Sometimes you could end up with this code in situations like this:
const data = getDataForUser(user);
const transformer = getTransformersForGDPR();
const sanitizedData = transformer(data);
where getTransformersForGDPR returns a single function that may contain a chain of data transformations.The example is contrived, of course, but you do end up with similar constructs especially if you have been exposed to functional programming.
const data = getDataFromState(state);
const transformer = getTransformersForState(state);
const sanitizedData = transformer(data);:)
Most math texts have a fair amount of documentation when formulas are introduced. Perhaps substantially more than the table in this article.
The thing with these combinators is that they are so low level, so names for them are almost useless. If you were to program with them directly, you'd learn to use and recognize useful sequences of them. At least that's the theory for APL-like languages like J.
Your critique is like someone claiming imperative coders are horrible and don't value naming because of assembly language.
Of course, a true programmer delegates even the simplest calculation to a computer, so they have less of a need for notation that is easy to manipulate.
So then what name do you want to give 'x' in `I = x => x`? Is `something` better, more readable?
If you write these functions ad-hoc in your modules as local helper functions, then by all means, use a more appropriate name:
[1,2,3,undefined,6].map(anIntegerOrUndefined => anIntegerOrUndefined)
But the functions presented here are not those that you should have to write yourself. They are available in the Haskell base library, in JS you can import ramda or other packages. The important thing is to know how and when to use them.The only better names you could really give are ones like func1, func2, arg1, arg2, which don't add any information, just add noise. Trying to get any more specific than that would leave the reader trying to interpret the meaning of the name, but the whole point is that it's strictly "apply this thing to that in this way, without looking at what they are". Even fix-point, which is unarguably cryptic, really needs a paragraph explanation for the unfamiliar more than it would benefit from better names.
- K is "const" and C is "flip"; they're generally used to for arguments to a higher order function: "map (const 5) myList", "foldl (flip f) 0 myList"
- Psi is "on", since it's regularly used to construct a new function "h = f `on` g"
- S is a specific type of application, called infix as "f `ap` x", but also has an operator to intentionally make it look more like just line noise: "f < * > x". (Had to add spaces to this operator so HN wouldn't interpret it as italics)
The operators might seem opaque, but the idea is to make it more visually apparent that it's a pattern, not some application-specific business logic. There are lots of concepts of "apply" - there's pure application ($), Applicative application (< * >), Monadic application (=<<), etc. - writing them out would distracting to read. Using operators makes it easier to skim and get the general idea of how the code works without worrying about the underlying structural details, while still being precise about them.
In reality, expanding a lambda expression into SKI combinators takes up a lot of space. Since the notation is mainly for language implementors, using the short mnemonics allows you to compress the size of the resultant expression.
It's the same reason why the x86 opcode is named 'mov' not 'move'.