What’s so great about functional programming anyway?
jrsinclair.com
jrsinclair.com
The pipe feels like a block of restricted statements, `peekErr` and `scan` recreate conditionals, the Maybe monad behaves like a try-catch here, etc.
Of course there are differences, like `map` working on both lists and tasks, as opposed to a naive iteration. That's cool. But this new programming language comes with limitations, like the difficulty in peeking ahead or behind during the list iteration, reusing data between passes, and not to mention the lost tooling, like stack traces and breakpoints.
I've written many (toy) programming languages, the process is exactly like this article. And it feels awesome. But I question the wisdom of presenting all this abstract scaffolding as a viable Javascript solution, as opposed to a Javascripter's introduction to Haskell or Clojure, where this scaffolding is the language.
I've worked on some big TypeScript code bases that make heavy use of sort of FP the article promotes; dealing with stack traces and debugging the code is incredibly painful. Jumping around between 47 layers of curried wrapper functions and function composers that manage all state mostly in closures is a real drag.
Until the tooling is better, I can't recommend these idioms for real work.
TBF, there is a kind of beauty to the approach and as you really dig into it and understand it, it can feel like a revelation. There's something addictive about it. But any feeling of joy is obliterated by the pain of tracing an error in a debugger that isn't equipped to handle the idiom.
This leads to fewer errors than the imperative/object-oriented paradigms, and greater development velocity (and quicker developer onboarding, and better tools...) than the 100%-purely-functional strategy.
Hopefully, over time, we'll get functional programming techniques that can easily model more and more of real-world problems (while incurring minimal programmer learning curve and cognitive overhead), making that remainder get smaller without extra programmer pain - but we may never eliminate it completely, and that's ok. 100 lines of effectful code is far easier to examine and debug then the entire application, and our job as programmers is generally not to write purely functional code, but to build a software artifact.
The above applies to type systems, too, which is why gradual typing is so amazing. Usually 99/.9% of a codebase can be easily statically-typed, while the remaining small fraction contains logic that is complex enough that you have to contort your type system to address it, and in the process break junior's developers' heads - often better to box it, add runtime checks, and carefully manually examine the code then resort to templates or macros parameterized on lifetime variables or something equally eldritch.
(and, like the above, hopefully as time goes on we'll get more advanced and easier-to-use type systems to shrink that remainder)
JS with Ramda (and an FRP library, if needed) is the sweet spot for me. I use a lot of pipes() but usually don't break them down into smaller functions until there is a reason to; but FP makes it trivial to do so.
I couldn't come up with a succinct way to say exactly this, since this is very much how I am with it right now. Thanks for that.
I will add I'm comfortable with, and prefer, map/select/reduce over for loops where provided.
That said, you need these things less when you don't have mutable state and you do have referential integrity so you can zoom in on the critical path of side effecting code.
The only problem I encountered regularly was that bugs did only manifest in the optimized versions not in the debugged ones.
I would strongly object this.
Have you seen ZIO?
(Without tooling the stack traces can be noisy.. but that's incidental in this discussion context)
It took me years (and Elixir) to get past the "here are some words, some mean what you think, some mean something completely different to what you know, some mean almost the same thing. Also here's 30 overloaded operators that you have to juggle in your head and we wont introduce things like variables for another 4 pages." without running back to something imperative to actually build something.
Functional programming's great when you have immutability to drop an entire class of bugs and the mental energy that accompanies that, can pass-around and compose-with small functions easily which makes writing, testing and figuring much simpler and can make your brain click that "its all data" without trying to over complicate it by attaching methods to it.
Honestly I think that's 90% of what the royal You needs to understand to get FP and actually try it. Selling people on "its all stateless man" or other ivory tower tripe is such garbage because (as the tutorials always point out) a program with no state is useless, and then the poor shmuck has to learn about monads just to add a key to a map and print something at which point they're asking "how was this better again?"
I think a lot of the friction you mentioned comes from learning imperative programming first. Our vocabulary, tools, and even requirements come with an implicit imperative assumption.
PS: I didn't downvote you, though your tone is harsher than I prefer for HN.
A hundred percent. FP for me, is "let's stop gluing state and functions together for no reason" and get all the benefits above. I understand pure functions, avoiding state, closures, and using first class functions as the basic building blocks of creating higher level structures.
When it becomes "a functor is a monoid of the schlerm frumulet" - ala the typical FP advocacy blog / latest React state management manifesto masquerading as library documentation, I zone out. The odd thing is, I don't feel I've lost anything by doing so.
The two main FP things that you need to learn to do Elixir well (IMO) are thinking about data structures first, and purity. Choose the right representation of the data at each stage and then what functions do you need to move between each. Make those functions side effect free (but not obsessively). Then you put your stateful code in GenServers and you have useful real code and most of it was incredibly easy to test.
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.
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.
If you are doing a lot of work inside those branches it may be worth a refactor to simplify the branching logic.
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.
Surely this would cause some strange performance outcomes with, say, a gradually built up immutable list.
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.
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.
Stuff like this is the reason why functional programming is synonymous with inefficiency and why Real Programming™ is still done in languages like C.
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.
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.
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.
Use the best tool for the job.
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.
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.
I love your answer.. And I love how you can use it in every language.
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.
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!
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.
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.
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.
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.
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.
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.
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.
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?
You dismiss or disagree, because you don't like the author's attitude?
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.
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.
But I think the basic premise of combining functional and relational is valid. At the very least it avoids the object relational impedance mismatch.
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.
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.
- 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.
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.
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.
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
Isn't that simply because you're a Java developer (as I understand from the rest of your post)?
> 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?
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.
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.
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'.
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...
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.
It feels egregious to implicate "the entire surface area of functional programming" here, when there are other obvious issues at play.
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.
This phenomenon seems to happen more frequently with FP or related tools and languages (RxJs abstraction soup comes to mind).
But their scope can be limited.
Do you have a link to a good one?
[1] https://elixirschool.com/en/lessons/basics/pipe_operator
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.
“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.
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?
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.
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.
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.
- 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.
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.
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).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...
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.
This is "safer" for any code still operating on the original object, as it will not be changed unexpectedly.
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.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, '<'),
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.
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.
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.
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.
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!
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.
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.
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.
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.
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, '<'),
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
}
}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).
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.
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.
(Jafar is the author of Rx .Net, and even he couldn't explain it to the future author of RxJava :) )
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.
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.
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.
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.
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)'.
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.
That's very much what Kotlin is aiming for.
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.
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.
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.
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.
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.
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.
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.)
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
…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.
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.
Writing concise, clear code is a skill that straddles any paradigm.
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.
The benefits of FT are mostly things like statelessness, no side effects.
And it's almost better to talk about it in those terms, and introduce concepts into imperative programming.
When people say 'functional core (aka libs) and imperative for the rest' it's ultimately what they mean.
Applying FP universally creates weird code and it becomes pedantic and ideological.
I wouldn't even use the example in the article almost nothing is gained.
Try to reduce state, use immutable objects as much as possible / where reasonable, make reusable function which naturally have fewer side effects etc..
I think that the material benefit of FP is in partial application of some elements of it, and unfortunately that doesn't create nice academic threads for people to talk about. It's more like applied know-how, than theory.
I skimmed through this, and came across this gem:
> If there happens to be an error, we log it and move on. If we needed more complex error handling, we might use other structures.
Yes. Yes, we always need more complex error handling. So, show us the "other structures". It may just turn out to be that the regular chain of `.map` calls with a try/catch around it is easier to understand and significantly more performant than any convoluted pile of abstractions you've built.
And then you'll need to handle different errors differently. And then you'll need to retry some stuff, but not other stuff etc.
> this is a sample chapter from my upcoming book: “A skeptic’s guide to functional programming with JavaScript.”
The problem is: it does a poor job of convincing skeptics why this is great, or even useful. I use, or used to use, functional programming, and I am not convinced by this chapter.
“Left as an exercise to the reader”. Reminds me those well-known tutorials with todo lists and other trivial nonsense, because a fully-fledged example would seed doubt of selling points instantly.
Sometimes I think that our area is one big IKEA store. You look at nice pictures, buy into the crap, and still feel okay because you’ve built most of it yourself. Not getting that this built-yourself part makes you relate to it much more than the initial advertisement or the practical value.
Or "draw the rest of the owl" :)
When I'm learning a new concept, I need to be able to clearly see the steps. Even if I do understand how to read and write in the abbreviated coding style, the extra step of mentally decoding it into it's more verbose form takes mental energy away from the absorption of new knowledge.
Clearly, this book is written for an advanced audience, but at the same time it's trying to teach an unfamiliar concept to that advanced audience.
Does anyone else here share my sentiment?
It makes me think of this software I saw once that would take a math-problem, and solve it for you, showing you the steps along the way to the solution. I want something like that for this kind of code.
The best way to improve this post would be to append comments to each discrete code section, explaining clearly what other code sections it interacts with, what it's intended to do, and any other notes needed to clarify the functionality.
This was the main thing slowing me down when I started to learn Clojure last year.
I'm now learning Emacs and and Elisp and I find the documentation even more difficult to comprehend somehow.
Two examples: Literate programming with org-babel: almost none of the documentation that I can find shows "real" examples with multiple header properties or variables being set for an org-babel file. I don't know how to mix and match them. The docs tend to show the most minimal example and I've been spinning my wheels for days trying to figure out how to integrate into my workflow.
Another elisp example: using auth-sources.el for handling credentials. I'm trying to enhance a few major modes to add support for storing server credentials, and none of the examples show how to actually prompt a user for a username and password to save it for later in the secure key store. I've checked other projects, and people do it in different ways, and they all seem needlessly complicated, with core auth logic embedded directly in unrelated function calls.
Compare this: https://www.gnu.org/software/emacs/manual/html_mono/auth.htm...
To this: https://pypi.org/project/keyring/
Edit: I'd like to add, I am loving Emacs and Clojure. I think it's worth the slog, but there is room for improvement for making a lot of these systems more approachable to the complete newcomers.
But you DO want these single responsibility methods like sanitizeMessage()! Having single responsibility functions like this means you have more Legos you can use where you need. You get improved code reuse. Perhaps more importantly, you also get functions which are easy to test and test more thoroughly than if you have a big procedure which does a lot of things.
People only seem to convert one way and not the other. And is it really a surprise that the vast majority of people resist change / doing things differently?
When I program, I like to use:
{ functions, types }
When FP gets bolted onto a mainstream language after the fact, you get to program with: { classes, mutability, side-effects, nulls, functions, types }
Then the FP skeptics think it's too complicated, and just want to go back to: { classes, mutability, side-effects, nulls }In other words, people only convert one way because it's the only way most people can convert.
I didn't really find returning to Java to be an encumbrance at all, and really enjoyed how productive Spring Boot made me. I still love Scala and if you don't fight it and use it long enough, the crazy sounding FP/category theory stuff starts to come up naturally and seem very comprehensible.
My takeaway is always avoid known bad practices, but otherwise to cut with the grain. Write Java like Java, JavaScript like JavaScript and Scala like Scala.
The biggest problem in software development is the slow down of value output over time caused by accumulating complexity. Anything that can help keep complexity down is a big win for long term value output.
Functional programming, in my experience, created much more complicated code because it seems like the authors like to be a clever as possible.
But granted it needs to be part of a broader pressure to keep things simple, otherwise devs will like you say, find a way to overcomplicate.
One can mix functional and OOP code well by compartmentalizing the parts of the code that are in OOP style, and situating it in an overall functional context. For example, the methods of a class can certainly be written in a very functional style, and classes themselves can be instantiated within functions.
The mark of a skilled developer is to know which paradigm is the best to solve a particular problem, just like the choice of a data structure can greatly affect simplicity of code to address a problem.
Some context. I'm sure I'm biased by a number of factors. One is that I started programming a long time ago in C and I was passing around structs with data and function pointers, so objects. Another one is that I used a number of different languages, currently Elixir, Python, Ruby, some JavaScript, often in the same week for different customers: I like easy to understand code because I this kind of projects with sparse teams is easier to maintain than layer over layer of heavily engineered one (so factories, metaprogramming, etc.) Third, I end up using a minimum common denominator of all those languages plus inescapable and/or great features: I wish all languages had Elixir's pattern matching. This approach works and it reduces the context switch between projects.
So, when I'm coding in Elixir sometimes I have to create large data structures and pass them around, piping them to Module1.fn1, Module2.fn2, etc. It's more or less the same than composing methods in OOP, just different syntax. Sometimes those modules become bags of functions too. What's more difficult to do in OOP is to engineer a class to accept arbitrary meaningful methods. In Ruby I often code methods that return a hash (Python dict, JS object) and create a pipeline of .map.map.map in their caller, like a |> pipeline in Elixir.
I prefer mixed paradigm approaches, where one can create the few classes needed to represent the few data types that are objects (Elixir's and Erlang's GenServer) and code everything else as functions. I have a couple of Django projects where the only classes are the mandatory database models and management commands. Everything else is functions in modules. That would be much more pleasant to code in Elixir (if it had classes instead of those noisy and uninformative handle_call, handle_cast, start_link.) Ruby has modules too but for some reason customers really like to create classes for everything, even two method ones like
class Command
def initialize(args)
...
end
def run
...
end
end
Command.new(args).run
which are quite funny to look at. That should be command(args) - Some kind of data/information, in some (digital) form of one structure or another.
- Methods, processes and operations that transform, transport and do other things to that data.
For example, in OOP, those two elements are generally combined together in organisational units ("objects"); on the other hand, in functional programming they are strictly separated.Each of those paradigms - and many others - are just tools in our arsenal to achieve our goals, and neither is intrinsically better than the other. Depending on the context and environment that we are operating within, each has its advantages and disadvantages; some are better suited than others in certain cases.
For me the question still remains: how is any of this better than good ol' reliable `for` loop?
Which inspired this great cartoon: https://twitter.com/nice_byte/status/1466940940229046273
The argument I read in this article seems to be going in that direction, but I'm not sure it is being presented clearly. The semi-monoid functors are a fair way down in the weeds and not really what people should focus on when deciding why functional programming is great. Unless they really like abstractions in which case more power to them.
const newData = notificationData.map(ev => {
return {
username : ev.username,
message : ev.message.replace(/</g, '<'),
date : new Date(ev.date * 1000).toGMTString(),
displayName: ev.displayName,
sender : `https://example.com/users/${ev.username}`,
source : `https://example.com/${ev.sourceType}/${ev.sourceId}`,
icon : `${iconPrefix}${ev.sourceType}${iconSuffix}`
}
});here is where one gets into trouble, reading functional code. while it may look better when you write it, try reading your own code after a few weeks. you will appreciate it much more when your control flow in a significantly large enterprise application is through bunch of ifs and imperative loops than scattered across various functions and operators.
another problem, debugging. when your inputs cross various function boundaries, no matter how smart IDEs you have, you end up messing around adding impurities to your pure functions and adding watches with conditions all over the place.
lastly, all that lux code does hide a significant problem. Performance. I don't know about javascript but all those temporary objects one keeps creating for the pursuit of statelessness is maddening. i don't know if there is anything characteristically different about a language like Haskell which handles this problem. Does it end up creating new objects every time for e.g. a map is called like in this example to update all the attributes?
No. This is optimised away.
It's yet another reason why it's frustrating to see FP-like things bolted onto mainstream imperative stuff. You can't optimise away 'mostly functional' code. Wrapping a Set with an UnmodifiableSet isn't the same thing as (f x) + (f x) == 2 * (f x)
Author describes functional programming zealots and seems to imply it's all annoying because of all the abstractions, then proceeds to do exactly that while calling it "the good parts".
Functional programming definitely has its use cases, as side effects can introduce bugs. Unfortunately real world business logics often does require side effects. Often you only find out about side effects later in a project, and then you suddenly need to start hacking your clean functional approach.
Yeah, all business logic requires side effects, else it is doing nothing :).
But following some FP practices, you can push the side effects very far out to ward the edges. Instead of typical OOP, where objects get passed around and mutated anywhere and everywhere (which makes identifying where certain things started to go wrong), with pure, single responsibility functions you can write very simple but thorough tests which give you vastly more confidence about the code. And as a huge added benefit, the lines of test code actually go down because there's so much less setup and teardown (mocking, stubbing, etc.).
> real world business logics often does require side effects
> you suddenly need to start hacking your clean functional approach
These are great arguments as to why you need:
a) A type system which tracks effects
b) A language which makes refactoring safe and easy
Functional is great in cutting amount of repeatable code. It is great for writing tools to process things regardless of their exact type and for writing infrastructure code.
Object orientation is great when you want to model the domain of the problem. Things that actually make sense when you are talking to non-developers at your company. Clients, contracts, appointments, employees, teams, invoices, things like that. But when you start making objects that do not represent anything meaningful you probably went a bit too far.
I think knowing various paradigms, knowing when to use which and how to marry them together is critical for maintainable implementations.
Things also generally compose together more easily than in OO. There's other benefits too, but these are my main ones.
So, once you set up this invariant groundwork, you free your mind from thinking about certain problems, as the compiler catches them for you.
This isn't to say these things can't be done in OO (they have been done), but they're usually not enforced to the same level, as it's normally a library, not native functionality
In fact, some of the more popular functional languages are untyped - those being Clojure, Erlang and Scheme.
There was nothing more elegant about it and it wasn't obvious from just looking at the function call what the function was doing. It wasn't any simpler than while(condition){}.
All because he didn't like while loops (and statements in general). It seemed tortuous and unnatural.
FP has some good parts but so does imperative programming, and I think throwing away the good parts of imperative programming and OOP for the sake of "pure" FP is a serious mistake.
I can perfectly imagine the most intelligent and smart people I've ever meet saying this same thing.
Aaaaaaaanyway, in it he talks about two ways of how we function as human beings, and I'm likely presenting this wrong, but one is basically the best practice theoretical mode of being and the other is how we operate at 17:00 on a Thursday after a week where both our children have been sick, and the overlaying message is that everything that isn't designed for the second mode of being is likely going to fail. Now the way it's presented in the research and the educational material, this has nothing to do with programming. It's more along the lines of companies needing to formulate missions and goals that aren't corporate bullshit, because nobody understands corporate bullshit when they are faced with an angry customer some late Thursday afternoon.
After a few decades in SWE, however, I've become sort of a fan of designing software for that 17:00 Thursday EnKopVand mindset, and functional programming helps a lot in that regard because it kills soooo many of the complexity pitfalls that you really don't want to deal with when you're tired, lazy and incompetent. Of course the other side of this is that I'm not religious about functional programming either, I rarely write classes these days, but if there is a good reason to write one, I will.
That was when I learned that for some people, a monad really just is a monoid in the category of endofunctors.
It's just fake humility. The same people would get offended if you actually suggested they were lazy and incompetent.
[0]: https://bonsai.red
Chastised, Anton took his leave from his master and returned to his cell, intent on studying closures. He carefully read the entire "Lambda: The Ultimate..." series of papers and its cousins, and implemented a small Scheme interpreter with a closure-based object system. He learned much, and looked forward to informing his master of his progress.
On his next walk with Qc Na, Anton attempted to impress his master by saying "Master, I have diligently studied the matter, and now understand that objects are truly a poor man's closures." Qc Na responded by hitting Anton with his stick, saying "When will you learn? Closures are a poor man's object." At that moment, Anton became enlightened.
- Anton van Straaten
Strawman! Most coders, even staunchly “imperative” ones care a lot about tech debt, tidyness and code quality. That is why things like SOLID and design patterns are popular. For better or worse good programmers are proud of their code, and I don’t meet too many who feel they need to “ship faster”. Product people might want that though.
Bob: What about feature x?
Alice: Oh yeah, well we can add that! It has proven useful.
Bob: Agreed! But now do you see the need feature Y?
Alice: Hmm good point. Feature X would be better with Y. Let's add that too.
(Repeat n times)
Alice: Isn't my language pretty nice now?
Bob: I have to agree, it's very productive
Alice: See I told you, we don't need an FP language after all!
Bob: ...
* https://leanpub.com/sofp is impressive but is in a different category
* https://www.manning.com/books/functional-and-reactive-domain... is the only one I know which was trying to discuss building real life software
As a current reader of fsharpforfunandprofit.com, I'd like to see modern C# comparison with F#. C# has introduced decent pattern matching, records, nullables. LINQ was introduced long time ago - many things that make F# possible. Here is some comparison, but its 10 years old without the goodies C# offers today: https://fsharpforfunandprofit.com/posts/designing-for-correc...
There are other valuable things, but IMHO the key is that, like OO programming, they are valuable sometimes, not always. Being explicit what is mutable and what can produces side effects is really good, but forcing everything to be immutable and without side effects, is not.
So, while I like some things about functional programming, I'm not a fan of pure functional programming. I'd rather have a language that supports mixing multiple paradigms, even though that means giving up some things that are avaliable only in pure functional languages.
> Being explicit what is mutable and what can produces side effects is really good
!!! That's what Haskell is !!!
What language are you using which is explicit about side effects?
const dataForTemplate = notificationData
.map(addReadableDate)
.map(sanitizeMessage)
.map(buildLinkToSender)
.map(buildLinkToSource)
.map(addIcon);
is the best one, it has way less mental overhead comparing with the end result.Also a catch with no error handling
try {
return Just(JSON.parse(data));
} catch () {
return Nothing();
}
in most cases is not the best idea, at least I'd put a log about itNevertheless, waiting for the book from the author
UPD: edited code formatting
const dataForTemplate = notificationData.map(pipe(
addReadableDate,
sanitizeMessage,
buildLinkToSender,
buildLinkToSource,
addIcon
))
> in most cases is not the best idea, at least I'd put a log about itWell, in FP you don't want to do side effects like write to console. Instead you want to hold onto the errors (with an `data Either e a = Left e | Right a` instead of `data Maybe a = Just a | Nothing`) til you get to the part of your application where you do do the side effects.
try {
return Right(JSON.parse(data))
} catch (err) {
return Left(err)
}With just a few lines of mini-framework definition code, you can define a Processor system which behaves like a pipe+reducer but takes a simple data structure of initial value, functions to call with that data (and optional additional params), with the result of each step fed to the next. But in the framework you take the output of the previous step and update the results key within a process hash. Or if the function fails, you can accumulate errors in an errors key-value pair. Steps can have flags, such as :hard-fail, etc. which the Processor can use to affect special behaviors (like a rollback or a termination) based on errors in steps.
This enables really dynamic programming options as the transformations to be done on the data can be determined at runtime based on formulas or process generators.
It might seem that such an approach would make debugging and troubleshooting more difficult, but because you can add additional metadata and debug behaviors to the Processor steps data, tracing exactly what has occurred in very easy.
The only downside I've found from this approach is that IDEs may not "see" your call to functions as they are just references or strings/symbols (depending on the language).
Also JavaScript biggest advantage is that you can mix up. And it’s biggest disadvantage is that non experience developers will mix up too.
Also you need to cloning multi level objects.
To work 100% FP in JavaScript you need the libraries and you need to have a flat data structure every where.
I don’t know how many JSON APIs you see who work like that.
JavaScript has also a rich set of language features you can use to save your time and still have quality code.
Erlang, Elixir, OCaml, Clojure, (Hell, even Ruby.)
The problem with the current state of JS and Its popular libraries, is that the convention is to write OOP and Imperative code. Unless your entire team is onboard, its causes a lot of issues.
I feel like js-transpiled languages like Purescript, Rescript, ClojureScript, ReasonML, etc do a really good job of allowing Devs to write functional code on the frontend, while leveraging the JS-lib ecosystem.
I personally have a hard time reasoning about while and for loops, etc, and mutable variables, my monkey brain can't keep track of that many spinning plates.
I didn't start as a skeptic and really think functional programming looks beautiful and is very useful in preventing side effects. However, in most practical situations I've found over the years functional programming to be harder to understand and debug when stuff goes wrong.
In javascript in particular, functional programming is very nice but at the same time usually way more ineffecient so for many tasks I tend to go back to normal for loops just because it's that much faster.
I had a collegue that wrote a lot of functional code and his code was extremely hard to follow and understand. Even if it was beautiful figuring out where the system went was the hard part and I had to go to the function decleration, understand it, move on to the next until I found whatever the issue was. When I found the issue, I couldn't easily fix it because the next function called was expecting the data structure to behave in a specific way so for one issue once I had to change basically every function called before and after which was way more tedious then if it would have been more sequential.
I don't know. There is something to say about functional programming but lately I have kind of tilted back into the more standard approach. Functional programming is a lot more beautiful in practically every implementation I've seen but the standard approach is, in my experience, usually more useful in practical terms.
That's not a problem with functional code, it's about badly designed abstractions. You get the same issues in garden-variety OOP codebases, only to a far greater extent.
Most of my functions though are void functions (returning null or undefined). It’s not essential to me that functions must return a value, in defiance to functional programming, so long as like instructions are grouped in a single container that may be called by reference.
Storing data as discreet changes and relying on pure function selectors to calculate values is wonderful.
It's not always a viable approach at scale (at least not for my ability in certain circumstances) but, when it is, testing is a breeze and I love being able to debug forward/backward in time with consistent results guaranteed.
I am sure I'm missing something here. But I don't understand why I would ever need to use code like this. My clients and I value shipping code as fast as possible. This just looks like it would turn my 1 hour job into 2 hours with not much benefit.
Anyone who doesn't have a lot of experience with JavaScript or functional programming will have a really hard time reading and understanding my code. I've been coding for a long time now in JavaScript but this looks like a lot of complex code to me for simple tasks.
I want to love functional programming because everyone is talking about it. But what am I missing? What would be the benefit of recreating a language inside a language?
Perhaps, but if I understand this article correctly, I must first know and understand all these laws and tools before I can have that confidence. Meanwhile, the arcane abstractions don't exactly inspire much of that confidence.
Look, I love what someone else here has already dubbed "softcore FP". I love chaining functions that pass immutable data structures to each other. And I fully admit I've written some highly abstract code that looks quite a bit like some of this. But does it inspire confidence? Quite often the effect is that you've created your own language that others don't know, which means your code will be harder to understand for others, and they may lack confidence in the code, and not dare to maintain it properly.
I want to believe that this direction is the right one, but I lack the confidence that it really is.
I have no good way of describing what I mean but I tend to think about functionality not things/items first I guess.
Or in other words, OO code ties business logic to presentation logic. By sticking to the dominant 'one class per file' principle, the same code behaves differently based only on whether it appears in one file or two, so it's not trivial to move code around to make it more organized.
When I write functional code, I first write all the functions and types I need in a single file, without worrying about presentation or naming until it compiles. Then, before committing, I reorganize them into folders, files, and modules so it's easier to read and navigate, and I can do it any way is more appropriate (sometimes layer-first is more readable, sometimes domain-first).
I can also split pure functions into chains of smaller, pure, private functions if they're too long to follow (90% of the time some functions end up being way longer than I expected), which is _way_ simpler than splitting a large class into smaller ones.
For any kind of I/O, though, FP is not even applicable in the pure sense of a function, as disk/network access is inherently a side effect. JS can of course mask much of this with its Promises.
Any kind of business-oriented logic, especially handling of different kinds of data entities, is still good to encapsulate in classes in OOP fashion. Classes are a good impedance match for entity types, as instances are to entities.
Sometimes, though, data transformations are still more clearly expressed in imperative style. At the very least they’re more accessible to juniors, unlike the innards of declarative transformation engines.
Imperative, declarative, object-oriented, functional — these are all tools for different jobs.
If you meant to do it, it's an effect, not a side-effect.
It seems like one goal of this style of FP is to abstract away control flow itself - why bother with if-statements when you can compose automatically short-circuiting monadic APIs like Option and Result? An admirable goal that I am 100% in favor of. However I feel like it might also obfuscate untested branches in source code and artificially minimize computed cyclomatic complexity if the tools are not aware of these control flow abstractions.
I guess it's not super important in the grand scheme of things (I'm well aware of cyclomatic complexity's dubious usefulness as a metric).
React itself certainly can be functional, but... (hot take) functional programming without the part where you actually make state changes is - drumroll - useless.
If it ain't changing state and there are no side effects - no one gives a fuck. Because it literally isn't doing anything.
Turns out computers are fundamentally state machines. Functional programming certainly has some nifty ideas, and it can be a helpful way to reason about code that will lead to state changes - but the part people want is the state change.
To me, this sort of FP seems to have been tried and have failed. I don't know how much of this is due to the language and how much is due to the pattern not working well in the minds of developers. I wonder if JS had an implementation of something like C#'s LINQ if it would be easier to write code this way.
When I actually have this problem, I usually define the input as an interface and the output as another interface(maybe extending the input interface).
Then I write a SINGLE function that calls those other functions, collects the data into variables, and then builds a new object of the new data type, and then returns that.
Maybe I'm missing the point here.
IMO, chaining functions together rather than mutating objects and variables seem to be the most Pareto efficent takeaway from FP (at least in the Scheme/Clojure style, haven't tried F#/OcaML or Haskell). And HOP, though Python has native support for map and reduce and filter in the last few years, so great enough.
And to be honest when I try to do something complicated in Clojure I end up blowing the stack anyway. Recursion is a bit too hard to grasp for non-programmers like me.
So the model of the future, in my opinion, should look more like https://www.val-lang.dev/
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2022/p26...
This is less restrictive and also very scalable IMHO. Value semantics, basically.
It's how things work at scale too.
Source control? Git
Money? Ledgers
Distributed computing? Map-reduce, Raft
the thing i loved about FP was the elimination of exception handling
the thing i disliked about FP was how complex classes were implemented as bizarre templates of structured list subtypes - too much focus on the data types and structures of the implementation, obscuring the domain-model (aka business) types and structures required for human comprehension.
disclosure: scala, a few years ago
there is a happy medium...theoretically
I'm not that great at functional programming, and perhaps this is a non-issue, but I feel there could be some way to achieve the same results without losing that ability.
EDIT: That being said, I really want to read this book!
Three main things of functional programming:
1. Keep your data and function independent
2. avoid changing state of variables, instead declare others
3. accept function/reference as a param
I love FP and I think it makes writing robust software easier. However, I don't think the benefits can be explained well. The only way to see it is to give it an honest try - in a true functional language. This is too much of an upfront commitment for the unconvinced.
I’m guessing this may be seen as a part of FP but not the core?
But I expect everyone in JS to be comfortable with `Array.prototype.map`, `Array.prototype.reduce`, and closures as these are core tenants of the language.
you should read the previous articles from the author, eg. https://jrsinclair.com/articles/2022/javascript-function-com...
then
https://jrsinclair.com/articles/2022/what-if-the-team-hates-...
it would address some of the concerns and critiques ppl had on this topic here.
Rust and Haskell are the only languages I know which help with this.
It is very good at describing the kind of objects we tend to care about, so it is sensible to consider the converse and ask what kinds of things is it good at describing.
how naturally we can express our code is a very strong measure of how well we understand it. Both in terms of what it can and cant do.
Imagine having blocks that stick together like legos, but you cant see the dot lattice because your vision is too poor. FP is about seeing the dots.
const x = await promise1(someArgs); const y = await promise2(someFunction(x));
we do
function processor(monad, args) { return monad.unit(promise1(args)).map(someFunction).then(promise2) }
That's all about monad.
Well, nothing. It is one of many paradigms. It has its place just as the others.
- OOP developers, as their languages gradually port all functional programming language features
Practice some "Feynman technique" on your subject, or resign yourself to producing a suggested reading list of works that actually explain the topic, or please for the love of cognition stop masking signal with your noise.
(allows (ideas (elegantly(expressed))))My opinion is that if you were to summarize the original article to only one of its words, it would be the lovely word "boffin". The right amount of boffinry can be a rewarding and beautiful thing. Sometimes though "C'est magnifique mais ce n'est pas la guerre".
If your priority is to ensure that similar concepts are grouped together inside flexible and composable modules, then co-locating related state and logic is necessary.
Unless a module can fully encapsulate (have full ownership over) the state which it operates on, it cannot fully separate its concerns from that of other modules. If you have a global state store and any module can potentially read from and write to any part of that state, then you are bound to end up with modules which have overlapping responsibilities - These 'modules' won't be very modular or composable because, whenever multiple modules share the same state, they invariably end up having to have an awareness of each other's behaviors (to avoid writing conflicting state changes or to know when they should update themselves).
Underlying FP philosophy is the notion that there is no value in encapsulation; that there is no value in assigning exclusive ownership over specific state to a specific module; that such ownership is dangerous because it makes the module unpredictable from outside. What FP proponents fail to realize is that once they've rejected the philosophy of state encapsulation, they've implicitly accepted the philosophy of state multi-ownership. Whenever you have shared ownership of state, it becomes unclear who is responsible for that state; this leads to potential conflicts. Avoiding conflicts forces modules to become aware of each other's existence and interactions and this reduces their modularity.
The functional programmer's need for modules to be fully predictable with each interaction was born out of a need to micromanage them. On the other hand, OOP proponents understand the value of module autonomy and see them as black-boxes which need to be trusted to do their work. OOP proponents understand the value of simple interfaces when it comes to modularity and they understand that such simple interfaces are only possible if the module has sufficient autonomy and this requires it to have full control over its state.
An analogy for FP in the real world would be like a computer manufacturer relying on chips from a chip manufacturer, but the computer manufacturer insists that the chip manufacturer must produce chips using only the materials provided by the computer manufacturer... Let's say that, eventually, a new superior semiconductor material is discovered; the chip manufacturer will not be able to use it to improve their chips. Although it has autonomy over its manufacturing processes, it is constrained to the materials provided to it by the manufacturing company; therefore it cannot take advantage of the innovation behind the scenes without the computer manufacturer also updating its processes and materials. The chip manufacturer is predictable but it is not modular; it cannot improve itself without involvement from the computer manufacturer.
I know that the chapter and book is coming at the topic pro-functional, and there's nothing wrong with that. But boy does that sentence ever ring true as a web application developer.
Functional brings with it a set of core principles and values that provide a lot of benefit. So does OOP. So does procedural. I'm a fan of always picking the right tool for the job at hand.
And JavaScript certainly brings with it a set of Functional-style features. Use them liberally, by all means. Let them solve problems for you.
As a professional, learn as much as you can about Functional programming. Also learn as much as you can about OOP, imperative approaches, procedural and any topic that will be relevant to your field. Be the best professional that you can possibly be.
But don't, for the love of God, try and write a modern frontend web application written in JavaScript (or TypeScript) from a "purely" Functional-first point of view. Not only will you fail and hate your life, but you will throw out lots of babies out with tons of bathwater.
JavaScript is not even close to a Functional language. It doesn't matter if Brenden Eich wanted to bring Scheme to the browser, that's not what he ended up creating. Not only is it far from being a functional language, but I would argue that it is downright antithetical to FP.
- Everything is mutable, even functions themselves.
- Not only does JS have a mutable global namespace but modern frontend apps often have to deal with no fewer than FOUR shared global states (the global namespace, state management a la redux store, local storage and server-side persistence). If cookies still count then that might count as five.
- Functional programming favours primitive types. Simplicity is the name of the game. Most client/server applications have to deal with rich domain models. Functional is great for "processes", while OOP is great for "things" (i.e: rich domain modelling and grouping data structures with the logic that mutates them).
- The asynchronous event loop brings side effects as a core feature of the language. Yes we have strategies for putting our side effects in a corner, but the entire application is built from the ground up around the concept of side effects. Instead of taking simple input and producing simple outputs, we send things off to the server and/or update a global state store so that multiple things can update independently. Side effects are integral to a complex web application, no matter how many pure functions you write (and you should write pure functions whenever you can, they are simple, easy to test and debug).
None of the above should matter except that I have come across many FP "purists" who come at the topic from a borderline religious angle.
I think this has to do with how trendy our industry is. OOP was very hot in the 90s and 2000s. FP start to gain traction, at least in web development, with the advent of Scala and Douglas Crockford's JavaScript The Good Parts. There is a tendency in our industry to think in binary terms. That we saw lots of problems with OOP applications and FP is promising a solution therefore OOP = evil and FP = a gift sent down form the heavens to save our souls. Nothing could be further from the truth, and trendiness is the root of all evil IMO.