Readit News logoReadit News
BoppreH · 3 years ago
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.

daotoad · 3 years ago
I agree 100%.

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.

throw10920 · 3 years ago
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)

disantlor · 3 years ago
"softcore FP" is a great way to put it.

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.

myuzio · 3 years ago
It looks nice in code, but it's a hell to debug.
michaelcampbell · 3 years ago
> I love softcore FP (immutability, first-class functions, etc),

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.

philihp · 3 years ago
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.
lmm · 3 years ago
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).
lupire · 3 years ago
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.

fho · 3 years ago
Hmm ... What's your problem with ghci debugging? Iirc it does not work worse then eg pdb.

The only problem I encountered regularly was that bugs did only manifest in the optimized versions not in the debugged ones.

still_grokking · 3 years ago
> It's a weakness of functional programming, not the language.

I would strongly object this.

Have you seen ZIO?

https://zio.dev/

fulafel · 3 years ago
FWIW stack traces and break points work fine in Clojure. See eg https://calva.io/debugger/ for some animated gifs showing this in action.

(Without tooling the stack traces can be noisy.. but that's incidental in this discussion context)

BoppreH · 3 years ago
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.

[1] https://en.wikipedia.org/wiki/Greenspun%27s_tenth_rule

bongobingo1 · 3 years ago
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?"

BoppreH · 3 years ago
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.

nailer · 3 years ago
> 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.

rkangel · 3 years ago
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.

throwaway2037 · 3 years ago
One take away from functional programming that I have incorporated into my Java (and C#) code: If/When possible, never (ever ever!!!) re-assign a local variable. (Rare exception: Classic C-style for-loop iteration with mutable `i` integer variable.) Really, really, I am not trolling / not a joke. How / Why? How? All primative parameter value must be final, e.g., `final iint level`, and local variables must always be final. Why? Final vars are significantly easier to "reason about" when reading code, as compared to non-final (mutable) vars. If you are using containers, always, always, always use immutable containers from Google Guava unless you have an exceptionally good reason. Yes, the memory overhead will be higher, but big enterprise corps do not care about adding 4/8/16 extra GB RAM to host your Java / C# app. Yes, I know that any `Object` in Java is potentially mutable. As a work-around, try to make all `Object`s immutable where possible. On the surface, the whole thing / exercise feels like a terrible Computer Science nitemare. In practice, it becomes trivial to convert code from single-threaded to multi-threaded because most of your data classes are already "truly" final.
P_I_Staker · 3 years ago
That's interesting. On the other hand, I've seen code go through great lengths to not do a double assign.

I doubt it was easier to reason about at all. The code set an error var on a number of if / else conditions. They then used a separate flag, so that a double write wouldn't be necessary. ie. int flag = 0;

int err; // notice no initializer

if(stuff) { flag = 1; err = 1; } else {}

if(other_stuff) { flag = 1; err = 2; } else {}

if(flag) { LogError(err); }

I follow the "avoid double write" convention too, but in some cases, it can become very difficult to reason whether a local is uninitialized; this is a pretty terrible class of bug, because you work with garbage data off of the stack.

Setting to zero at declaration makes it much clearer that it will never be uninitialized, but it's essentially meaningless and arguably a form of dead code.

Maybe someone will suggest a redesign? More functions, smaller functions? It seems that there's a strong (though unpopular) argument that you should use medium sized functions, and some teams insist on doing this. In some cases, it can be easier to find the content, verify specifications.

Edit: On second thought I can see we didn't avoid the double write, just did it in a different variable. So I don't understand what the author's point is, lol.

Twisol · 3 years ago
I've seen some imperative code in the wild that looks like this:

    bool go = true;
    if (go) doA(&go);
    if (go) doB(&go);
    if (go) doC(&go);
As a functional thinker, this is "just" an encoding of the Maybe monad, so I actually quite like it. If you care about small efficiency wins, though, it's not great -- an early bail means checking all the remaining conditions. It's cute though!

I'm generally pretty okay with similar patterns over accumulators, as long as it's clear that the purpose of that variable is to be an accumulator. If we're just overwriting a variable because we don't need the old value anymore, or something like that, I'm much less charitable.

A big benefit of functional programming, for me, is to learn the safe roads as explicitly as possible, so that you can then identify (and use) them when they aren't so well signposted.

bluefirebrand · 3 years ago
When I have a flag that is bouncing through a bunch of conditionals only to return at the end, I prefer to change it to multiple return paths instead of multiple spots to modify the flag that is being returned.

If you are doing a lot of work inside those branches it may be worth a refactor to simplify the branching logic.

wtetzner · 3 years ago
> If you are using containers, always, always, always use immutable containers from Google Guava unless you have an exceptionally good reason.

I actually prefer pcollections: https://github.com/hrldcpr/pcollections

AtomicReference + immutable data types is a really nice way to program in Java, and is basically the way most Clojure programs are written.

acchow · 3 years ago
Read the example about “producers” - If I chain a whole bunch of producing together across many different Pcollections, I’m essentially forming a graph behind the scenes? Since no copying is happening

Surely this would cause some strange performance outcomes with, say, a gradually built up immutable list.

lupire · 3 years ago
It's much more important to make global variables (object fields) final, not locals. Locals are much much easier to test and reason about, and functional languages lie Haskell and Clojure have great support for mutable locals (ST / transient)
rzzzt · 3 years ago
Java 11+ has factory methods for unmodifiable collections: https://docs.oracle.com/en/java/javase/11/docs/api/java.base...
hinkley · 3 years ago
There's a short list of reasons to factor out a method. The most popular one is "if you see the need for inline comments for a block of code, maybe the code shouldn't be inline", but feeling the need to redefine a variable, especially in the middle of a function, is often a good indicator of a new scope starting. Sending variable foo with an increment, a merge, or a concatenation to another function is fairly readable. Having foo and foo' inline is quite a bit harder on working memory.
gregmac · 3 years ago
I agree with the other comments about it often being a hint to break apart a method, but there's still valid reasons to reassign.

I'd rather focus my energy on adding unit testing, and refactoring to be testable. If you have easy-to-read unit tests that cover all possible edge cases, I stop caring (as much) about how the actual method is written. Be as optimized/clever/concise as you want. I don't care if you reassign your local variables.

quickthrower2 · 3 years ago
I rarely reassign local variables too. (Other then the set to null as a fallback then set again in a branch “pattern”).

The only excuse is when I am doing something algorithmic and that would improve the performance.

By extension I prefer local variables over class state and static state and other “self managed lifetime” stuff like that.

And I am a GC blub programmer, I don’t use Rust or C.

throwaway2037 · 3 years ago
You wrote: <<GC blub programmer>> This is a great expression! Hat tip to Joel Spolsky for his blub programming language paradox. I know the feeling. When you read too much HN, you think the whole world is writing C & Rust.
matheusmoreira · 3 years ago
> Yes, the memory overhead will be higher, but big enterprise corps do not care about adding 4/8/16 extra GB RAM to host your Java / C# app.

Stuff like this is the reason why functional programming is synonymous with inefficiency and why Real Programming™ is still done in languages like C.

throwaway2037 · 3 years ago
It is interesting that you wrote <<why functional programming is synonymous with inefficiency>>. Most people would say <<why GC blub languages [Java, C#, etc.] are synonymous with inefficiency>>. I actually agree with my rewrite, but I deliver value much slower with non-GC languages. The IDE uplift from Java & C# is just so crazy good. It is hard to beat for many enterprise scenarios. Plus, the developers are much cheaper than C, C++, and Rust programmers.
senand · 3 years ago
Agreed, I see this as a code smell. It's already hard from a semantic point-of-view, normally you have then the same name for two different meanings.
waynesonfire · 3 years ago
> If/When possible, never (ever ever!!!) re-assign a local variable.

Neat rule, but, it won't work. Firstly, as an example, Scala which allows you to declare a read only val, you still see vars used. There is no way you're going to get any traction enforcing this across the developer spectrum.

The benefit of these concepts are realized when they are the only option, hence FP and why mixed paradigm languages are half-assed. Java isn't even a mixed-paradgim. You're wishing.

viscanti · 3 years ago
> Neat rule, but, it won't work.

You'd get the benefit of easier to reason about code everywhere you used it, even if others within the codebase don't. Using that argument, we would argue that it's never worth trying to find a cleaner way to implement something because maybe some intern some day will do something weird in a different part of the code base. We don't have to drop down to the lowest common denominator for code quality and we benefit every time we simplify things, even if not everyone does.

lmm · 3 years ago
> Scala which allows you to declare a read only val, you still see vars used.

You see them very rarely, and usually with a narrow scope.

I agree that you probably can't enforce an absolute rule of no mutable variables. But making it the exception rather than the rule (e.g. require it to be justified in code review) makes a huge difference.

kaba0 · 3 years ago
Local mutability is perfectly fine, and depending on the algorithm can be much more readable.

Use the best tool for the job.

fleddr · 3 years ago
I'm sure this is a wonderful intellectual exercise in computer science and math, but if this is to be the advertisement in favor of functional programming, I wouldn't consider myself a customer.

You start out with code that will not win any beauty awards but it gets the job done. It's easy to understand by programmers of any seniority, it's simple to debug and reasonably easy to put in an automatic unit test.

Next, you add all kinds of vague, poorly named meta utilities that don't seem to solve any tangible real world problem. You even mess with built-in JS methods like map. The code is now harder to understand and more difficult to debug.

A massive pain is added for some theoretical purity that few even understand or will benefit from. I'll double down on my rebellion by stating that the portability of code is overrated.

Here we're writing code to format notification objects. In our overengineering mindset, we break this down into many pieces and for each piece consider reusability outside this context to be of prime importance.

Why though? Why not just solve the problem directly without these extra self-inflicted goals? The idea that this is a best practice is a disease in our industry.

Most code that is written prematurely as reusable, will in fact never be reused outside its original context. And even if it is reused, that doesn't mean that outcome is so great.

Say that the sub logic to format a user profile link turned out to be needed outside this notification context. Our foresight to have made it reusable in the first place was solid. Now two completely different functional contexts are reusing this logic. Next, the marketing department goes: well actually...you need to add a tracking param specifically for links in the notification context, and only there.

Now your "portability" is a problem. There's various ways to deal with it, and I'm sure we'll pick the most complicated one in the name of some best practice.

After 20 years in the industry, you know how I would write the logic? A single method "formatNotification". It wouldn't weirdly copy the entire object over and over again, it would directly manipulate the object, one piece at a time. Error checking is in the method as well. You can read the entire logic top to bottom without jumping into 7 files. You can safely make changes to it and its intuitive to debug. Any junior would get it in about 2 minutes.

Clear, explicit code that requires minimum cognitive parsing.

spc476 · 3 years ago
You can apply some FP notions to any code base:

1. Don't use globals. Zero global variables should be the goal, that way, you avoid "spooky action at a distance" where some code here changes a global that changes the behavior of code over there. A function that avoids global variables is easier to deal with than one that doesn't. If you feel you need to use a global, think about why you need one before adding it. Maybe you can avoid it.

2. Parameters to functions are immutable. That way, you won't have to worry about data changing during a function call. If the parameter can be mutated, can you signal the intent in the function name or signature, so the programmer knows that to expect? Generally, try to avoid changing parameters in a function.

3. Separate I/O from processing. Do input, do your processing, then output. God, I wish the major component I worked on did that (concurrent DB requests)---it would make testing the business logic so much easier as it can be tested independently from the I/O.

Those three things can be done in any language, and just by doing those three things, you can most of the benefit of FP without the mathematical jargon (Monads? How do they work?). There's no need for over-engineered code, and you can be as explicit as you like while keeping the cognitive aspects to a minimum.

fleddr · 3 years ago
Well done. Your comment shows actual FP best practices and their immediate, significant advantages. And you did so succinctly.
john_the_writer · 3 years ago
Gods I hate the term monads. I've read the wiki at least 20 times in the past year and it sounds like such bloated rubbish.

I love your answer.. And I love how you can use it in every language.

leidenfrost · 3 years ago
Code readability should be the top most priority when writing code. Even more than portability and performance.

Anyone can rewrite a piece of good, succinct, clear code. Even if its performance is poor.

That cannot be said for clever hacker code that -was- fast until business requirements changed over time and now the guy that used to understand the code is long gone.

sfvisser · 3 years ago
You're really fighting the wrong fight here. FP is not about prematurely writing reusable code.

It's about recognizing patterns in code that exist all over the place and reusing those existing patterns for the new code you're writing. This way you can break your new feature into say 20% newly written domain specific code and 80% reusing existing patterns. Patterns that help with composition, enforcing invariants, simplifying writing test-cases, and more clearly signaling intent.

For example, if you provide a monad instance for your "formatting notification objects" I will probably understand how to use your library without even reading a single line of implementation at all. Just by scanning a few type signatures.

This way you and your team mates have a huge head start in understanding your new addition to an existing code base. This is a great win for everyone!

fleddr · 3 years ago
I'm not fighting, only offering an opinion. We simply disagree at a fundamental level.

My first and main point is that 80% of software developers, the silent majority of so-called "bread programmers", do not grasp these concepts. So even if the concept is great, it doesn't matter.

Second, and this is purely based on experience, I do not subscribe to the idea that your envisioned way of writing a new business feature is very realistic or useful. It leads to dependency hell, difficult to understand code, which is difficult to debug. I don't even subscribe to the idea (anymore) that if you're asked to implement a business feature, that it is implied that you should write it as if it can be torn apart and used for some imagined other feature. Nobody asked for that, we just made that up.

One can say that I've become a DRY infidel. I don't expect you to agree, very few would agree. Yet I've arrived here for a reason: 20 years of headaches looking at "best practice" codebases.

salawat · 3 years ago
...except that has nothing to do with FP and everything to do with... actually reading code.
foobiekr · 3 years ago
One of the things that you imply is another issue: inheriting someone’s functional code is brutal. I’d rather inherit mediocreville php than decipher the “clever” code that someone who got excited by scalaz polished for weeks.
pcblues · 3 years ago
I think this is key to maintainability. Provably maintainable does not equal maintainable in the real world. Unfortunate? For some. When I wrote software in company X I wrote code simply with old idioms so that anyone could understand, step through, change and fix. Code that can be inspected, stepped through and debugged line-by-line by a graduate is infinitely more useful to a business than an opaque one-line piece of beauty. Unfortunate? For some. But provably correct code is not as important as maintainability in a world of quickly changing requirements. Unfortunate? Not really an issue for the people who pay you.
oldboyFX · 3 years ago
Thank you for writing this. I've come to exactly the same conclusion after a decade of building and delivering complex tangible applications which others have to maintain after I'm gone.
fleddr · 3 years ago
Thanks for the support.

I used to live all the best practices, but over time learned that the number one enemy in a codebase is complexity. 90% of software development is maintenance. People doing maintenance are lowly productive because the vast majority of time is spent on trying to understand how things even work, debugging, and making changes in a way that is safe.

The reason code is complex are abstractions and dependencies. Many of which are of a questionable or negative value and never deliver their imagined benefits. Hence, instead of "abstract by default" and seeing DRY as some religion, I abstract when value is real, proven and outweighs the massive downsides of doing so.

Imagine that we'd need to write a recipe. Sane and pragmatic people would write it as a sequential step of instructions. If a software engineer were to write one, just to follow the recipe you'd need to have 23 additional papers each describing a sub step referred to from the main instructions. Next, we would abstract each substep into "Motion" and "Activity" generalizations. This way, you can reuse "Motion" when you walk your dog later that day. Quite obviously, motions can fail so we extend it with a MotionError, which is a sub class of Error. MotionError has a Logger interface. These 3 super abstractions are useful, instead of just directly saying that you failed a recipe step, we can now apply failure wherever we want, even onto life itself. Since recipes are written in a language, we should abstract Language, itself a composition of sentences, words, characters and glyphs with a Dictionary interface, which itself is an array of Name and Value objects, that we might as well generalize as Thing.

Anyway, those were recipe steps. Next, let's do ingredients, also known as immutable multivariant singletons. But not really, because as we follow the instructions, the ingredients morph into something else, which we'll solve with a Morp object, which holds an interface StateChange, consisting of two State objects, which are Things. A ThingsIterator takes care of handling multiple things but not in a way you'd expect, we'd override map() because I really feel my Thing is special. Thinking of recipes, they can potentially come from any source so we'll implement Verbal (an abstraction of Language), Database and the jungle that comes with it, all neatly wrapped into MoreThings.

Next we redo all of his by writing unit tests, so that our software that by now implements most of the universe is qualitative. It's math.

Or...we can just write the damn recipe.

wtetzner · 3 years ago
> It wouldn't weirdly copy the entire object over and over again, it would directly manipulate the object, one piece at a time.

I think this is the only part I disagree with. Immutable data types make it much easier to understand code, because everything is local. You don't have to worry that some other part of the code base might have a reference to this object, and could be manipulating it out from under you.

john_the_writer · 3 years ago
In practice this doesn't really happen in the context of a function call.

Like calling do_something(object) might call something inside it that changed the object, but not likely unknown..

Now if you're talking about threading. Honestly, I worked as a c++, java, rails and now elixir dev for > 20 years. I can't tell you how many times I built something that threaded, but not all that often. It's just doesn't come up all that often.

ozim · 3 years ago
I agree - problem is loads of people think they should write framework and not a simple CRUD app.

I remember one meeting where clueless business person told developers (including me) that we need an *abstract* solution for a problem.

It took me quite some hours to explain to other devs that for this business person abstract solution is not what they think it is. Business person wanted CRUD app where they could configure names for database entries where developers wanted to make some fancy data types to be used with parametric polymorphism.

pcblues · 3 years ago
You nailed it! Language is different in different parts of an organisation or life. A programmer says 'or' to a lay person. A lay person thinks 'xor'.
jimbokun · 3 years ago
Sounds like Go is your ideal language.
fleddr · 3 years ago
False. The best language is logo turtle: http://blog.core-ed.org/files/2014/08/logo_turtle-21.png

Dead Comment

sidlls · 3 years ago
"Instead, the most intelligent coders I knew tended to take it up; the people most passionate about writing good code."

Ah, yes, the most intelligent people tended to take it up, therefore if I take it up I, too, am intelligent.

That single line in the article encapsulates everything wrong with FP zealots' approach to their advocacy.

galaxyLogic · 3 years ago
People often confuse intelligence with complexity.

Yes you have to be intelligent to understand complex problems and complex solutions. But just because you are intelligent enough to understand a complicated solution, does not mean the solution is "intelligent".

True intelligence is about coming up with solutions where complexity is minimized.

Even though an intelligent person can understand a complicated solution the complexity of a solution strains even the most intelligent programmer. It takes time to ensure yourself that the solution is correct. Whereas with a simple solution that is easy. It takes intelligence to find a solution that is simple and thus easy.

NaturalPhallacy · 3 years ago
Completely agree. My favorite example of this is Douglas Adams' description of flight (or orbital mechanics): "Throw yourself at the ground and miss."
kaba0 · 3 years ago
That’s a great take, but I have to mention that I disagree with a statement I may have only read into the thread: FP is not more complex just for the sake of it.
didibus · 3 years ago
Do you mean that talent is required to leverage FP effectively?

Kind of like how a faster car in the hands of a less talented driver won't make them faster, but might just cause them to crash? Whereas a talented driver would pick a faster car and actually benefit from it?

Personally I think it's not so much about intelligence or talent, but passion. In think the developers who demonstrates that they've learned and tried more development techniques and styles, that shows a passion and interest towards software development, engineering and comp-sci. That passion is often key to success and long term performance.

There's probably a cutoff between talent and passion, where passion alone probably has some limits, but also where talent alone has limits as well. If you find someone with both though, that's a pretty good sign.

The question is, are talented developers drawn to FP, or is it passionate developers that are? Or those that have both?

And the other question is, can FP make someone more talented? Can it make them more passionate?

epgui · 3 years ago
Isn't that a bit of an ad hominem?

You dismiss or disagree, because you don't like the author's attitude?

sidlls · 3 years ago
Eh, maybe. But when the argument itself is a bit of an ad hominem what is one left with? The usual implication of the “argument” is that if one doesn’t think FP is all that great, it’s because they don’t understand it (and, usually, that they don’t understand it because they are not intelligent).
dsnr · 3 years ago
The gazillion of lines of imperative C/C++ code out there that every OS runs on, are probably written by less than intelligent people.
goatlover · 3 years ago
Like Linus Torvalds. Lispers tend to make similar claims. So does Alan Kay about Smalltalk. Yet most of the world runs on top of C/C++, or COBOL (or Java) for financial transactions and Fortran for heavy duty numerical calculations. Not that FP, Lisp and ST don’t have their advantages. They clearly do. But their supremacy is overstated when it comes to code that runs the world.
gpanders · 3 years ago
Ah yes, OS development, a domain famous for attracting stupid people.

Dead Comment

Dead Comment

bob1029 · 3 years ago
FP is only part of the equation when working with meaningfully-complex systems. Having a clear data + relational model is the other big part.

I would strongly recommend reviewing the functional-relational programming model described in this paper - http://curtclifton.net/papers/MoseleyMarks06a.pdf

The overall theme goes something like: FP is for handling all your logic, and RP is for arranging all of your data. Some sort of relational programming model is the only way to practically represent a domain with high dimensionality.

For real-world applications, we asked ourselves: "Which programming paradigm already exhibits attributes of both FP and RP?" The most obvious answer we arrived at is SQL. Today, we use SQL as a FRP language for configuring our product. 100% of the domain logic is defined as SQL queries. 100% of the domain data lives in the database. The database is scoped per unit of work/session, so there are strong guarantees of determinism throughout.

Writing domain logic in SQL is paradise. Our tables are intentionally kept small, so we never have to worry about performance & indexing. CTEs and such are encouraged to make queries as human-friendly as possible. Some of our most incredibly complex procedural code areas were replaced with not even 500 characters worth of SQL text that a domain expert can directly understand.

eddsh1994 · 3 years ago
Oh god, no! Please no!

Okay maybe it works for you, but I've worked on one of these systems for a financial engine with over 10m SQL LoC. This was a big product that was ~15 years old and used by dozens of companies having bespoke feature flags that changed significant parts of the calculation engine. Everyone except a couple grey beards who'd joined straight out of university left that place after a few years because of how insane it was to work on and we all became way too interested in good architecture design from that experience. My friends from that time who I still keep in touch with are almost entirely working on FP-based environments now.

alexvoda · 3 years ago
When done poorly that is the result. And it is very easy to do poorly.

But I think the basic premise of combining functional and relational is valid. At the very least it avoids the object relational impedance mismatch.

UK-Al05 · 3 years ago
Nearly every system I worked on that had significant business logic in SQL turned out to be a maintenance nightmare. Used to be a lot of this in the 90s or early 2000s. Where dB venders encouraged it for obvious reasons.

SQL isn't built with sensible decisions. Delete/update forget a where clause? Whoops. Lots of traps like that. Also it's not easily testable. Domain concepts don't always easily map.

alexvoda · 3 years ago
Part of the reason is that SQL is simply a bad language (the JavaScript of databases).

Part of the reason is also the thinking and philosophy behind DBMSs as interactive systems. Especially with regards to code.

A function doesn't just exist. You create a function and it lives in the database until you alter it or drop it.

This creates a different (temporal?) impedance mismatch with standard code management tools like version control. The result is most often a maintenance nightmare.

Dead Comment

alexvoda · 3 years ago
A few points:

- I can understand why this might appear as hell to many. SQL is unwieldy and doesn't play nice with many tools we use to make life easier.

- I agree with the fundamental premise that a combination of functional and relational principles can be highly effective for data heavy scenarios

- i disagree that pure SQL is the solution to achieve this. If using MS SQL Server, there are great opportunities to leavrage the CLR and F# (with caveats because F# gets less love than C#). You can write functional logic once and use it both outside of the database and inside the database. PostgreSQL has extensions for other languages.

sbelskie · 3 years ago
Does SQL CLR support .NET Core/.NET 5+ or just framework?
wibblewobble124 · 3 years ago
SQL is not functional. But otherwise I support the desire for an FP and relational language.
bob1029 · 3 years ago
> SQL is not functional

I agree that you are technically correct. It is simply declarative in nature. I've got a habit of conflating these terms. Functions aren't first-class citizens in SQL. We also have some UDFs that are impure (because the real world unfortunately exists).

I'd be perfectly happy to rename this a "Declarative-Relational" programming model if that sits better with everyone.

gizmo · 3 years ago
GOOD: having large chunks of code that runs (mostly) without side-effects. As projects get bigger global state can trip you up, so try to keep it in check. That's where the real value of functional programming is.

BAD: copying data needlessly and turning trivial stuff into a functional pipeline that is impossible to debug. You want your code to read top to bottom like the problem you're trying to solve, if at all possible. If your call stack looks like map, fold, reduce you've introduced a ton of complexity, and why exactly?

Every programmer should understand functional programming. Higher order functions can be super useful. Immutable objects are invaluable for preventing subtle bugs. But if you feel the need to transform simple linear code into a map of higher order functions because of some abstract benefits you're probably doing stuff that's too easy for you and you should get your mental challenge from solving a more difficult problem instead.

samsquire · 3 years ago
I would rather inherit someone's stateful JavaScript or Java than a complicated codebase by a seasoned Clojure, Haskell, Scala developer.

I stand a better chance understanding the sequential code than a complicated recursive (edit: nested) monad. Depending how it was written.

I can probably understand Kafka pipeline's or a functional map, reduce pipeline.

My parser for my programming language which I'm building is a recursive Pratt parser but it's written in Java.

I want to understand functional programming better. I am interested in software transactional memory and parallelism. I am aware Erlang and Go use message passing but only Erlang is functional.

In theory functional code can be parallelised due to immutability but you need some kind of communication to indicate transference of values. Imagine a pipeline where you need to share a value to a few other threads. I'm aware Haskell has software transactional memory

amelius · 3 years ago
> I would rather inherit someone's stateful JavaScript or Java than a complicated codebase by a seasoned Clojure, Haskell, Scala developer.

Isn't that simply because you're a Java developer (as I understand from the rest of your post)?

lmarcos · 3 years ago
Agree. The first version of the example (the one using only `map`) is way easier to understand and maintain than the rigmarole the author ended up with (writing their own functors, pipes, peakerror and what not). It's like the author didn't find the first version "complex" enough, so they had to make it more complicated for the sake of "functional programming".
throw827474737 · 3 years ago
Also all "functional" programs have their state somewhere.. just usually much stricter isolated and manipulated - so no difference in statefulness.

> I would rather inherit someone's stateful JavaScript or Java than a complicated codebase by a seasoned Clojure, Haskell, Scala developer. I stand a better chance understanding the sequential code than a complicated recursive monad. Depending how it was written.

True, but you add here the "complicated" attribute deliberately .. usually it is the other way round. The functional approach code is simpler to read and reason ( and usually not with much recursive monoids), while the believed sequential program has so many "concurrent" paths (even if there is no real concurrency, but just in the many hidden ways how the super distributed statefulness is manipulated).. not?

Philip-J-Fry · 3 years ago
Functional programmers are just as likely to make overly complicated code the same as a developer in a conventional language.

I'd rather inherit a complicated Java program than a complicated Scala program. But I'd take a well written Scala program over a well written Java program any day of the week.

epgui · 3 years ago
Is there even such a thing as a “recursive monad”? That sounds made up.
kalekold · 3 years ago
I've just left a startup company who's entire backend was written in a functional style using Typescript by an agency. The only reason I can fathom why is 'why not, functional programming is cool right!'. A new dev team was created to take over and it was a disaster. It was an absolute mess of piped, mapped, reduced functions everywhere and completely unreadable and unmaintainable. I remember getting lost in a hierarchy of about 30+ piped (non db framework) functions to write a JS object to a database. I didn't stay long.

Since I quit, the entire new engineering team quit and it looks like the company is going under. Functional programming is a big mistake for some real-world code.

atraac · 3 years ago
> Functional programming is a big mistake for some real-world code.

Generalizing much? I write C# for a living, Elixir in my free time, I would take Elixir codebase any day of the week. If you try to write C# in a strictly functional fashion it's going to be shitshow as well. Moderation is the key, using immutable data structures, getting rid of side effects if possible etc.

You had a bad experience because you and/or people you worked with simply tried to fit a square peg into a round hole, you didn't get it in, threw a tantrum and now you're blaming all the squares for being bad.

I on the other hand, after learning functional language, have trouble looking at most code written by pure 'OOP developers', most of it is a spaghetti shitshow of hierarchy classes, dependencies and jumping across 20 different files of factories and providers because DUHH sOlId and ClEaN. That doesn't mean that OOP is a 'mistake for real-world code'.

lolinder · 3 years ago
> I've just left a startup company who's entire backend was written ... by an agency. ... A new dev team was created to take over and it was a disaster.

I honestly think these are the important bits of this story. The startup outsourced their backend to an agency, then tried to replace the agency with a brand new internal dev team.

There's no way that story ends well, regardless of the paradigm the agency chose or how skilled they might have been (probably not very). Every codebase is unmaintainable when the team that built it is suddenly replaced.

Peter Naur had a lot to say about this in Programming as Theory Building [0]:

> The extended life of a program according to these notions depends on the taking over by new generations of programmers of the theory of the program. For a new programmer to come to possess an existing theory of a program it is insufficient that he or she has the opportunity to become familiar with the program text and other documentation. What is required is that the new programmer has the opportunity to work in close contact with the programmers who already possess the theory, so as to be able to become familiar with the place of the program in the wider context of the relevant real world situations and so as to acquire the knowledge of how the program works and how unusual program reactions and program modifications are handled within the program theory.

[0] https://gist.github.com/onlurking/fc5c81d18cfce9ff81bc968a7f...

bigDinosaur · 3 years ago
You can make code in any paradigm suck. You can do horrible unmaintainable things in any language in any paradigm: Java with twenty layers of abstractions, Python with immense amounts of spaghetti, C with its hard to control safety. You can also do awful, abysmal imperative or OOP code in Typescript. So I just don't really see how you can single out FP here at all. Your codebase sucked, and whoever was hired to write it in a FP style just sucked at doing so. Sorry.
mhitza · 3 years ago
> Functional programming is a big mistake for some real-world code.

The emphasis on "some" should be stronger in your comment, otherwise it reads, on a quick pass, as a broad dismissal of functional programming.

Functional programming concepts and ideas have been steadily incorporated in most mainstream languages in the last 10+ years. However, when people move past the language's functional programming primitives, it's when the project enters potentially dangerous territory. Your, and the article's, example of pipes, for one.

Personally, I'd like more languages to incorporate ADTs (algebraic datatypes) for me to be able to "de"layer programs back to a mostly procedural + functional programming. And based on the current adoption rate of FP concepts, I'm not sure we're that far away from having proper ADTs and pattern matching in the most popular imperative programming languages of today.

eckza · 3 years ago
Inexperienced developers that don't understand what they're doing, armed with a language known best for being worst-in-class at everything except for "running everywhere", is a recipe for disaster, no matter how you spin it.

It feels egregious to implicate "the entire surface area of functional programming" here, when there are other obvious issues at play.

WastingMyTime89 · 3 years ago
> copying data needlessly and turning trivial stuff into a functional pipeline that is impossible to debug. You want your code to read top to bottom like the problem you're trying to solve, if at all possible. If your call stack looks like map, fold, reduce you've introduced a ton of complexity, and why exactly?

Hard to follow code is mostly a consequence of point-free style in Haskell which is an heresy to be avoided at all cost.

ML typically uses a piping operator to compose code instead and that leads to top to bottom code which is extremely easy to read.

There is zero difference then between a map and a loop. Loops are not read top down anyway nor are function calls. It seems to me you are just more used to these indirections than other ones and are therefor blind to them. This leads to an argument which I consider a straw man personally.

christophilus · 3 years ago
I'm reminded of the Kernighan quote / trope: "Everyone knows that debugging is twice as hard as writing a program in the first place. So, if you're as clever as you can be when you write it, how will you ever debug it?"

This phenomenon seems to happen more frequently with FP or related tools and languages (RxJs abstraction soup comes to mind).

Chris2048 · 3 years ago
> Loops are not read top down anyway nor are function calls

But their scope can be limited.

lixtra · 3 years ago
I tried googling for an example[1] and had difficulty finding a good one.

Do you have a link to a good one?

[1] https://elixirschool.com/en/lessons/basics/pipe_operator

chriswarbo · 3 years ago
> simple linear code

If you're talking about a sequence of statements, I've rarely come across such a thing.

Firstly, the mere existence of statements in a language is an incredible complication:

- It forks the grammar into two distinct sub-languages (expressions and statements)

- Expressions compose with each other, and with statements; but statements don't compose with expressions.

- It introduces a notion of (logical) time into the semantics ('before' a statement is executed, and 'after' a statement is executed)

- The times of each statement need to be coordinated. Chaining one-after-another is easy (usually with ';'), but concurrency gives an exponential explosion of possible interleavings.

- The computer often cannot help us spot mistakes, and compilers have very little ability to optimise statements.

This is especially silly in Python, where many common tasks require statements, which often requires rewrites (e.g. we can't put statements in lambdas; hence we often need separate function defs (which themselves are statements); pulling those out may lose access to required lexically-scoped variables, requiring even more plumbing and wrapping; urgh.)

Compare this to functional code, where there's only a single language (expressions) where everything is composable; where there is no control flow (only data dependencies); there is no notion of time; mistakes are more obvious (e.g. 'no such variable' errors at compile time); compilers have lots of scope to optimise; and code is trivial to parallelise.

bazoom42 · 3 years ago
Worth noting Haskell ended up reinventing the statement/expression distincion in the do-notation. So apparently the distinction do have value.

“Logical time” actually matters when you have side-effects. Of course we can agree code without side effects is simpler to reason about. Unfortunately you need side effects to do anything useful.

t43562 · 3 years ago
It might be easier to explain things to oneself in terms of time though - we are not necessarily functional in thinking. e.g. I imagine trying to explain f(g(x)) versus g(f(x)) without using the word "first" or "then"
chronial · 3 years ago
> pulling those out may lose access to required lexically-scoped variables

The only situation for this that I can think of is inside a list comprehension / generator expression. You are aware that you can define functions at any scope in python?

galaxyLogic · 3 years ago
Part of the problem with "functional pipelines" as in the article is that you are in essence creating a domain-specific language to describe the problem your program is modeling and solving.

Problem is that is not a programming language supported by anybody else than you. It is not part of the standard library. If it was it would probably be of high quality, highly tested-in-practice code.

But if you constructed your pipeline-framework (as in the article) by yourself you now need to understand not only the solution in terms of your created framework, but also how the framework itself exactly works. If this is a one-off problem-solution what looks like simpler code in the end hides its complexity inside its framework-code. And as you keep programming your framework is a moving target. You will improve it from time to time. Understanding it becomes a moving target too. It is a real problem for maintenance for you or anybody else who wants to use and adapt your existing code in the future.

Think about having the problem and a solution and being able to describe both in English. But then you say hey if I invent this new language Klingon and express the problem and solution in Klingon, then both the problem and solution become simpler. Cool. But now you must also program the Klingon interpreter for yourself. And be aware of which version of Klingon your code is assuming is used.

This is the tendency and cost of over-engineering. It is fun and natural to look for the "ideal solution", but it comes with a cost.

piokoch · 3 years ago
100% agree, the monstrosity that author proposes is really hard to swallow. Simple made hard.

The idea to handle validation (we get HTML instead of expected JSON) by passing some Nothing object down the flow is horrible, if we expect JSON and get something unparsable, well, the best strategy is to fail fast and let know client side that we have a wrong data. Instead we show off with some fancy code structure with zero value for the user of the software.

mejutoco · 3 years ago
Agree, especially with having as much part of the code as pure functions.

I think where functional programming really shines is in combination with a good type system, that can enforce those restrictions. In languages like js I find it not so useful (even if using map, filter, etc.) because I have no guarantees that there is a hack somewhere in the function that I did not see.

When there are a lot of pure functions the cognitive load is reduced so much I can use the "extra capacity" to focus on the task.

gizmo · 3 years ago
In Javascript Object.freeze() and Object.seal() are wonderful to prevent accidental mutation.
blauditore · 3 years ago
Avoiding side-effects is usually lumped toghether with async-style interfaces into the term "functional programming". However, the two are separate properties with separate benefits and downsides:

- No side-effects makes reasoning about logic easier, code more deterministic, and tests more reliable. However, certain problems become harder to solve (e.g. simple caching or memoization).

- Async-style interfaces allow for waiting on slow operations in a non-blocking fashion, meaning those don't occupy threads, thus a more economic use of resources. However, async-style code can be harder to read, write, and debug, though this varies heavily between languages and frameworks.

brap · 3 years ago
Some solutions are much easier to express in an imperative way.

You can still write pure functions and avoid state in imperative code. Use consts, immutable objects, etc. You get some of the benefits of functional programming while still writing readable code.

wiseowise · 3 years ago
Map, reduce and fold are much easier to reason about than imperative loops, what’s your problem exactly?
Gordonjcp · 3 years ago
I don't get the point of "immutable objects". Why have an object at all? Why not just hardcode the values if you want them not to change?
TeMPOraL · 3 years ago
The point is that if you have a reference to such object, you know for sure it'll stay looking the same way at all times, no matter what happens in the program. E.g. take this pseudocode:

  let foo = MakeMeAnObject();
  let bar = MakeSomethingElse(foo);
  DoSomethingForSideEffects(foo, JoinThings(bar, baz));
  return foo;
With immutable objects, you can be certain that neither foo nor bar were modified by any of this code. So, when e.g. debugging a problem with that DoSomethingForSideEffects() call, you don't have to worry about values of foo and bar having been changed somewhere, by someone, between their introduction and the point you're debugging.

Neither of them can be modified by it in the future - e.g. someone else can't change MakeSomethingElse() to also modify its inputs, thereby accidentally breaking your code that's using this function.

Another way of looking at it: a lot of problems with reasoning about code, or parallelism, can be drastically simplified by assuming the data being passed around is only copied, and not modified in-place. "Immutable objects" as a language feature is just making this assumption the default, and enforcing it at the language/compiler level.

In terms of use, it isn't that much more inconvenient over mutable objects. You can always do something like:

  foo = DoSomeTransformation(foo);
It's just that DoSomeTransformation() is not modifying the object, but instead returning a new instance of the same objects, with relevant fields having different values. The obvious drawback here is, for large data structures, there will be lot of copying involved - but that's where the languages with immutable objects are usually "cheating", e.g. by using sophisticated data structures masquerading as simple ones, as to only ever copy things that have actually changed (i.e. "distinct" objects end up sharing a lot of their data under the hood).

i_no_can_eat · 3 years ago
immutable means that you set them only once, typically at runtime. Not that they have the same values always, everytime you run.
andrekandre · 3 years ago
"immutable" is kind of overloaded term, but what it means in this context is an object itself doesn't mutate when you want to change a value (basically value semantics[0] instead of reference semantics[1])

for example (pseudocode)

  var newUser = Person(name: "some guy", age: 0)

  // newUser is replaced with a new object that is
  // initialized with field.name and previous age (0)
  // the old object is discarded
  newUser.name = field.name

  // object is passed as copy-on-write, assuming a 1 sec delay
  // it will print the objects values at this point in time
  // (age: 0) even if altered later
  async(after:1) { print(newUser.age) } // prints 0

  // age was changed to 32 a nanosecond later,
  // but now a new object again is initialized
  // instead of mutated
  newUser.age = 32 
  print(newUser.age) // prints 32

  ----------
  output:
    32
    0
[0] https://en.wikipedia.org/wiki/Value_semantics

[1] https://stackoverflow.com/questions/27084007/what-is-value-a...

ankurdhama · 3 years ago
It makes code easy to understand as you can easily identify what is input and what is output and can easily figure out the flow of data i.e what is being computed based on what. Without immutable objects a function taking object can mutate those objects and now they are acting as input and output, which leads to complexity.
dopidopHN · 3 years ago
Those are often records from a database. There is a way to change it, it’s to make a copy of it.

I like being able to trust that something is what I think it is because it cannot be something else. Meaning: if I know that something can’t change, I don’t have to check for eventual accidental change.

jimbokun · 3 years ago
The idea is that a given object never changes, but it's straightforward and efficient to create a new object from the original one with some of the values changed.

This is "safer" for any code still operating on the original object, as it will not be changed unexpectedly.

spc476 · 3 years ago
It's about trusting a function call. Imagine the following C functions:

    extern result fubar(struct foo *);
    extern result snafu(struct foo const *);
Just by looking at the function prototype, I know that calling snafu() won't change the struct foo I pass it. I don't know what fubar() will do to it. Maybe it won't change, maybe it will. I'd have to check the implementation of fubar() to find out.

Deleted Comment

Dead Comment

Deleted Comment

steinuil · 3 years ago
I'm a fan of functional programming but I'm pretty sure this post would do a terrible job of convincing anyone to try FP out. There's a very bad pattern of replicating very specific language features and control flow structures just to make them more similar to point-free Haskell, which is not going to win anybody over.

The author begins by replacing a language feature, the . operator, with a pipe() function. After that they swap out exceptions for Result, null/undefined for Maybe, Promise with Task, and the final code ends up becoming an obfuscated mess of wrapper functions and custom control flow for what you could write as:

    fetch(urlForData)
      .then(data => data.json())
      .then((notifications) =>
        notifications.map((notification) => ({
          ...notification,
          readableDate: new Date(notification.date * 1000).toGMTString(),
          message: notification.message.replace(/</g, '&lt;'),
          sender: `https://example.com/users/${notification.username}`,
          source: `https://example.com/${notification.sourceType}/${notification.sourceId}`,
          icon: `https://example.com/assets/icons/${notification.sourceType}-small.svg`,
        })
      )
      .catch((err) => { console.log(err); return fallback });
...which is just as functional because doesn't involve any mutation and it doesn't require several pages of wrapper functions to set up, you can tell what it does at a glance without having to look up any other pieces of code, it's gonna run faster, and it uses standard control flow which you can easily debug it using the tools you use for any other JS code.

This post has nothing to do with functional programming, this is a poor monad tutorial.

still_grokking · 3 years ago
FP is the best thing since sliced bread, don't get me wrong!

But the seemingly arising "Haskell religion" is at least as mislead as the OOP-religion that held mainstream in stranglehold for a long time.

Haskell's syntax isn't anything to imitate. It's hostile to IDE features, at least. Alone that should make it a no-go.

The next thing is that Haskell's features may make sense in the Haskell context, but they don't make any sense at all in almost any other language.

When you don't have strong types, no lazy evaluation by default, and it's not mandatory to use IO-wrappers, it makes not sense to mimic solutions that arose out necessities that are again results of constrains of Haskell's feature design. You need to do some things in Haskell the Haskell way because Haskell is the way it is. In a different language the chosen solutions are at best bonkers (even they may make sense for Haskell).

As always: It's a terrible idea to start doing something because "it's cool" and the "new hot shit", without actually understanding why exactly things are (or should be) done the way they are. The "Haskell religion" is by now imho already mostly only cargo cult… The more worrisome part is that it seems it attracts more and more acolytes. This will end up badly. It will likely kill the good ideas behind FP and only leave a hull of religious customs that the cult followers will insist on; exactly like it happened to OOP in the past.

That's my personal opinion, speaking as a big FP Scala fan.

misja111 · 3 years ago
> But the seemingly arising "Haskell religion" is at least as mislead as the OOP-religion that held mainstream in stranglehold for a long time.

I wanted to say the same but you were faster than me. The article reminded me a lot of the design patterns fad in the OOP world: that compulsion to make everything abstract and reusable even if there's no use for it yet. But hey, at some point it might be needed and then it would be great!

Of course there are some cases where you really want to be ahead of what might come , e.g. public library api's, but those are rare.

chongli · 3 years ago
Haskell's syntax isn't anything to imitate. It's hostile to IDE features, at least.

Can you elaborate on this? I haven't used Haskell in years but I recall enjoying some of its really cool IDE-friendly features, such as typed holes and the ability to press a key and have whatever is under my editor's cursor show me its type and another key to insert that inferred type into the document.

I have heard that more recent versions support nice record syntax using the '.' operator (but without whitespace, to differentiate it from composition). That's also very IDE friendly since type inference in the editor should be able to resolve the members of a data type that's been defined with record syntax.

efnx · 3 years ago
I think a lot of this (to quote the Dude) “is just like, your opinion, man”.

Haskell is great but it’s not for everybody. It really gets you thinking about software engineering in a different way.

I wrote Haskell full time for five years, with some of the best in the industry and I can tell you that there is no real religion. I will admit the language attracts ideologists, theorists and folks who like to pioneer (which I like), but everybody is just trying to make things work - these are patterns that solve problems and guidelines that avoid problems.

Now, it’s really hard to take the good parts of Haskell and bring them to a language like JavaScript and have it feel “natural” - especially to someone who isn’t a native Haskell writer! And especially a concept as reliant on higher kinded types as effects!

tikhonj · 3 years ago
JavaScript et al do have IO wrappers, they just call them promises and only force some effects through them.
ladyattis · 3 years ago
I'm biased but I find the pattern for FP in LISP-like languages easier to understand. I think that and other languages with FP facilities are better to emulate than Haskell which seems more focused on mathematicians.
deltasevennine · 3 years ago
This opinion is also biased. We have no theoretical method for determining which design philosophy is better than the other.

We can't know whether the OOP religion is better, we also can't know if the Haskell religion is better, and we can't know whether NEITHER is better. (this is key, even the neutral point of view where both are "good" can't be proven).

We do have theories to determine algorithmic efficiency. Computational complexity allows us to quantify which algorithm is faster and better. But whether that algorithm was better implemented using FP concepts or OOP concepts, we don't know... we can't know.

A lot of people like you just pick a random religion. It may seem more reasonable and measured to pick the neutral ground. But this in itself is A Religion.

It's the "it's all apples and oranges approach" or the "FP and OOP are just different tools in a toolbox" approach.... but without any mathematical theory to quantify "better" there's no way we can really ever know. Rotten apples and rotten oranges ALSO exist in a world full of apples and oranges.

You can't see it but even on an intuitive level this "opinion" is really really biased. It seems reasonable when you have two options to choose from "OOP" and "FP", but what if you have more options? We have Declarative programming, Lisp style programming, assembly language programming, logic programming, reg-exp... Are we really to apply this philosophy to ALL possible styles of programming? Is every single thing in the universe truly apples and oranges or just a tool in a toolbox?

With this many options it's unlikely. Something must be bad, something must be good and many things are better then other things.

I am of the opinion that normal Procedural and imperative programming with functions is Superior to OOP for the majority of applications. I am not saying FP is better than imperative programming, I am saying OOP is a overall a bad tool even compared with normal programming. But I can't prove my opinion to be right, and you can't prove it to be wrong.

Without proof, all we can do is move in circles and argue endlessly. But, psychologically, people tend to fall for your argument because it's less extreme, it seemingly takes the "reasonable" mediator approach. But like I said even this approach is one form of an extreme and it is not reasonable at all.

I mean your evidence is just a bunch of qualitative factoids. An opponent to your opinion will come at you with another list of qualitative factoids. You mix all the factoids together and you have a bigger list of factoids with no definitive conclusion.

borbulon · 3 years ago
> ends up becoming an obfuscated mess of wrapper functions and custom control flow

This right here is the reason I don't like pure functional programming. Any engineer should be able to pick up your code and understand it pretty close to immediately. And with pure FP, even when they understand functional programming, they end up wasting valuable time tracking down what it is you're trying to do.

* I guess I need to edit to say I mean pure FP in a language not explicitly built for pure FP. The article is about implementing in JS, this is what I'm addressing.

jerf · 3 years ago
There's a difference between "pure FP in an imperative-oriented language" and "pure FP in a pure-FP-oriented language". In Haskell, this isn't an obfuscated mess of wrapper functions and custom control flow, it's the most straightforward and sensible way to do things. Functions have very thin syntax and don't get in the way, laziness means you lose all the function wrappers that imperative languages add just for that, etc.

Personally I find trying to write Haskell in an imperative language to make exactly as much sense as trying to write imperative code in Haskell. Same error, same reasons, same outcome.

valenterry · 3 years ago
> Any engineer should be able to pick up your code and understand it pretty close to immediately.

So a webdev should almost immediately pick up a mix of C++ and assembly?

No. There's a reason we specialize. That is not a good argument against functional programming.

steinuil · 3 years ago
I have to agree with you as far as pure functional programming goes. I also don't like the Haskell approach of of overgeneralizing concepts just because you can; it's the FP equivalent of stuffing every design pattern you can into your OO code. I'd argue that not every pure functional programming language has to be like that but I'm pretty sure all the ones that exist are.
nailer · 3 years ago
Or post ES2017, without all the .then()s:

  const doThing = async (url, fallback) => {
    try {
      const response = await fetch(url)
      const notifications = await response.json()
      return notifications.map((notification) => ({
        ...notification,
        readableDate: new Date(notification.date * SECONDS).toGMTString(),
        message: notification.message.replace(/</g, '&lt;'),
        sender: `https://example.com/users/${notification.username}`,
        source: `https://example.com/${notification.sourceType}/${notification.sourceId}`,
        icon: `https://example.com/assets/icons/${notification.sourceType}-small.svg`,
      }))
    } catch (error) {
      console.log(error.message); 
      return fallback
    }
  }

still_grokking · 3 years ago
Is this code really equivalent?

Wouldn't it block on the `await` calls, whereas the original code would instantly pass control back to the caller?

(Sorry if this question is odd. My JS is a little bit rusted).

adamwk · 3 years ago
And now we’ve gone full circle to writing imperative code
toastal · 3 years ago
Totally agreed. Coworkers will hate you if you start mixing in these styles into an existing code base. If you want to do FP, go switch languages (and likely jobs/teams) to something where the ergonomic are clear, concise, and idomatic—then the benefits become more obvious. It's a bit like teaching object-orientation through OCaml… just because you can, doesn't mean the community recommends it generally.
agentultra · 3 years ago
Javascript is a multi-paradigm language as are most others people are mentioning in these threads. It shows in the specification for the language too: higher-order functions, anonymous functions, Array.map/filter/reduce, etc. Like it or not the language has facilities to enable programming in a functional style.

There are advantages to this approach like enabling different styles when appropriate.

There are disadvantages as well: you rely on the discipline of the programmers or tools to catch mistakes.

But there's nothing inherently wrong about thinking of programs in terms of FP ideas. There are other ways to think about programming than in terms of program counters, control flow statements, and procedures. It's not even idiomatic Javascript to write code that way!

JS is a more functional language than most people seem to think.

The tragic thing about TFA is that it's not an article but a chapter in a long series of posts and this is just one step along the way to showing how an FP style can manage the complexities of a non-trivially large JS code base.

tmountain · 3 years ago
I love FP, but I wouldn’t let someone introduce these abstractions into our JavaScript code base, as they ramp up the complexity in understanding code that can be represented in a simpler fashion with the same results.
Kerrick · 3 years ago
Your response describes exactly how I felt using RxJS for the first time.
dtech · 3 years ago
I've been working with Rx for 10 years or so and I think it's a terrible model.

If you need processing of streams of asynchronous dynamic ("hot") data it's the least-bad model I know, otherwise there are much better ways, especially now that many languages have async-await keywords or at least a Future/Promise type.

dmitriid · 3 years ago
It took the author of RxJava months to understand the concept. Screenshot from the book: https://twitter.com/dmitriid/status/811561007504093184

(Jafar is the author of Rx .Net, and even he couldn't explain it to the future author of RxJava :) )

azangru · 3 years ago
I just wanted to say, among all the negativity in the comments, that I love RxJS, and fell in love with it after Jafar's workshop.

As Ben Lesh often says, there are sadly a lot of misconceptions around Rxjs and the Observable type. The Observable type is a primitive so useful that it keeps getting reinvented over and over (React's useEffect is a weird React-only observable; a redux store is an observable); whereas Rx is a set of convenience functions to help with the use of the Observable type. If people are happy to use lodash, I can't understand what makes them so unhappy about Rx, which is like lodash, but for async collections.

antihero · 3 years ago
I found using rxjs with redux to be a way of decoupling control flow. Instead of having a function with a bunch of thunks, you did something and then other bits of code to could effectively subscribe to the side effects of that and you built your control flow up that way. It had pros and cons, was quite powerful but you ended up with a lot of indirection and not exactly knowing what was going to happen.
Cthulhu_ · 3 years ago
I've only been using RxJs for a short while now (we're moving to react soon), I don't really get it. I mean I get it, just not why we're using it just to consume some REST APIs. 80-90% of that is boilerplate code to please RxJs, and only alll the way down through two layers of stores / states and a library another team maintains is there a single call to `fetch()` that does the actual work.

I'm pushing to use react-query with the new app, I think people will get confused when it turns out hooking up the whole API will take hours.

briznad · 3 years ago
I read halfway through the article before my eyes glazed over and I could no longer tell whether the post was serious or a joke.
theptip · 3 years ago
Thanks for this. I got the same growing sense of discomfort reading the OP, that decomposing everything into operations makes the code way harder to grok vs. just doing the entire transform in one step.

I think the point being made was “each transform could be reusable across your codebase”, but I think for this example you really pay a high comprehensibility cost. And duplicating a few lines of code is often actually the right call, instead of coupling two unrelated bits of code that happen to be saying the same thing right now (but which will plausibly diverge in the future).

As a new Rustacean, I do really like Result and Option, but writing idiomatically in your language is really important too.

chris37879 · 3 years ago
Exactly this. I realized that a lot of how I use objects fits in nicely with FP patterns, but when I started looking into FP "best practices" the number of times the best practice is "Replicate this exact behavior from a 'more functional' language, even though your language has idioms for that" is astounding. I decided I'll just keep programming without side effects, if that's what FP is.
AtNightWeCode · 3 years ago
That example is what's called fluent code in OO in combination with the builder pattern. To experience peak builder pattern hell one should look at the Android API.

Deleted Comment

gehen88 · 3 years ago
I read the whole thing waiting for the aha-erlebnis which never came. I'm a full stack JS/TS engineer with a decade of experience. I expected this article to be written for someone like me. It didn't click, even though I already love and use functional aspects like immutability and pure functions. I feel like it's the whole new set of terminology that puts me off (and I'm talking about `scan` and `Task`, not even `Functor` or `Monad`). I have confidence I can learn and apply this in a few weeks, but I can't realistically expect junior/medior devs to quickly onboard into a codebase like that.

Maybe I'm biased against "true" functional programming because I've been on Clojure and Scala projects in the past (as a backend dev) and both experiences have been good for my personal/professional development, but a shitshow in terms of long-term project maintenance caused by the enormous learning curve for onboarding devs. The article talks about how great it is for confidently refactoring code (which I bet is true) but doesn't talk about getting people to understand the code in the first place (which is still a requirement before you can do any kind of refactoring).

My only hope is for ECMAScript (or maybe TypeScript) to introduce these OK/Err/Maybe/Task concepts as a language feature, in a way which befits the language rather than trying to be "complete" about it. We don't need the full spectrum of tools, just a handful.

piaste · 3 years ago
> I read the whole thing waiting for the aha-erlebnis which never came. I'm a full stack JS/TS engineer with a decade of experience. I expected this article to be written for someone like me. It didn't click

Don't worry, it's not about you. The article is genuinely underwhelming.

It walks you up the abstraction tree to the building of higher-kinded types, but then just handwaves it with 'and now you can do a lot of things!' but doesn't show them.

It needs a final part where the flexibility is displayed. Something like 'if (debugMode) runpipeline(synchronousDebugWriter) else runpipeline(promise)'.

fleddr · 3 years ago
I couldn't agree more. I feel that some thought leaders debating intellectual concepts in computer programming have no idea how real world software development takes place these days.

Developers are under enormous time pressure to deliver. They face an exponential skill curve as their scope is massively broad (i.e. devops). Things need to be shipped fast, time for "proper" engineering is compromised. Team members working on the codebase are of various skill levels and ever changing. Finally, a lot of products being worked on have a limited shelf life.

For 80% of developers worldwide, the concepts discussed in the article are too steep, and therefore unusable.

itronitron · 3 years ago
I've occasionally watched colleagues give presentations on functional programming over the years, and while I can see why certain people are drawn to it the stated benefits of functional programming have never seemed that significant to me. The advantages that FP provides aren't likely to be needed by developers that are capable of learning it.
zmmmmm · 3 years ago
Always makes me sad that Scala got sucked into the pure-functional priesthood type culture rather than the "better Java, by being mostly functional and immutable and then practical as hell when appropriate" pathway. I really like coding using Scala but the way I like to do it feels totally non-idiomatic.
gehen88 · 3 years ago
So true. I was involved in two very different Scala projects. One was the sensible "better Java" way, which was mostly great. The other was a big enterprise project with a core group of "hardcore" FP enthusiasts which was very stressful because of imposter syndrome and troubles to onboard new folks. I have been against Scala ever since, exactly because of this FP cult.
piaste · 3 years ago
> "better Java, by being mostly functional and immutable and then practical as hell when appropriate"

That's very much what Kotlin is aiming for.

davedx · 3 years ago
Scala tried to be too much. Too many paradigms. Too much flexibility and power. Many people might think they want that, but a subset are probably going to have an easier, happier life choosing a less powerful language...
ravenstine · 3 years ago
Maybe I'm wrong, but I think all the talk around weird ideas like Functors, Monads, etc., are mostly red herrings and aren't that applicable to most everyday software engineering tasks.

Just use functions, avoid state, use map/reduce if it's more readable, avoid OOP most of the time (if not all of it), avoid abstracting every single damn thing, and what you're writing seems functional enough even if it doesn't totally satisfy the academic view of what functional programming is.

PartiallyTyped · 3 years ago
> Maybe I'm wrong, but I think all the talk around weird ideas like Functors, Monads, etc., are mostly red herrings and aren't that applicable to most everyday software engineering tasks.

They are a red herring. In most cases, all you need to know about a monad is that it defines some kind of transformation of the enclosed data by applying a function onto it that returns a Monad of the same kind.

e.g. the list monad [a] says that if you bind it with a function f: a -> [b] (ie a function that takes a value and returns a list of b), the monad will transform to [b] by concatenating the lists.

the maybe monad Maybe[a] says if you bind it with a function f: a -> Maybe[b], if Maybe has type Some(a), the data of the monad is replaced by the result of the function. If the monad has type Nothing, then it retains nothing. It's no different to

a = f(a) if a is not None else a

So a monad is just an object that defines the transformation of the underlying data when applying a function that returns a monad of the same type, nothing more.

davedx · 3 years ago
OK/Err/Maybe can be trivially implemented with TypeScript, if the project development team wants them. We have it in the current project I work on and it works well with GraphQL.

For OK/Err, in my experience it kind of depends on "how happy is your dev team with using exceptions for general purpose errors"? The orthodox school of thought says "exceptions only for exceptional errors", in which case things like OK/Err give you a nice way to structure your control flow and its typings.

`Maybe` is used by `graphql-code-generator` to explicitly mark optional typings in generated TypeScript types for a GraphQL schema. I don't think it's necessary (TypeScript has `?` after all) but some people prefer it.

Cthulhu_ · 3 years ago
I've used patterns like that in Scala; I see their value in building a correct system etc etc etc, but only if it's consistently used throughout the codebase.

As it stands, most JS/TS projects aren't very consistent to begin with; error handling is either not done at all (let it fail), or a mix of exceptions, failing promises, error responses / types / states, etc.

But that's not really down to the language, more the underlying culture.

substation13 · 3 years ago
> My only hope is for ECMAScript (or maybe TypeScript) to introduce these OK/Err/Maybe/Task concepts as a language feature

When using these concepts the need for do-notation comes up pretty quickly. It would be like using JS promises without the async keyword!

Of course, follow this to it's conclusion and you will have a functional language.

Cthulhu_ · 3 years ago
I mean they could ADD it, just like nowadays individuals can choose to implement it themselves, but it wouldn't supersede any existing error / result implementations (success/error callbacks, throw/catch, promises which use both, etc).

To improve or change a language, I think you should get rid of another feature if it solves the same problem, instead of add another option.

jerf · 3 years ago
"I read the whole thing waiting for the aha-erlebnis which never came."

This is increasingly my go-to metaphor for this: This article and many of its kind are talking about bricks. They really like bricks, because they're square, and they come in several nice colors, you can really bash a clam with them, they're cheap, they're quite uniform, they make great doorstops and hold down stacks of paper really well, and they have a personal preference for the texture of bricks over other possible building materials. These people think bricks are great, and you should incorporate them into all your projects, be it steel bridges, mud huts, a shed out back, a house, everything. Bricks should be everywhere.

Then they build you a tutorial where they show how it looks to build a mud hut, and how nice it is to put some random bricks in to it. Isn't that nice. Now your mud hut has bricks in it! It's better now.

But that's not what bricks are about. Bricks are not about textures or being good at bashing open clams. Bricks are about building walls. Walls that may not be the solution to every wall, but certainly have their place in the field of wall building because of their flexibility, easy of construction, strength, cheapness, etc. Trying to understand bricks out of the context of using them with mortar to build walls is missing the point.

Contra the endless stream of tutorials that make it look like functional programming is essentially mapping over arrays and using Result/Option instead of error returns, that is not what functional programming is about. That is a particular brick functional programming is built out of. It isn't the only brick, and if you scan a real Haskell program, isn't even necessarily one of the major ones in practice. They turn out to be a specific example of a very simple "recursion scheme". These simple "bricks" show up a lot precisely because they are so simple, but generally the architecture layer of the program is built out of something more interesting, because "map" turns out to be a very small and incapable primitive to build a real program out of.

In my considered opinion and experience, if you spend time with "functional programming" and come away thinking "oh, it's about 'map' and 'Result'", the point of functional programming was completely missed.

And stop telling people that's what it's about! You're putting a bad taste in everyone's mouth, because when all the imperative programmers look at your so-called "functional" code in imperative languages and say, "That's a nightmare. There's all this extra stuff and noise and it's not doing anything very useful for all that extra stuff."... they're completely right. Completely. It is a net negative to force this style in to places where it doesn't belong, quite a large one in my opinion. And especially stop being sanctimonious about they "don't get it" when people object to this style. It is the one advocating this style where it does not belong that does not "get it".

The worst thing that can happen in one's education is to think you've been exposed to some concept when you in fact haven't, and come away with a wrong impression without realizing there's a right one to be had. I still encourage curious programmers to clock some serious time with real functional programming to learn what it is about. This style of programming isn't it, and your negative impressions of this style don't necessarily apply to real functional programming. (It does some, perhaps, but probably not the way you think.)

Verdex · 3 years ago
Part 1: The request

Do you have a writeup that you can point me towards that goes into detail about why functional programming isn't about map/reduce/filter and is instead about reconceptualizing your entire program as consisting of recursion schemes[1]?

I'm asking because I've been working with FP languages for 15 years now and the first time I've seen this point of view is from your comments. [Although, I suppose you sort of see a half formed version of this in the little schemer and seasoned schemer books. But just not enough so that I would consider it the point they were trying to make sans your comments.]

Part 2: Furthering the discussion

Of course personally, FP isn't a single well formed idea or philosophy any more than a hurricane is a well formed entity. Just a bunch of dust and wind going in the same direction. As with all other programming paradigms. I'm perfectly happy with the true soul of FP being some reconceptualization of a program into recursion schemes because my plan, as with all paradigms, is to pick and choose the individual conceptual motes and mix them together in a way that allows me to best solve my problems.

I actually dislike what I think you're saying recursion schemes are for a similar reason as to why I dislike excess shared mutable references and loops with excess mutation. It places the programmer into a sea of dynamic context that must be mentally managed in order to understand the meaning of the program. Meanwhile, map/reduce/Result, places the programmer into a static reality where all meanings have computer verifiable proofs associated with them.

My version of FP doesn't have recursion or loops. Just map/reduce/ADT and functionality that allows you to convert recursive data into lists and lists into recursive data. Maybe that doesn't make it 'true' FP. Which doesn't bother me.

[1] - https://news.ycombinator.com/item?id=33438320 > Reconceptualizing your entire program as consisting of recursion schemes and operations that use those recursion schemes, what I think the deep, true essence of functional programming as a paradigm is

Deleted Comment

wokwokwok · 3 years ago
I’ve had similar experiences with scala and clojure professionally. I now actively oppose people attempting to add functional code to projects I work on.

…because when they say “more functional” most people mean:

I want less code.

I want the code to be shorter, because I’m lazy and I want it to be all on one screen.

…but that’s actively harmful to almost any code base.

You want simple code, not dense complicated code. Dense complicated code is for people who wrote the code, and a few smart talented people. Other people have to work on the code too. They cannot.

Actual functional code doesn’t strive for code density, it strives for code purity and algebraic structures.

That’s fine. Do that.

Dense map reduce reduce flow reduce functions can die in a fire.

rfrey · 3 years ago
I think you're conflating "readable" and "uncomplicated" with "familiar". I'm equally infuriated by OO code with dependency-injected-everything from some hidden framework configured by fourteen XML files somewhere in a different file tree, interfaces for every single class even if only instantiated once, factories to create builders that make adapters.

Maybe if I stared at it for twelve years it would become familiar and I would begin to think it was simple, readable and maintainable.

sunwukung · 3 years ago
I think this is about the level of abstraction. As React component extraction is to tag soup, so named functions composed are to fp primitives. In code reviews, if I see a big swamp of pipe/fold/cond etc at the top level, I'd kick it back and ask that to be wrapped in a named function that explains what it does, rather than exposing it's guts.

Writing concise, clear code is a skill that straddles any paradigm.

bigDinosaur · 3 years ago
Pretty poor attitude to just adopt so generally. I've seen 'actively harmful' qualities from all paradigms. Once peoples start adopting attitudes like yours they've just become the mirror of the condescending FP type and just kill outright any of the really cool features that are useful, as well as any discussion of them.
delta_p_delta_x · 3 years ago
Perhaps the snark caused the down-votes, but your point is legitimate. 'Pure FP' languages encourage code that is nearly unreadable and unparseable without any additional context (and sometimes, unreadable even with said context). There is some strange desperation for extreme terseness in pure FP languages like Haskell, Ocaml, Idris, etc.

Single-character variables and functions, point-free style... Coming from an imperative background, this just seems like flexing for the sake of flexing.

Why not make life a little easier by clearly naming variables? This isn't maths or physics where variables (for some equally-inane reason) must be one character. We have 4K screens today; I can accept lines that are significantly longer than 80 chars.