I love softcore FP (immutability, first-class functions, etc), but I always get the impression that hardcore FP, like in this article, is about reimplementing programming language concepts on top of the actual programming language used.
The pipe feels like a block of restricted statements, `peekErr` and `scan` recreate conditionals, the Maybe monad behaves like a try-catch here, etc.
Of course there are differences, like `map` working on both lists and tasks, as opposed to a naive iteration. That's cool. But this new programming language comes with limitations, like the difficulty in peeking ahead or behind during the list iteration, reusing data between passes, and not to mention the lost tooling, like stack traces and breakpoints.
I've written many (toy) programming languages, the process is exactly like this article. And it feels awesome. But I question the wisdom of presenting all this abstract scaffolding as a viable Javascript solution, as opposed to a Javascripter's introduction to Haskell or Clojure, where this scaffolding is the language.
I've worked on some big TypeScript code bases that make heavy use of sort of FP the article promotes; dealing with stack traces and debugging the code is incredibly painful. Jumping around between 47 layers of curried wrapper functions and function composers that manage all state mostly in closures is a real drag.
Until the tooling is better, I can't recommend these idioms for real work.
TBF, there is a kind of beauty to the approach and as you really dig into it and understand it, it can feel like a revelation. There's something addictive about it. But any feeling of joy is obliterated by the pain of tracing an error in a debugger that isn't equipped to handle the idiom.
As a complement to what you said: a far better paradigm to everything-must-be-purely-functional is "write as much of your program as is practical in the functional style, and then for the (hopefully small) remnant, just give it the side-effects and mutation".
This leads to fewer errors than the imperative/object-oriented paradigms, and greater development velocity (and quicker developer onboarding, and better tools...) than the 100%-purely-functional strategy.
Hopefully, over time, we'll get functional programming techniques that can easily model more and more of real-world problems (while incurring minimal programmer learning curve and cognitive overhead), making that remainder get smaller without extra programmer pain - but we may never eliminate it completely, and that's ok. 100 lines of effectful code is far easier to examine and debug then the entire application, and our job as programmers is generally not to write purely functional code, but to build a software artifact.
The above applies to type systems, too, which is why gradual typing is so amazing. Usually 99/.9% of a codebase can be easily statically-typed, while the remaining small fraction contains logic that is complex enough that you have to contort your type system to address it, and in the process break junior's developers' heads - often better to box it, add runtime checks, and carefully manually examine the code then resort to templates or macros parameterized on lifetime variables or something equally eldritch.
(and, like the above, hopefully as time goes on we'll get more advanced and easier-to-use type systems to shrink that remainder)
JS with Ramda (and an FRP library, if needed) is the sweet spot for me. I use a lot of pipes() but usually don't break them down into smaller functions until there is a reason to; but FP makes it trivial to do so.
We maintain a service which is making heavy use of Ramda. It seemed like the right tool for the job, because the service is mostly doing data transformation, and the code ends up "clean" and terse. However, we found that onboarding people who are not familiar with FP is time consuming and often people outright refused to learn it. We decided to ditch Ramda, rewrite the service in vanilla JS, prefering immutability where possible. We're about halfway done and it was definitely the right decision. Sure, `R.mapObjIndexed(doSomething, someObject)` is simpler compared to `Object.fromEntries(Object.entries(someObject).map(doSomething))` and now there's a ton of multi-level object spreads, but at least it's simple enough to understand for anyone familiar with JS.
We also came up with a `pipe` function that handles Promises. It makes chaining (async) functions very convenient:
I've found this too. Once I got comfortable with map/reduce/filter in JS, the thought of using an imperative while or for loop feels as backwards as writing a goto/label.
I think that's sort of true; to me a lot of the value of fancier and FP is that you can do things "in userspace", with plain values and functions, rather than needing magical language keywords. Magical language keywords do have some advantages because they can have tight integration with the language implementation, like stack traces as you mention. But they're also hard to understand and reason about, especially when it comes to how they interact with each other. (E.g. in languages that have both builtin async and builtin exceptions, the interaction between the two tends to be complex and confusing; whereas where async and error handling are implemented by libraries, it's easy to see exactly what's happening in any scenario, since it all just follows the normal rules of the language).
I don't know Clojure, but stack traces and breakpoints are nearly unusable in Haskell also, for exactly the reason you say. It's a weakness of functional programming, not the language.
That said, you need these things less when you don't have mutable state and you do have referential integrity so you can zoom in on the critical path of side effecting code.
Sorry, I wouldn't dream to imply that Clojure is inferior to Javascript. My dig was at the article's implementation of half of Clojure[1], which does have tooling problems.
The only thing stopping Functional Programming from taking off are functional programmers.
It took me years (and Elixir) to get past the "here are some words, some mean what you think, some mean something completely different to what you know, some mean almost the same thing. Also here's 30 overloaded operators that you have to juggle in your head and we wont introduce things like variables for another 4 pages." without running back to something imperative to actually build something.
Functional programming's great when you have immutability to drop an entire class of bugs and the mental energy that accompanies that, can pass-around and compose-with small functions easily which makes writing, testing and figuring much simpler and can make your brain click that "its all data" without trying to over complicate it by attaching methods to it.
Honestly I think that's 90% of what the royal You needs to understand to get FP and actually try it. Selling people on "its all stateless man" or other ivory tower tripe is such garbage because (as the tutorials always point out) a program with no state is useless, and then the poor shmuck has to learn about monads just to add a key to a map and print something at which point they're asking "how was this better again?"
I think that's a bit harsh. It's frustrating to translate an imperative task to an FP language, but it's also frustrating to translate a Bash command into Python, or Prolog semantics into Go, or any program full of mutations and global state into (safe) Rust.
I think a lot of the friction you mentioned comes from learning imperative programming first. Our vocabulary, tools, and even requirements come with an implicit imperative assumption.
PS: I didn't downvote you, though your tone is harsher than I prefer for HN.
I would argue that command sequences that mutate global state are far more intuitive to humans (food recipes, furniture assembly manuals, the steps to take to fix a flat tire on your bike, etc.) than functions. So it’s not just what we learn first in a pedagogic sense. We’re already wired for imperative programming. “Thread the state of the world through the food recipe instructions” is a ridiculous concept to a normal person.
I agree in principle. However, if you work imperatively, you'll use _much_ more (global) state than if you worked with a pragmatic functional language like Clojure. Which in turn leads to normal people not understanding what's going on, since humans are built to keep, say, 10 things (atoms) in mind, not 100.
I strongly disagree, it is definitely the teachers and advocates of FP which are lacking.
There are plenty of programs and libraries written in "imperative" languages like Python with lots of functional style applied. That should be the starting point, not sermonizing about whether a monad can even be defined with English words.
Haskell is particularly, egregiously bad for this.
None of the concepts are particularly complex. They're unfamiliar, sort of, but they're not insanely obscure.
But then there's the labelling. If an ordinary for-loop was called a yarborough and a function call a demicurry it really wouldn't help with the learning.
I realise the concepts are supposed to map[1] to category theory, but category theory stole words like monad from elsewhere anyway.
That is somewhat true, but in practice, not really so.
Imagine you're a Chinese programmer. Obviously a lot of open source documentation is in English, so you have to learn basic English anyways (I'm bilingual from an early age, but most people struggle if they start the process even in their teens).
Then you see English-speaking programmers talking about yarborough and demicurry as if they were English words. Now you're majorly confused. Worse, an encyclopedia of philosphy tells you that it means something (see eg. https://plato.stanford.edu/entries/leibniz/#MonWorPhe ) What's the actual connection? When you ask, people casually tell you to read up on category theory, which is apparently something they teach to advanced Math majors. Remember we're struggling to learn basic English here!!!
Tangent: what I like about (the) Haskell (ecosystem) is that most often you'll find the exact right tool for a job.
Eg: in Python you get `dict` ... In Haskell you get very specialized versions of you need them. Do you have integers as keys? `IntMap` is your friend. Hash able keys? `HashMap`! Don't know anything about your keys except for it a key is different from another? You still functions that tread a list of tuples as a map.
> I think a lot of the friction you mentioned comes from learning imperative programming first. Our vocabulary, tools, and even requirements come with an implicit imperative assumption.
Yes that's exactly my point and why no one cares about how great a monad is when trying to learn FP.
How is that a problem of functional programming? When you learn a language you don’t start with “Well in English we say…” you learn language with its idioms and quirks.
It's not. It's a problem with people trying to sell FP to non FP programmers. A non FP programmer doesn't care about monads. They care about making what they already do simpler and clearer.
> When you learn a language you don’t start with “Well in English we say…” you learn language with its idioms and quirks.
Idioms and quirks come after rote memorization of basic vocabulary and sentence structure, which are then used to learn the idioms and quirks. I think Bongo is saying FP-ers are skipping the basic vocabulary part and jumping straight to the idioms and quirks.
It was interesting in 2015/2016 to see co-workers learn some of that basic vocabulary of functional programming without realizing they were touching on it, using map/fold/etc when learning React.
> Functional programming's great when you have immutability to drop an entire class of bugs and the mental energy that accompanies that, can pass-around and compose-with small functions easily which makes writing, testing and figuring much simpler and can make your brain click that "its all data" without trying to over complicate it by attaching methods to it.
A hundred percent. FP for me, is "let's stop gluing state and functions together for no reason" and get all the benefits above. I understand pure functions, avoiding state, closures, and using first class functions as the basic building blocks of creating higher level structures.
When it becomes "a functor is a monoid of the schlerm frumulet" - ala the typical FP advocacy blog / latest React state management manifesto masquerading as library documentation, I zone out. The odd thing is, I don't feel I've lost anything by doing so.
I agree that Elixir is the ideal gateway into FP. It's also quite a good argument that "FP" at its core is something more fundamental than what is talked about in this article. Elixir doesn't really use many of the category theory derived approaches at all, it has a convenient way of doing method chaining and some higher order function stuff and that's about it. And the results are excellent code.
The two main FP things that you need to learn to do Elixir well (IMO) are thinking about data structures first, and purity. Choose the right representation of the data at each stage and then what functions do you need to move between each. Make those functions side effect free (but not obsessively). Then you put your stateful code in GenServers and you have useful real code and most of it was incredibly easy to test.
Pretty much this. I've been learning Common Lisp and the way I try to design my programs is to have a somewhat clear understand on how your data should transform from input to output. Then you write the functions.
Let's say your data is a log file and you want to load everything in memory. You write the code that returns the data, which you either store in a variable or compose with other functions. At the end, you compose all of these functions in your programs. Everything is an expression, meaning that you can extract things easily into functions. This is how you make things readable. `load-log-from-file` and `auth-failure-count` is better than keeping everything together in a single function.
The pipe feels like a block of restricted statements, `peekErr` and `scan` recreate conditionals, the Maybe monad behaves like a try-catch here, etc.
Of course there are differences, like `map` working on both lists and tasks, as opposed to a naive iteration. That's cool. But this new programming language comes with limitations, like the difficulty in peeking ahead or behind during the list iteration, reusing data between passes, and not to mention the lost tooling, like stack traces and breakpoints.
I've written many (toy) programming languages, the process is exactly like this article. And it feels awesome. But I question the wisdom of presenting all this abstract scaffolding as a viable Javascript solution, as opposed to a Javascripter's introduction to Haskell or Clojure, where this scaffolding is the language.