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 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.
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:
const pipe = (...functions) => (input) => functions.reduce((chain, currentFunction) => chain.then(currentFunction), Promise.resolve(input));
I couldn't come up with a succinct way to say exactly this, since this is very much how I am with it right now. Thanks for that.
I will add I'm comfortable with, and prefer, map/select/reduce over for loops where provided.
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.
The only problem I encountered regularly was that bugs did only manifest in the optimized versions not in the debugged ones.
I would strongly object this.
Have you seen ZIO?
(Without tooling the stack traces can be noisy.. but that's incidental in this discussion context)
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 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.
Oh, that's how it works? That actually makes sense. Thanks!
Some traditional CS algorithm? Mutable it is. But a compiler with different passes is a great fit for the FP paradigm.
But even if we go to pure math, you will have constructive proofs beside traditional ones.
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.
"A monad is just a monoid in the category of endofunctors, what's the problem?"
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 to category theory, but category theory stole words like monad from elsewhere anyway.
German, Polish, and Chinese programmer all learn what "for" means.
And anyway, Java calls functions "methods", yet somehow we survived.
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!!!
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.
Yes that's exactly my point and why no one cares about how great a monad is when trying to learn FP.
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.
is same as
That's the beauty of immutable data.
2. the beauty of immutable data isn't in hiding meaning behind meaningless piles of functional abstractions
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.
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.
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.