Just a quick heads-up, if you want to truly write functional code in the browser there is js_of_ocaml available in the OCaml realm. With Bonsai [0] a somewhat usable framework for webapps is available now, too. There are drawbacks like filesize, but if you don't have to serve the webapp on the first pageload it shouldn't be a problem.
I am not a zealot as the book might describe. My personal goals are portability and predictability of code. Use of functions with defined return types in TypeScript, as opposed to other containers by reference, allow me to achieve those goals.
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.
Before you come at me with pitchforks: I built commercial software with Erlang. I use functional(-style) programming in all languages that support it.
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.
> One of the people I showed this to had an interesting reaction. Their response was something like: “Hey! I like functional programming because I’m lazy and incompetent. It’s about all the things I don’t have to think about.”
I can perfectly imagine the most intelligent and smart people I've ever meet saying this same thing.
There is a Danish scientist called Morten Münster who's done research on behavioural science. He's written a book for management that's gotten very popular in my country, and this is how I became acquainted with it back when I did a stint in management (which around here includes getting a MBA type education in management).
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.
Wow, I was really expecting this to be an argument against FP, until it wasn't. I love the concept of designing for Thursday 5pm though. My approach to achieve that is simply different (it doesn't include FP).
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.
It's so funny, because I thought your comment would lead to: when it's 17:00 on a bad day, I'd rather debug some Go code that is perhaps mundane but easy to follow than a chunk of Haskell code of a colleague that drank too much category theory kool-aid.
Which goes to show that what one wants to debug at 17:00 on a bad day is very personal?
> Which goes to show that what one wants to debug at 17:00 on a bad day is very personal?
It really depends, it's possible to write mundane, simple functional code (though I think more common in OCaml and Erlang than Haskell) but much of the community is sort of very excited about all this higher-order stuff that might be great but is not quite as useful and obvious as the core primitives of algebraic data types and pattern matching. I imagine a lot of people probably felt similarly about the Design Patterns craze with OOP: it's not that OOP isn't useful, just that inheritance is maybe not what you want most of the time and not everything needs to involve design patterns.
I'd rather be debugging an OCaml program than a Go program for sure.
Right, but I think (a combination of) certain abstractions invite abstractionitis. OCaml and Erlang avoid that to some extend by being more restrained about what they add to the type system. On the other hand, these languages allow side-effects, removing the need to rely on monads, monad transformers, monad transformer stacks, etc.
I agree that algebraic data types and pattern matching lead to better code. But even though they were introduced (?) by ML, there is nothing holding imperative languages from adopting them (see e.g. Rust).
I mostly write Typescript these days, and being lazy, I'll just quote wikipedia, but I'd much rather debug:
const result = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
.filter(n => n % 2 === 0)
.map(a => a * 10)
.reduce((a, b) => a + b);
than:
const numList = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let result = 0;
for (let i = 0; i < numList.length; i++) {
if (numList[i] % 2 === 0) {
result += numList[i] * 10;
}
}
Maybe it doesn't make so much sense in this simple example, probably even less so if you're not familiar with Javascript, but it mostly comes down to the state of what you're working on. In FP you know what you get, how it looks while you're working with it and exactly what to expect as the outcome, in OOP, well, you sort of don't. Reading Wikipedia, though, maybe what I like is called Functional Programming with Higher-order functions and not just Functional Programming?
Like I said. I'm not extremely religious about it, and I do think a lot of OOP design principles and code practices are slowly heading toward a more FP way of thinking. In that way I think it's sort of interesting that you mention Go, because with Go you seem to mostly work with immutable states and functions, rather than mutable objects, which is more functional than imperative programming, but maybe I just haven't worked enough with Go to know better. If you ask me, everything should frankly be immutable by default, but retrain the ability to become mutable like they do it in Rust with the "mut" keyword. I really, really, enjoyed working with that for the brief period I did. Anyway, I'm not sure I'm ever going to get into religious FP, I may very rarely use classes, but it's not like an abstract class can't be healthy for your 17:00 afternoon self once in a while.
But basically every best practice in regards to OOP that I was taught at university back 20+ years ago, the stuff they still teach today (I'm an external examiner a few times a year), has proven to be sort of useless in the real world for me. Maybe it works in more professional or competent organisations but it sure hasn't worked well in any place that I've ever worked, and yes, it does feel sort of dirty to examine people in theories I disagree with, but it's good money and a good way to keep up with both the CS world and possible hires.
Can confirm. I took Advanced Functional Programming for my master's. Of the ~15 people who took the class, the majority scored <=65% or gave up, a few people got pretty good grades because they were reasonably intelligent and willing to put in the work (me), and then there were a few people who scored >=95% because the high levels of abstraction genuinely made things easier for them.
That was when I learned that for some people, a monad really just is a monoid in the category of endofunctors.
I don't think this is true. How fat you are can be pretty objectively measured and it won't change in the short term.
Being lazy and incompetent however is always fluctuating. There are days I'm lazy, there are days when I can work 12 hours and feel energized. There is also days where everything in my mind aligns and all the problems melt away. There is also days where I ram my head into the wall on some relatively simple problem.
The point being is that programming is something that no human can do without errors. So you want a language that provides as much low overhead guard rails as possible to stop yourself from shooting yourself in the foot. Even on the days you're lazy and incompetent.
I think it’s often Socratic laziness. Not fake, but “humans are dumb and weak, let’s make this easy for ourselves.”
For most of my coding work I take it a step further than this with the simple premise that the code you wrote professionally is not for you. It’s for whoever next has to work on it, no matter their level of expertise. Sure, maybe you ‘just know’ that the equality operator comes between bitwise-OR and logical-OR in precedence, but the code isn’t for you so maybe just use the brackets anyway.
"The same people would get offended if you actually suggested they were lazy and incompetent. "
Maybe, because it is all relative.
I know my flaws and compared to my (often unrealistically) high standards, I feel incompetent quite a lot.
So I can say the above and mean it.
But if I get criticized as incompetent by someone way below my level, then yes, I might take offense and depending on the mood, also hit back (or ignore it).
(Off topic comment) With the font you're using to write code it's sometimes difficult to read some of the characters. For instance, I had some problems to read the `getKey` method, since the `K` char is pretty blurred.
It's supposed to look like an old typewriter with partly broken types, but it looks like they overdid it on the "K"... plus no typewriter I know of can write with light color on a dark background, so that kind of breaks the illusion.
GOOD: having large chunks of code that runs (mostly) without side-effects. As projects get bigger global state can trip you up, so try to keep it in check. That's where the real value of functional programming is.
BAD: copying data needlessly and turning trivial stuff into a functional pipeline that is impossible to debug. You want your code to read top to bottom like the problem you're trying to solve, if at all possible. If your call stack looks like map, fold, reduce you've introduced a ton of complexity, and why exactly?
Every programmer should understand functional programming. Higher order functions can be super useful. Immutable objects are invaluable for preventing subtle bugs. But if you feel the need to transform simple linear code into a map of higher order functions because of some abstract benefits you're probably doing stuff that's too easy for you and you should get your mental challenge from solving a more difficult problem instead.
Agree, especially with having as much part of the code as pure functions.
I think where functional programming really shines is in combination with a good type system, that can enforce those restrictions. In languages like js I find it not so useful (even if using map, filter, etc.) because I have no guarantees that there is a hack somewhere in the function that I did not see.
When there are a lot of pure functions the cognitive load is reduced so much I can use the "extra capacity" to focus on the task.
I did not know that. Another one is const to declare simple variables. There are some features, I agree. But how do I declare that a function returns a specific type and have it enforced? IMO, only by switching to Typescript or similar.
VSCode can enforce return types with jsdoc annotations, even when using plain javascript. VSCode uses typescript internally for all type inference, so you get that for free. Type inference works flawlessly 95% of the time, even for iterators/generators/inheritance/containers.
I would rather inherit someone's stateful JavaScript or Java than a complicated codebase by a seasoned Clojure, Haskell, Scala developer.
I stand a better chance understanding the sequential code than a complicated recursive (edit: nested) monad. Depending how it was written.
I can probably understand Kafka pipeline's or a functional map, reduce pipeline.
My parser for my programming language which I'm building is a recursive Pratt parser but it's written in Java.
I want to understand functional programming better. I am interested in software transactional memory and parallelism. I am aware Erlang and Go use message passing but only Erlang is functional.
In theory functional code can be parallelised due to immutability but you need some kind of communication to indicate transference of values. Imagine a pipeline where you need to share a value to a few other threads. I'm aware Haskell has software transactional memory
Agree. The first version of the example (the one using only `map`) is way easier to understand and maintain than the rigmarole the author ended up with (writing their own functors, pipes, peakerror and what not). It's like the author didn't find the first version "complex" enough, so they had to make it more complicated for the sake of "functional programming".
No there is no such thing as a recursive monad. A monad is a type for which strictly defined operations are provided. It can’t be recursive by definition.
Also all "functional" programs have their state somewhere.. just usually much stricter isolated and manipulated - so no difference in statefulness.
> I would rather inherit someone's stateful JavaScript or Java than a complicated codebase by a seasoned Clojure, Haskell, Scala developer. I stand a better chance understanding the sequential code than a complicated recursive monad. Depending how it was written.
True, but you add here the "complicated" attribute deliberately .. usually it is the other way round. The functional approach code is simpler to read and reason ( and usually not with much recursive monoids), while the believed sequential program has so many "concurrent" paths (even if there is no real concurrency, but just in the many hidden ways how the super distributed statefulness is manipulated).. not?
It spurned a chain of events that you might find interesting.
I am interested in parallel software and multithreading, so that's the kind of software I enjoy writing.
The branches of a single threaded software can think of potential future interleavings.
Your post inspired me to think of programs as "circular" or not. That is, they return to a steady state and can function properly no matter how many times they run and whether or not the execution is interrupted at any point.
A program that is interrupted can leave the program state in an invalid state that cannot be recovered.
So I started writing a parser to parse an assignment language. I plan to track object identities membership over time and see if objects get stuck in processing - they stop moving.
program {
available = {};
assigned = {};
main {
program = programs {
current_available = program.available {
available += (current_available, 1);
assigned - current_available;
}
item = program.requests {
available (=) item {
program.answers += item;
available - item;
assigned + (item, 1);
}
}
}
}
}
program {
requests = [];
requests += (add, 1);
main {
item = answers {
new_requests = [(add, 1)];
available += (item, 1);
new_request = new_requests {
requests += new_request;
}
}
answers -= answers;
}
}
Functional programmers are just as likely to make overly complicated code the same as a developer in a conventional language.
I'd rather inherit a complicated Java program than a complicated Scala program. But I'd take a well written Scala program over a well written Java program any day of the week.
Those are often records from a database.
There is a way to change it, it’s to make a copy of it.
I like being able to trust that something is what I think it is because it cannot be something else. Meaning: if I know that something can’t change, I don’t have to check for eventual accidental change.
Because of aliasing. Some other functions/objects/threads may have a reference to the old version. If they were in the middle of a computation using that object, they don't have to add a whole bunch of checks to ensure that the data they're holding didn't suddenly change from underneath them. This happens a lot in concurrent programming, but even in single-threaded programs it makes reasoning about the current state of the system easier.
> It can't be inconsistent. You've got one copy of the data in memory, everything uses that, everything sees the same thing.
Wrong. For instance, state can be stored in locals. Two threads accessing the same structure, one reading and one writing, the reader loads some state into locals and the writer then invalidates that state in the middle of the reader's computation, and the reader proceeds to computing a result mixing old state and new state thus leading to inconsistency. This ABA problem is well known and it just can't happen if the structure is immutable. And this doesn't even go into cache coherency issues.
I frankly don't think you appreciate the number of hazards just making data structures immutable actually addresses, and given you don't seem aware of the hazards implicit to mutable data structures, I suppose that's not surprising.
> What kind of programs would you write that would benefit from this
Pretty much every single program that accesses a relational database benefits from this. There are a few of those around n in case you didn't know. Perhaps you've heard of multiversion concurrency control?
The point is that if you have a reference to such object, you know for sure it'll stay looking the same way at all times, no matter what happens in the program. E.g. take this pseudocode:
let foo = MakeMeAnObject();
let bar = MakeSomethingElse(foo);
DoSomethingForSideEffects(foo, JoinThings(bar, baz));
return foo;
With immutable objects, you can be certain that neither foo nor bar were modified by any of this code. So, when e.g. debugging a problem with that DoSomethingForSideEffects() call, you don't have to worry about values of foo and bar having been changed somewhere, by someone, between their introduction and the point you're debugging.
Neither of them can be modified by it in the future - e.g. someone else can't change MakeSomethingElse() to also modify its inputs, thereby accidentally breaking your code that's using this function.
Another way of looking at it: a lot of problems with reasoning about code, or parallelism, can be drastically simplified by assuming the data being passed around is only copied, and not modified in-place. "Immutable objects" as a language feature is just making this assumption the default, and enforcing it at the language/compiler level.
In terms of use, it isn't that much more inconvenient over mutable objects. You can always do something like:
foo = DoSomeTransformation(foo);
It's just that DoSomeTransformation() is not modifying the object, but instead returning a new instance of the same objects, with relevant fields having different values. The obvious drawback here is, for large data structures, there will be lot of copying involved - but that's where the languages with immutable objects are usually "cheating", e.g. by using sophisticated data structures masquerading as simple ones, as to only ever copy things that have actually changed (i.e. "distinct" objects end up sharing a lot of their data under the hood).
> 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
I can't think of anything where I'd want that. Don't you end up needing infinite amounts of memory? Isn't it absolutely slow as balls copying all that stuff around?
Not really. You can copy the new parts and reference the old, garbage collect dead things, etc. At least in languages where immutability is a core tenet vs stuck on as an after thought.
Is it as fast mutable everything? No, some copy must happen, but is a mutable-standard languages as fast as raw hand tuned asm? Also (probably) no, but the trade offs are worth it. It likely matters a lot less than you think unless you're writing actually performance critical tight loop code vs just thinking about performance.
If immutability is a part of the language, then the compiler and the runtime know about it.
This way:
- passing an object around can always be by reference, since no one can change it
- depending on structures used, for changed bits you don't need to copy the entire structure, but simply shift pointers to old data or new data
- garbage collection can become trivial, and granular, since you know exactly what's new and old, and shifting data around becomes just moving pointers around (again, depends on implementation and optimisations)
There are downsides if you are not careful, of course:
- creating a bunch of new data that references old data will run out of memory, but this doesn't happen as often as you would think.
- sending data between processes/threads may result in copying that data (depends on implementation)
However, the upside is quite good: your data never changes under you. That is a call to func(data) doesn't sneakily change data. And all data becomes trivially thread-safe without mutexes or race conditions.
Why would I want to have a thing in memory, copy it to more memory, modify the copy, then free up the original memory every time? That just seems like a waste.
The reason I like it is that I have spent a lot of time debugging bar code like:
Var object=object()
If (isValid(object))
// Do something
In which the isValid function modified the object in unexpected way and caused the issue. With mutability, I loose confidence in the code and literally have to read implementation of everything the object touches to be sure i understand what's going on. Much more relevant in bigger projects.
Suppose you want to draw a six sided star. You could do it by drawing a triangle and then invert that triangle and draw it again. But suppose you want to cache the two triangles so that the next time you draw it will happen more quickly. You have some "Triangle invert(Triangle t)" function. It takes a triangle and returns the inverted version of that triangle. If that function modifies the triangle in place then you will first have to make a copy of it to make sure that you have the original triangle. If the code is written in a functional language you can assume that the funtion will always return a copy. If it is not functional then you may not know whether the function returns a copy or not and it may come as a "surprise" that the function modified the triangle in place.
In theory the functional approach is more stable and predictable. But whether this really causes a lot of bugs in practice is another question.
> Why would I want to have a thing in memory, copy it to more memory, modify the copy, then free up the original memory every time?
You don't do that, runtime does that for you.
The main benefit is better assumptions about code you write. The prime example is how different languages handle object construction and modification:
var date = new Date(2022, 11, 21)
var other = date.addDays(10)
The question is: what do `date` and `other` contain?
Depending on the language the answer may surprise you. Some languages modify `date`. Others don't. And you never know which of the methods exposed on an object modify it unless you read documentation. Neither the compiler nor the type system can help you.
However, if objects are immutable, you are guaranteed that the original object, `date` is never modified. And if you need to use it again, you know it contains the same data without it suddenly changing.
This gives rise to another important property: you can send this to another thread without mutexes or without creating weird synchronization points like Java's AtomicInteger. Since the object cannot be mutated, threads don't have to fight for exclusive access to read it.
It makes code easy to understand as you can easily identify what is input and what is output and can easily figure out the flow of data i.e what is being computed based on what. Without immutable objects a function taking object can mutate those objects and now they are acting as input and output, which leads to complexity.
"immutable" is kind of overloaded term, but what it means in this context is an object itself doesn't mutate when you want to change a value (basically value semantics[0] instead of reference semantics[1])
for example (pseudocode)
var newUser = Person(name: "some guy", age: 0)
// newUser is replaced with a new object that is
// initialized with field.name and previous age (0)
// the old object is discarded
newUser.name = field.name
// object is passed as copy-on-write, assuming a 1 sec delay
// it will print the objects values at this point in time
// (age: 0) even if altered later
async(after:1) { print(newUser.age) } // prints 0
// age was changed to 32 a nanosecond later,
// but now a new object again is initialized
// instead of mutated
newUser.age = 32
print(newUser.age) // prints 32
----------
output:
32
0
The idea is that a given object never changes, but it's straightforward and efficient to create a new object from the original one with some of the values changed.
This is "safer" for any code still operating on the original object, as it will not be changed unexpectedly.
It's about trusting a function call. Imagine the following C functions:
extern result fubar(struct foo *);
extern result snafu(struct foo const *);
Just by looking at the function prototype, I know that calling snafu() won't change the struct foo I pass it. I don't know what fubar() will do to it. Maybe it won't change, maybe it will. I'd have to check the implementation of fubar() to find out.
I've just left a startup company who's entire backend was written in a functional style using Typescript by an agency. The only reason I can fathom why is 'why not, functional programming is cool right!'. A new dev team was created to take over and it was a disaster. It was an absolute mess of piped, mapped, reduced functions everywhere and completely unreadable and unmaintainable. I remember getting lost in a hierarchy of about 30+ piped (non db framework) functions to write a JS object to a database. I didn't stay long.
Since I quit, the entire new engineering team quit and it looks like the company is going under. Functional programming is a big mistake for some real-world code.
You can make code in any paradigm suck. You can do horrible unmaintainable things in any language in any paradigm: Java with twenty layers of abstractions, Python with immense amounts of spaghetti, C with its hard to control safety. You can also do awful, abysmal imperative or OOP code in Typescript. So I just don't really see how you can single out FP here at all. Your codebase sucked, and whoever was hired to write it in a FP style just sucked at doing so. Sorry.
> So I just don't really see how you can single out FP here at all.
Not OP but imho, it’s because FP is "sold" as the perfect solution for readability and code maintainability. Just use FP and nothing can go wrong. That’s at least the impression I get when I read about FP.
The fact that one can write abysmal OOP code is nothing new.
> it’s because FP is "sold" as the perfect solution for readability and code maintainability. Just use FP and nothing can go wrong. That’s at least the impression I get when I read about FP.
That’s because it is. FP is not immune to incorrect implementation. Both statements are true.
I generally agree with you. That said I do think the GP has a point w.r.t. Java and OOP. It's somewhat analogous in the sense that Java's main selling point was its ability to overdo OO abstractions.
What I don't agree with the GP is specifically the claims on other languages:
""" Python with immense amounts of spaghetti, C with its hard to control safety, abysmal imperative or OOP code in Typescript ""
These are not sold as advantages for the respective languages. Python doesn't say it's easier for the programmer to write spaghetti. C's unsafe constructs are a known side effect of being closer to the metal (and optimizing for speed at expense of safety). Typescript's selling point is type checking, which is orthogonal to abysmal code.
But FP is just sold as making it easier to make abstractions all the way down. And for many of us FP-skeptics, that's not a selling point, that's a turn off. (And to reiterate my original point - this is the same for Java, OOP and OOP-skeptics)
> 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'.
Inexperienced developers that don't understand what they're doing, armed with a language known best for being worst-in-class at everything except for "running everywhere", is a recipe for disaster, no matter how you spin it.
It feels egregious to implicate "the entire surface area of functional programming" here, when there are other obvious issues at play.
> I've just left a startup company who's entire backend was written ... by an agency. ... A new dev team was created to take over and it was a disaster.
I honestly think these are the important bits of this story. The startup outsourced their backend to an agency, then tried to replace the agency with a brand new internal dev team.
There's no way that story ends well, regardless of the paradigm the agency chose or how skilled they might have been (probably not very). Every codebase is unmaintainable when the team that built it is suddenly replaced.
Peter Naur had a lot to say about this in Programming as Theory Building [0]:
> The extended life of a program according to these notions depends on the taking over by new generations of programmers of the theory of the program. For a new programmer to come to possess an existing theory of a program it is insufficient that he or she has the opportunity to become familiar with the program text and other documentation. What is required is that the new programmer has the opportunity to work in close contact with the programmers who already possess the theory, so as to be able to become familiar with the place of the program in the wider context of the relevant real world situations and so as to acquire the knowledge of how the program works and how unusual program reactions and program modifications are handled within the program theory.
> Functional programming is a big mistake for some real-world code.
The emphasis on "some" should be stronger in your comment, otherwise it reads, on a quick pass, as a broad dismissal of functional programming.
Functional programming concepts and ideas have been steadily incorporated in most mainstream languages in the last 10+ years. However, when people move past the language's functional programming primitives, it's when the project enters potentially dangerous territory. Your, and the article's, example of pipes, for one.
Personally, I'd like more languages to incorporate ADTs (algebraic datatypes) for me to be able to "de"layer programs back to a mostly procedural + functional programming. And based on the current adoption rate of FP concepts, I'm not sure we're that far away from having proper ADTs and pattern matching in the most popular imperative programming languages of today.
Some solutions are much easier to express in an imperative way.
You can still write pure functions and avoid state in imperative code. Use consts, immutable objects, etc. You get some of the benefits of functional programming while still writing readable code.
> copying data needlessly and turning trivial stuff into a functional pipeline that is impossible to debug. You want your code to read top to bottom like the problem you're trying to solve, if at all possible. If your call stack looks like map, fold, reduce you've introduced a ton of complexity, and why exactly?
Hard to follow code is mostly a consequence of point-free style in Haskell which is an heresy to be avoided at all cost.
ML typically uses a piping operator to compose code instead and that leads to top to bottom code which is extremely easy to read.
There is zero difference then between a map and a loop. Loops are not read top down anyway nor are function calls. It seems to me you are just more used to these indirections than other ones and are therefor blind to them. This leads to an argument which I consider a straw man personally.
I'm reminded of the Kernighan quote / trope: "Everyone knows that debugging is twice as hard as writing a program in the first place. So, if you're as clever as you can be when you write it, how will you ever debug it?"
This phenomenon seems to happen more frequently with FP or related tools and languages (RxJs abstraction soup comes to mind).
100% agree, the monstrosity that author proposes is really hard to swallow. Simple made hard.
The idea to handle validation (we get HTML instead of expected JSON) by passing some Nothing object down the flow is horrible, if we expect JSON and get something unparsable, well, the best strategy is to fail fast and let know client side that we have a wrong data. Instead we show off with some fancy code structure with zero value for the user of the software.
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.
> pulling those out may lose access to required lexically-scoped variables
The only situation for this that I can think of is inside a list comprehension / generator expression. You are aware that you can define functions at any scope in python?
pruned = map(
# Remove all elems whose foo is less than the number of elems
lambda elems: list(filter(
lambda elem: elem.foo < len(elems),
elems
))
)
Now let's say we want a running total of all the foos. We can insert an expression to do this:
total_foos = [0]
pruned = map(
# Remove all elems whose foo is less than the number of elems
lambda elems: list(filter(
lambda elem: (
# Add elem.foo to our total_foos accumulator
total_foos.append(total_foos.pop() + elem.foo),
elem.foo < len(elems)
)[-1],
elems
))
)
total_foos = total_foos.pop()
This is rather awkward and "non-Pythonic"; ideally we would use 'total_foos += elem.foo', but that can't exist in a lambda. Hence:
total_foos = 0
# Have to define this outside the map, since it's a statement
def checkAndAccumulate(elems):
"""This checkAndAccumulate function is just a wrapper, for closing-over
the elems variable (since it's not in-scope outside the map call).
Returns the actual checking+accumulating function."""
def checker(elem):
"""The actual function we want to filter with"""
total_foos += elem.foo
return elem.foo < len(elems)
return checker
pruned = map(
# Remove all elems whose foo is less than the number of elems
lambda elems: list(filter(
checkAndAccumulate(elems),
elems
))
)
> 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.
This code is only complicated because you insist on following some abstract ideal.
The actual way to solve this in python is:
total_foos = sum(elem.foo for elem in elems)
pruned = [e for e in elems if e.foo < len(elems)]
Which is shorten than even your first code sample. If you directly translate your last example into sensible python, you get a nice example of some "simple linear code":
total_foos = 0
pruned = []
for elem in elems:
total_foos += elem.foo
if elem.foo < len(elems):
pruned.append(elem)
The existence of statements in python clearly stands in the way of achieving ideals of pure functional programming. But I think aiming for such ideals is the exact opposite of the point OP was making.
It might be easier to explain things to oneself in terms of time though - we are not necessarily functional in thinking. e.g. I imagine trying to explain f(g(x)) versus g(f(x)) without using the word "first" or "then"
That's data flow, which is fine: the dependency is explicit, checked by the compiler, and maintained regardless of where the code lives.
For example, if 'foo = g(x)' is defined in one module, and another module does 'f(foo)', then the data flow is preserved. If we try to force things to be the wrong way round, we'll get compiler errors like 'No such variable foo'.
Compare that to temporal ordering of statements; if one module executes 'g(x)' and another executes 'f()', how do we ensure the latter occurs after the former? How could the compiler tell us if they're the wrong way around? Very difficult.
> e.g. I imagine trying to explain f(g(x)) versus g(f(x)) without using the word "first" or "then"
Use "from" and "to", which talks about data dependencies rather than temporal dependencies. Most people assume dependency order implies temporal order, and then you introduce them to lazy evaluation to decouple even that.
Worth noting Haskell ended up reinventing the statement/expression distincion in the do-notation. So apparently the distinction do have value.
“Logical time” actually matters when you have side-effects. Of course we can agree code without side effects is simpler to reason about. Unfortunately you need side effects to do anything useful.
> “Logical time” actually matters when you have side-effects
"Time" doesn't matter; causality/dependency is what matters. That is modelled quite well by passing around values. For example:
let hostname = lookup("hostname")
username = lookup("username")
password = lookup("password")
connection = connect(hostname, username, password)
in query(connection, "SELECT foo FROM myTable")
The 'query' call depends on the result of the 'connect' call, so it must "happen afterwards". The 'connect' call depends on the result of the three 'lookup' calls, so it must "happen afterwards". The 'lookup' calls don't depend on each other, so they're concurrent (note that concurrent does not necessarily imply parallel).
This form of data dependency is no different than, say, arithmetic: to calculate '3 × (2 + 4)', the multiplication depends on the result of the addition, so it must "happen afterwards". (The imperative equivalent would be 'push 2; push 4; +; push 3; ×;')
> Worth noting Haskell ended up reinventing the statement/expression distincion in the do-notation
Yes, do-notation "desugars" into plugging return values into arguments, like above. This gets a little tedious for things like 'IO', where we pass around a unit/null value to represent "the real world".
Still, a nice lesson from Haskell is that there's value in making things opt-in; e.g. many languages have Maybe/Option, but it's less useful in languages which allow 'null' anywhere; many languages have IO/Effect/Task/etc. but it's less useful in languages which allow I/O anywhere; etc.
The real world type is a bit of an implementation detail in GHC. As a user, you shouldn't really have to deal with it. IO could be implemented differently under the hood, though real world works well in GHC.
But then the real world rudely intervenes because connections are flaky, services are down, and you need to add logging to weird and unexpected parts of your program because a call on device A is resolved in a weird state on device B, and the backend reports that everything's a-okay.
Still, you're showing the simple case of code without (global) side-effects.
What if instead we said
let connection = connect(hostname, username, password)
_ = query(connection, "INSERT INTO t1 VALUES ('abc')")
data = query(connection, "SELECT * FROM t1")
in print data
What is the order of the two calls to `query` actually matters, but how could the language know?
The order of execution of code is actually important every time you have such a side effect, and languages that don't strictly define it make this type of thing very error prone.
> What is the order of the two calls to `query` actually matters, but how could the language know?
The language can't "know the order" of these calls, since they are not ordered. No information is passed from one call to the other, hence neither is in each other's past light cone.
If you want to impose some order, you can introduce a data-dependency between the calls; e.g. returning some sort of value from the "INSERT" call, and incorporating that into the "SELECT" call. Examples include:
- Some sort of 'response' from the database, e.g. indicating success/failure
- GHC's implementation of IO as passing around a unit value for the "RealWorld"
- Lamport clocks
- A hash of the previous state (git, block chains, etc.)
- The 'connection' value itself (most useful in conjunction with linear types, or equivalent, to prevent "stale" connections being re-used)
- Continuation-passing style (passing around the continuation/stacktrace)
> languages that don't strictly define it make this type of thing very error prone
On the contrary, attempting to define a total order on such spatially-separated events is very error prone. Attempting to impose such Newtonian assumptions on real-world systems, from CPU cores to geographically distributed systems, leads to all sorts of inconsistencies and problems.
This is another example of opt-ins being better than defaults. It's more useful and clear to have no implicit order of calculations imposed by default, so that everything is automatically concurrent/distributed. If we want to impose some ordering, we can do so using the above mechanisms.
Attempting to go the other way (trying to run serial programs with concurrent semantics) is awkward and error-prone. See: multithreading.
Note that you haven't specified the database semantics either.
Perhaps the connection points to a 'snapshot' of the contents, like in Datomic; in which case doing an "INSERT" will not affect a "SELECT". In this case, a "SELECT" will only see the results of an "INSERT" if we query against an updated connection (i.e. if we introduce a data dependency!).
Perhaps performing multiple queries against the same connection causes the database history to branch into "multiple worlds": each correct on its own, but mutually-contradictory. That's how distributed systems tend to work; with various concensus algorithms to try and merge these different histories into some eventually-consistent whole.
PS: There is a well-defined answer in this example; since the "INSERT" query is dead code, it should never be evaluated ;)
PPS: Even in the "normal" case of executing these queries like statements, from top-to-bottom, against a "normal" SQL database, the semantics are under-defined. For example, if 'query' is asynchronous, the second query may race against the first (e.g. taking a faster path to a remote database and getting executed first). This can be prevented by making 'query' synchronous; however, that's just another way of saying we need a response from the database (i.e. a data dependency!)
In most programming languages, the order of the two statements would be well defined, and neither would be dead code.
Trying to make all statements implicitly concurrent unless they have explicit dependencies is a terrible way to complicate your life. That in some cases you can optimize the code (or the CPU will do it for you) by executing it in certain other orders where it is safe is supposed to remain an invisible optimization.
It is obvious to everyone that distributed code, eventual consistency, and other similar non-totally-ordered examples are much harder to get right than procedural code. Even simple print-based debugging/logging becomes excessively complex if you get rid of the local total ordering.
Even most network-based computing is done over TCP (or, more recently, QUIC) exactly because of how enormously useful having a total order is in practice (even if it's just an illusion/abstraction).
> PS: There is a well-defined answer in this example; since the "INSERT" query is dead code, it should never be evaluated ;)
It's only dead code if you assume Haskell's bizarre lazy execution model. In virtually every other language, unless the compiler can prove that query() has no side effects, the INSERT will be executed.
> In most programming languages, the order of the two statements would be well defined, and neither would be dead code.
There are no statements in the above examples, just expressions (composed of other expressions). Evaluation order of expressions is not well defined in "most popular languages", e.g. consider this expression:
printSecondArgument(
query(connection, "INSERT INTO t1 VALUES ('abc')"),
query(connection, "SELECT * FROM t1")
)
> Trying to make all statements implicitly concurrent unless they have explicit dependencies is a terrible way to complicate your life
I agree, that's one reason why I dislike statements (see my list in a parent comment)
> It is obvious to everyone that distributed code, eventual consistency, and other similar non-totally-ordered examples are much harder to get right than procedural code.
This is a category error. All code is distributed; the world is partially-ordered. "Procedural code" (i.e. serial/sequential execution) is a strategy for dealing with that. It's particularly easy (give each step a single dependency; its "predecessor"), but also maximally inefficient. That's often acceptable when running on a single machine, and sometimes acceptable for globally-distributed systems too (e.g. that's what a blockchain is).
Forcing it by default leads to all sorts of complication (e.g. multithreading, "thread-safety", etc.). Making it opt-in gives us the option of concurrency, even if we write almost everything in some "Serial monad" (more likey, a continuation-passing transformer)
In most popular languages, the order of evaluation of both statements and expressions is specified. For your example, the insert query call is guaranteed to happen before the select query call in Java, C#, JS, Go, Python, Ruby, Rust, Common Lisp, SML. It is indeed unspecified in C, C++, Haskell, Scheme, OCaml.
While C and C++ are extremely commonly used, I would still say that the majority of popular languages fully define evaluation order. Even more so since most of these languages considered they are fixing a flaw in C. Rust is particularly interesting here, as they initially did not specify the order, but then reversed that decision afelter more real-world experience.
> "Procedural code" (i.e. serial/sequential execution) is a strategy for dealing with that. It's particularly easy (give each step a single dependency; its "predecessor"), but also maximally inefficient.
> Forcing it by default leads to all sorts of complication (e.g. multithreading, "thread-safety", etc.). Making it opt-in gives us the option of concurrency, even if we write almost everything in some "Serial monad" (more likey, a continuation-passing transformer)
You yourself admit that serial code is a strategy for dealing with the complexity of the world - it doesn't complicate anything, it greatly simplifies things.
Threads and other similar constructs are normally opt-in and used either to model concurrency that is relevant to your business domain, or to try to achieve parallelism as an optimization. They are almost universally seen as a kind of necessary evil - and yet you seem to advocate for introducing the sort of problems threads bring into every sequential program.
Thinking of your program as a graph of data dependencies is an extremely difficult way to program, especially in the presence of any kind of side effects. I don't think I've ever seen anyone argue that it's actually something to strive for.
Even the most complete and influential formal model of concurrent programming, sir Tony Hoare's CSP, is aimed at making the order of operations as easy to follow as possible, with explicit ordering dependencies kept to a minimum (only when sending/receiving messages).
> That's often acceptable when running on a single machine, and sometimes acceptable for globally-distributed systems too (e.g. that's what a blockchain is).
It's not just blockchain: TCP and QUIC, ACID compliance and transactions, at-least-once message delivery in pub-subs: all of these are designed to provide an in-order abstraction on top of the underlying concurrency of the real world. And they are extremely popular because of just how much easier it is to be able to rely on this abstraction.
> You yourself admit that serial code is a strategy for dealing with the complexity of the world - it doesn't complicate anything, it greatly simplifies things.
Serial code greatly simplifies serial problems. If you want serial semantics, go for it. Parent comments mentioned Haskell, which lets us opt-in to serial semantics via things like `ST`. Also, whilst we could write a whole program this way, we're not forced to; we can make different decisions on a per-expression level. Having rich types also makes this more pleasant, since it prevents us using sequential code in a non-sequential way.
However, there is problem with serial code: it complicates concurrency. Again: if you don't want concurrency then serial semantics are fine, and you can ignore the rest of what I'm saying.
> Threads and other similar constructs are normally opt-in and used either to model concurrency that is relevant to your business domain, or to try to achieve parallelism as an optimization. They are almost universally seen as a kind of necessary evil
Such models are certainly evil, but not at all necessary. They're an artefact of trying to build concurrent semantics on top of serial semantics, rather than the other way around.
> and yet you seem to advocate for introducing the sort of problems threads bring into every sequential program.
Not at all. If you want sequential semantics, then write sequential programs. I'm advocating that concurrent programs not be written in terms of sequential semantics.
Going back to the Haskell example, if we have a serial program (e.g. using `ST`) and we want to split it into a few concurrent pieces, we can remove some of the data dependencies (either explicit, or implicit via `do`, etc.) to get independent tasks that can be run concurrently (we can also type-check that we've done it safely, if we like). That's easier than trying to run the serial code concurrently (say, by supplying an algebraic effect handler which doesn't obey some type class laws) then crossing our fingers. The latter is essentially what multithreading, and other unsafe uses of shared-mutable-state are doing.
Avoiding side-effects is usually lumped toghether with async-style interfaces into the term "functional programming". However, the two are separate properties with separate benefits and downsides:
- No side-effects makes reasoning about logic easier, code more deterministic, and tests more reliable. However, certain problems become harder to solve (e.g. simple caching or memoization).
- Async-style interfaces allow for waiting on slow operations in a non-blocking fashion, meaning those don't occupy threads, thus a more economic use of resources. However, async-style code can be harder to read, write, and debug, though this varies heavily between languages and frameworks.
Part of the problem with "functional pipelines" as in the article is that you are in essence creating a domain-specific language to describe the problem your program is modeling and solving.
Problem is that is not a programming language supported by anybody else than you. It is not part of the standard library. If it was it would probably be of high quality, highly tested-in-practice code.
But if you constructed your pipeline-framework (as in the article) by yourself you now need to understand not only the solution in terms of your created framework, but also how the framework itself exactly works. If this is a one-off problem-solution what looks like simpler code in the end hides its complexity inside its framework-code. And as you keep programming your framework is a moving target. You will improve it from time to time. Understanding it becomes a moving target too. It is a real problem for maintenance for you or anybody else who wants to use and adapt your existing code in the future.
Think about having the problem and a solution and being able to describe both in English. But then you say hey if I invent this new language Klingon and express the problem and solution in Klingon, then both the problem and solution become simpler. Cool. But now you must also program the Klingon interpreter for yourself. And be aware of which version of Klingon your code is assuming is used.
This is the tendency and cost of over-engineering. It is fun and natural to look for the "ideal solution", but it comes with a cost.
Yeah. I'm one of those who are not convinced yet about functional programming being so great. In most cases what people call functional programming are inefficient shortcuts to lengthier functions, so I never got all that hype about it :\",
The nice thing about functional programming is that it allows you to shift some of the "mundane" domain thinking onto the type system. A classical example of that might be to mix units in physics code. So, nonsenses like adding up lengths and time is no longer possible.
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
The benefits of type systems are either entirely orthogonal or at best are themselves a prerequisite for the benefits of FP. Your example of measurement units in particular is applicable to the most imperative goto laden code we could think of.
In fact, some of the more popular functional languages are untyped - those being Clojure, Erlang and Scheme.
Exactly my point, I'm sure there are benefits, but this example is more suited to Typed languages whereas many functional languages like you say are untyped. And man, I've worked with MIT Scheme in a major commercial software for 5+ years. What a nightmare it was. :(. I mostly understood the functional aspect of the language, but the sea of misplaced brackets and a complete lack of any debugger (at least for our version of it) whatsoever made it one of the worst languages to work with. The biggest irony was, since Scheme is apparently so extensible, they had created a home-grown "Object Oriented Scheme", which implemented only some aspects of OOP and omitted some other very important ones, and it was upto the developer to find out the hard way which ones were omitted! :D
Conceptually, the `map` function is just syntactic sugar for `for` loops, and it's also meant to simplify actions. There are functional equivalents to many small patterns of code that occur often - for instance
lst = []
for element in range(10):
lst.append(element * 2)
Is a very common pattern, that can be expressed with less typing (and less mental overhead, when you become used to it) by
lst = list(map(lambda x: x * 2, range(10)))
Similarly, another very common mini-pattern is
total = 0
for el in range(10):
total += 3 * el
Which can be more swiftly expressed by
total = reduce(lambda acc, el: acc + 3 * el, range(10))
These examples are trivial, but once you start using these higher level syntactic sugar-like constructs, you often find that code tends to fit together more nicely and in ways that make more sense, even if only because you used a filter operation before the map instead of writing another level of nested `if` statements inside a loop body. Code gets easier to follow, not unlike how it's easier to reason about the behavior of a `for` loop than it is to keep a bunch of `goto`s in your mental model of what the code does while at the same time thinking about whether the business logic part of it makes sense.
Tbh, I get your imperative examples instantly but my mind struggles to autograsp these multilevel functional parts like list(map(lambda. Too much noise, I have to think through it to get it.
I’d prefer a loop with ifs and continues/breaks.
It may be a lack of experience or habit, but then I can write code like that (and worse), and it doesn’t make any sense to me. Don’t get me wrong, not that I refuse to use maps or filters or folds, but not that I want to make a whole program of them either. They have their place where factoring-out a block of code feels stupid, but if I had something like:
[1, 2, for (x of xs) {
if (cond) {emit x}
}]
I’d never bother with these function-al things. It’s not FP that is rich, it’s a traditional imperative syntax that sucks. Paradigm shift is not equal to usage out of [in]convenience.
> I can write code like that (and worse), and it doesn’t make any sense to me
I'm learning FP and I see value with writing code with map, reduce etc as those are expressions instead of statements. Expressions guarantee you have a result of particular type (with possible side effects if you prefer), but statements DO side effects. The loop may or may not insert some value into resulting list because some branching happened or you forgot some else condition, with map you guarantee you get same number of objects of particular type back.
Plus that enables composition (like in C# LINQ) - by not extending some loop with different kind of responsibilities but just passing result to next function that modifies/filters the result.
> Tbh, I get your imperative examples instantly but my mind struggles to autograsp these multilevel functional parts like list(map(lambda. Too much noise, I have to think through it to get it.
That's exactly why most FP languages have pipe operator, so it's incredibly easy to read.
lst = range(10)
| map(fn x -> x * 2 end)
| map(etc...)
That doesn’t change much for my read ability. It even reads more imperatively, like: I take a range of 0 to 10, map it over x * 2, map it over… What do I get? A mapping? Maybe?
Meanwhile, a for loop is straightforward, you go from 0 to 10 and append a double or add a triple of x to an accumulator. I appended/added them, that’s what I get. It’s like my brain somehow follows {} scopes and understands their local envs with no effort.
If this syntactic style works for you without this friction, nice. But it doesn’t work for everyone and I suspect that this FP/PP is biased by this effect at both sides.
A mapped list/enumerable it was originally given. You don't think what you get when you add two numbers, don't you? Language just works certain way. Not understanding a simple building block of a language isn't a valid argument against it. All you essentially say is that you got so used to OOP concepts that anything else is hard to read. And it's ok, it's the same way for everyone... But it's not a valid argument to say that "fUnCtIoNAL bAd". The whole thing here boils down to what you already said - lack of experience.
My honest advice is - try to learn one functional language, like honestly learn and understand it, try writing something with it. It really does expand horizons.
It results into [1, 2, …(some xs satisfying cond)]. Emit does just that - emit another value into a context where for-expr was used. It emits some of x-es into that array literal.
In numbered-lines code you can’t see the start of a goto loop. In labeled-lines code it’s not that hard to infer that it is a jump target, but having a condition/iterator in one place is an obvious benefit.
That said, I believe most people who “boo GOTO” never actually used it to make sense of what could be wrong with it and how it feels really.
Anyway, I think that this analogy(?) is confusing and makes no sense.
Its greatest strength I see is its simplicity. OOP gives a lot of latitude to overcomplicate solutions, and I've found it tends to obscure more obvious solutions to certain problems, e.g. reaching for inheritance to modify behaviours instead doing it by passing functions as arguments, especially among less experienced team members.
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.
Once you zoom out to the bigger picture of dozens of devs writing the code rather than yourself, its more a matter of "statistically, if I recommend functional over OOP, how simple will the code be a year from now".
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.
Indeed. Although I have mostly stopped using OOP in most of my code, I sometimes encounter certain classes of problems that are exceedingly complicated to solve in a purely functional way, but that are a breeze to solve using OOP, believe it or not.
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.
> I sometimes encounter certain classes of problems that are exceedingly complicated to solve in a purely functional way, but that are a breeze to solve using OOP, believe it or not.
I agree. As an example having to create a class for bags of methods that don't fit naturally anywhere is bad. I also despise all the manager, factory, service, command things that emerge more often in OOP projects than in functional ones. I know that they denote well known patterns (if used in that way) but it's clutter, boilerplate, noise around the code that does the job. However functional only is not necessarily better in my experience.
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
> Ruby has modules too but for some reason customers really like to create classes for everything, even two method ones like [...]
Encapsulation and extraction of complex functionality (yes, you should not do it for trivial one-liners).
In Ruby, classes get used a lot for this because, uh, why not, but you might as well just have a module method - it's just that the way modules are used in Ruby makes this slightly awkward because Ruby is after all biased towards "everything is an object".
But in, say, Haskell I might write instead:
module Command
run :: [String] -> IO ()
run = ...
-- some other file
import qualified Command
...
Command.run args
(not that "Command.run" looks like super idiomatic code in Haskell or anything like that, probably you'd structure your code differently, but it's the general principle)
I didn't understand what was the point of the introduction with so many words like that.
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".
If you start with a program, see it as a block of logic that can be separated into parts with cuts in many different places. Choose the cuts so as to leave it a composition of functional programming abstractions. Then it is easy to reason about and it is possible to teach others to code in the same way. This is a benefit for functional programming - a promise of consistency in thought and style that is easy to debug.
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.
Interesting article with an interesting writing style, I may pick up the book even if I suck at reading books!
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.
> I had a collegue that wrote a lot of functional code and his code was extremely hard to follow and understand.
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.
The first code iteration in this article could have been deployed to Production before I finished reading the rest of the article.
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.
> real world business logics often does require side effects
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.).
I read the whole thing waiting for the aha-erlebnis which never came. I'm a full stack JS/TS engineer with a decade of experience. I expected this article to be written for someone like me. It didn't click, even though I already love and use functional aspects like immutability and pure functions. I feel like it's the whole new set of terminology that puts me off (and I'm talking about `scan` and `Task`, not even `Functor` or `Monad`). I have confidence I can learn and apply this in a few weeks, but I can't realistically expect junior/medior devs to quickly onboard into a codebase like that.
Maybe I'm biased against "true" functional programming because I've been on Clojure and Scala projects in the past (as a backend dev) and both experiences have been good for my personal/professional development, but a shitshow in terms of long-term project maintenance caused by the enormous learning curve for onboarding devs. The article talks about how great it is for confidently refactoring code (which I bet is true) but doesn't talk about getting people to understand the code in the first place (which is still a requirement before you can do any kind of refactoring).
My only hope is for ECMAScript (or maybe TypeScript) to introduce these OK/Err/Maybe/Task concepts as a language feature, in a way which befits the language rather than trying to be "complete" about it. We don't need the full spectrum of tools, just a handful.
Always makes me sad that Scala got sucked into the pure-functional priesthood type culture rather than the "better Java, by being mostly functional and immutable and then practical as hell when appropriate" pathway. I really like coding using Scala but the way I like to do it feels totally non-idiomatic.
So true. I was involved in two very different Scala projects. One was the sensible "better Java" way, which was mostly great. The other was a big enterprise project with a core group of "hardcore" FP enthusiasts which was very stressful because of imposter syndrome and troubles to onboard new folks. I have been against Scala ever since, exactly because of this FP cult.
> The other was a big enterprise project with a core group of "hardcore" Spanish enthusiasts which was very stressful because of imposter syndrome and troubles to onboard new folks. I have been against Spanish ever since, exactly because of this Spanish cult.
Scala tried to be too much. Too many paradigms. Too much flexibility and power. Many people might think they want that, but a subset are probably going to have an easier, happier life choosing a less powerful language...
> I read the whole thing waiting for the aha-erlebnis which never came. I'm a full stack JS/TS engineer with a decade of experience. I expected this article to be written for someone like me. It didn't click
Don't worry, it's not about you. The article is genuinely underwhelming.
It walks you up the abstraction tree to the building of higher-kinded types, but then just handwaves it with 'and now you can do a lot of things!' but doesn't show them.
It needs a final part where the flexibility is displayed. Something like 'if (debugMode) runpipeline(synchronousDebugWriter) else runpipeline(promise)'.
I couldn't agree more. I feel that some thought leaders debating intellectual concepts in computer programming have no idea how real world software development takes place these days.
Developers are under enormous time pressure to deliver. They face an exponential skill curve as their scope is massively broad (i.e. devops). Things need to be shipped fast, time for "proper" engineering is compromised. Team members working on the codebase are of various skill levels and ever changing. Finally, a lot of products being worked on have a limited shelf life.
For 80% of developers worldwide, the concepts discussed in the article are too steep, and therefore unusable.
I've occasionally watched colleagues give presentations on functional programming over the years, and while I can see why certain people are drawn to it the stated benefits of functional programming have never seemed that significant to me. The advantages that FP provides aren't likely to be needed by developers that are capable of learning it.
I’ve had similar experiences with scala and clojure professionally. I now actively oppose people attempting to add functional code to projects I work on.
…because when they say “more functional” most people mean:
I want less code.
I want the code to be shorter, because I’m lazy and I want it to be all on one screen.
…but that’s actively harmful to almost any code base.
You want simple code, not dense complicated code. Dense complicated code is for people who wrote the code, and a few smart talented people. Other people have to work on the code too. They cannot.
Actual functional code doesn’t strive for code density, it strives for code purity and algebraic structures.
That’s fine. Do that.
Dense map reduce reduce flow reduce functions can die in a fire.
Pretty poor attitude to just adopt so generally. I've seen 'actively harmful' qualities from all paradigms. Once peoples start adopting attitudes like yours they've just become the mirror of the condescending FP type and just kill outright any of the really cool features that are useful, as well as any discussion of them.
Here’s the real question: do you think dense code is more maintainable?
Generally yes? More than a verbose multilayered abstraction, probably?
…but where do you draw the line? Map statements instead of for loops? Collapse all the white space onto a single 200 character line? Make everything one big regex?
Dense code means that every change has more impact, because there’s less code; it’s unavoidable: less code to do the same work means more impact from changing any part of that code.
That is why it’s difficult to maintain; because you can’t touch it without making side effects; you can’t reason about a small part of the code, because the logic is dense and difficult to unpack into small parts.
Certainly OOP can often be verbose and annoying, but that’s a different thing.
Code density and being functional are orthogonal; some OOP is too dense too. …but generally I’ve found that inexperienced people see density and strive for it, believing this makes it functional; but the truth is the opposite.
Good functional programming is often naturally concise, but most people don’t actually seem to understand FP.
They just seem think it means to put more “reduce” statements in the code, remove for loops and generally make the code harder to debug and denser.
…in my, limited experience, working with lots and lot of different folk at many different organisations.
For me it's not density - it's the OOP class abstractions and all that. I'm not smart enough to keep up with it vs the FP approach of just doing data transformations.
I think of OOP done well at a high level as "structural logic". Whereas in FP, one might use `map()` and `bind()` to replace complex imperative logic flows, in OOP, this is done with object hierarchies and structures.
When you have a abstract base class or an interface that defines some contract, it's creating a "shape" that defines how an instance of the class can be used.
I think that this might be why some folks have an affinity for OOP and some have an affinity for FP. OOP affinity might be tied to more visual thinkers. For me, I see the "shapes" of the code defined by the contracts.
I think this is about the level of abstraction. As React component extraction is to tag soup, so named functions composed are to fp primitives. In code reviews, if I see a big swamp of pipe/fold/cond etc at the top level, I'd kick it back and ask that to be wrapped in a named function that explains what it does, rather than exposing it's guts.
Writing concise, clear code is a skill that straddles any paradigm.
Perhaps the snark caused the down-votes, but your point is legitimate. 'Pure FP' languages encourage code that is nearly unreadable and unparseable without any additional context (and sometimes, unreadable even with said context). There is some strange desperation for extreme terseness in pure FP languages like Haskell, Ocaml, Idris, etc.
Single-character variables and functions, point-free style... Coming from an imperative background, this just seems like flexing for the sake of flexing.
Why not make life a little easier by clearly naming variables? This isn't maths or physics where variables (for some equally-inane reason) must be one character. We have 4K screens today; I can accept lines that are significantly longer than 80 chars.
When your code is sufficiently abstract, there often really aren't better variable names than a or x. My experience is that it's about the scope for that variable. If it's in a one-line lambda, then it'll be one letter. If it is going to be used in the next 10 lines or so, make an abbreviator. And it's longer, or particular unclear, spell it all out. Adding extra words don't make BusinessAbstractFactoryIBuilder more readable.
While I understand and agree with this meme[1], I think that's the other extreme, where everything is a Factory Builder thing.
Even so, I would rather too much information than too little, which is what FP programs tend to do. Over-abstraction is also a problem, in my view. Even in a LINQ lambda, for instance, I might write
There's nothing wrong with single character variables if you're not using them like a complete idiot. A line like reports.map(r => whatever) makes it blatantly obvious that r is a report.
Fwiw, OCaml doesn't chase extreme terseness or point-free programming. It's not really equipped for that.
OCaml is designed for straightforward code centered on functions, modules, and a mix of imperative and immutable data structures; all with a _really_ simple execution model and a pretty nice type system. The core language has very few quirks and it's easy to predict what it does, and how efficiently.
There's not really any comparison with Haskell, which emphasizes heavy optimizations to make its terseness, based on lazy evaluation, work well in practice.
I think you're conflating "readable" and "uncomplicated" with "familiar". I'm equally infuriated by OO code with dependency-injected-everything from some hidden framework configured by fourteen XML files somewhere in a different file tree, interfaces for every single class even if only instantiated once, factories to create builders that make adapters.
Maybe if I stared at it for twelve years it would become familiar and I would begin to think it was simple, readable and maintainable.
Yeah. Sure, "simple" and "complex" sorta have an objective definition. But colloquially "readable", "simple", "complicated", etc have a tendency to track with "things with which I'm familiar/comfortable (or not)".
Over the decades I've come to the conclusion that there's no such thing as a one size fits all sweet spot on this stuff. Different people are going to have different experiences with what they find straightforward or not. They will have different backgrounds, mental models, ways of perceiving the world. It all adds up. As a profession we need to understand this reality and find ways around it instead of getting into dogmatic arguments as if there's One Right Answer.
Common example I give - the GP complained about FP advocates wanting code to take up less screen space. I have come across many devs who struggle with concise code, and many others who struggle when code is not concise. Similarly, I have come across plenty of devs who start having trouble when code is spread out (sometimes that means within a file, across files, both, etc). I have also come across plenty of devs who have trouble when it's all pulled together.
OK/Err/Maybe can be trivially implemented with TypeScript, if the project development team wants them. We have it in the current project I work on and it works well with GraphQL.
For OK/Err, in my experience it kind of depends on "how happy is your dev team with using exceptions for general purpose errors"? The orthodox school of thought says "exceptions only for exceptional errors", in which case things like OK/Err give you a nice way to structure your control flow and its typings.
`Maybe` is used by `graphql-code-generator` to explicitly mark optional typings in generated TypeScript types for a GraphQL schema. I don't think it's necessary (TypeScript has `?` after all) but some people prefer it.
I've used patterns like that in Scala; I see their value in building a correct system etc etc etc, but only if it's consistently used throughout the codebase.
As it stands, most JS/TS projects aren't very consistent to begin with; error handling is either not done at all (let it fail), or a mix of exceptions, failing promises, error responses / types / states, etc.
But that's not really down to the language, more the underlying culture.
I mean they could ADD it, just like nowadays individuals can choose to implement it themselves, but it wouldn't supersede any existing error / result implementations (success/error callbacks, throw/catch, promises which use both, etc).
To improve or change a language, I think you should get rid of another feature if it solves the same problem, instead of add another option.
"I read the whole thing waiting for the aha-erlebnis which never came."
This is increasingly my go-to metaphor for this: This article and many of its kind are talking about bricks. They really like bricks, because they're square, and they come in several nice colors, you can really bash a clam with them, they're cheap, they're quite uniform, they make great doorstops and hold down stacks of paper really well, and they have a personal preference for the texture of bricks over other possible building materials. These people think bricks are great, and you should incorporate them into all your projects, be it steel bridges, mud huts, a shed out back, a house, everything. Bricks should be everywhere.
Then they build you a tutorial where they show how it looks to build a mud hut, and how nice it is to put some random bricks in to it. Isn't that nice. Now your mud hut has bricks in it! It's better now.
But that's not what bricks are about. Bricks are not about textures or being good at bashing open clams. Bricks are about building walls. Walls that may not be the solution to every wall, but certainly have their place in the field of wall building because of their flexibility, easy of construction, strength, cheapness, etc. Trying to understand bricks out of the context of using them with mortar to build walls is missing the point.
Contra the endless stream of tutorials that make it look like functional programming is essentially mapping over arrays and using Result/Option instead of error returns, that is not what functional programming is about. That is a particular brick functional programming is built out of. It isn't the only brick, and if you scan a real Haskell program, isn't even necessarily one of the major ones in practice. They turn out to be a specific example of a very simple "recursion scheme". These simple "bricks" show up a lot precisely because they are so simple, but generally the architecture layer of the program is built out of something more interesting, because "map" turns out to be a very small and incapable primitive to build a real program out of.
In my considered opinion and experience, if you spend time with "functional programming" and come away thinking "oh, it's about 'map' and 'Result'", the point of functional programming was completely missed.
And stop telling people that's what it's about! You're putting a bad taste in everyone's mouth, because when all the imperative programmers look at your so-called "functional" code in imperative languages and say, "That's a nightmare. There's all this extra stuff and noise and it's not doing anything very useful for all that extra stuff."... they're completely right. Completely. It is a net negative to force this style in to places where it doesn't belong, quite a large one in my opinion. And especially stop being sanctimonious about they "don't get it" when people object to this style. It is the one advocating this style where it does not belong that does not "get it".
The worst thing that can happen in one's education is to think you've been exposed to some concept when you in fact haven't, and come away with a wrong impression without realizing there's a right one to be had. I still encourage curious programmers to clock some serious time with real functional programming to learn what it is about. This style of programming isn't it, and your negative impressions of this style don't necessarily apply to real functional programming. (It does some, perhaps, but probably not the way you think.)
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
Maybe I'm wrong, but I think all the talk around weird ideas like Functors, Monads, etc., are mostly red herrings and aren't that applicable to most everyday software engineering tasks.
Just use functions, avoid state, use map/reduce if it's more readable, avoid OOP most of the time (if not all of it), avoid abstracting every single damn thing, and what you're writing seems functional enough even if it doesn't totally satisfy the academic view of what functional programming is.
> Maybe I'm wrong, but I think all the talk around weird ideas like Functors, Monads, etc., are mostly red herrings and aren't that applicable to most everyday software engineering tasks.
They are a red herring. In most cases, all you need to know about a monad is that it defines some kind of transformation of the enclosed data by applying a function onto it that returns a Monad of the same kind.
e.g. the list monad [a] says that if you bind it with a function f: a -> [b] (ie a function that takes a value and returns a list of b), the monad will transform to [b] by concatenating the lists.
the maybe monad Maybe[a] says if you bind it with a function f: a -> Maybe[b], if Maybe has type Some(a), the data of the monad is replaced by the result of the function. If the monad has type Nothing, then it retains nothing. It's no different to
a = f(a) if a is not None else a
So a monad is just an object that defines the transformation of the underlying data when applying a function that returns a monad of the same type, nothing more.
`sanitizeMessage` is literally just `message.replace(/</g, '<')` but it has to go through a ton of abstractive scaffolding just so that it can fit into a single call to map(). This is like jumping through a ton of generic classes in Java only to find a single one-liner implementation at the end of the call.
I believe that his example for sanitizeMessage() was just to give an example. An actual sanitizer would be much more complex, and thus having a separate function defined to do that one thing would be more obviously reasonable.
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.
While there are areas where my functional convictions have greatly diminished, my mid career zeal had the tremendous benefit of illuminating new architecture and data design principles.
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.
Every FP advocate I know comes from an imperative/OOP background… None of them are juniors. None of the “FP skeptics” (or people parroting an obvious variant of uninformative “right tool for the job”) I know come from an FP background.
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?
The problem of the Java code above is that it tries to serve many masters, one of them being the JIT, i.e. the code is written that way so the JIT can fully inline the whole code.
The first "final", the "StatelessOp", the "StreamOpFlag.NOT_SORTED | StreamOpFlag.NOT_DISTINCT", the "int flags" are all here because of performance.
Well your second example relies for speed and efficiency on Haskell being lazy. If you did e.g. a map -> filter -> sum chain, the performance is not that great without lazy evaluation guaranteed by Haskell's implementation, which can complicate code in other scenarios or in the complexity of the compiler. OCaml is also an FP language, and the second example would be almost the same text, but with much worse performance.
Also, Python is an OOP language, but a simple map implementation there is not much more verbose:
True, but I tried to make it as close to the Haskell example as possible.
I just think the OOP -> verbose/complex argument is really annoying, especially if it applies mostly to Java code.
But usually the issue is that people talk about OOP vs FP without specifying the scale, by example the Java stream API hide the mutations / side effects (not unlike Haskell monads do), so you can use
Without going into the core of whether FP is better or not, if OOP/imperative is what 99.9% of people start with, you'll expect to have both FP advocates and skeptics coming from that background. It's just that FP skeptics have tried it and gone back.
In other words, people only convert one way because it's the only way most people can convert.
When I go back to my old map/zip/method-chaining Rust code from a year or two ago, I regret doing so and wish I had written code as imperative sequential operations (looping over numeric ranges and container items, branching, and matching) like I write and understand today.
I was 3 years hardcore Scala, but recently went back to Java for a role.
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.
> What we care about is “can we deliver better code, faster“
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.
The dirty secret is Typescript is going to become the most popular language due to it’s dirty pragmatism and simultaneously very elegant features. It is definitely a get shit done language. Typescript without JS toolchains (for example compiled to WASM would make it very good).
There's no agreed way to organise a functional codebase. You end up with thousands of functions, can you imagine the mess a big team is capable creating? With some discipline I suppose it could work, but you would need some organising principle and some serious discipline. Object oriented is organised by virtue of the object paradigm.
> Object oriented is organised by virtue of the object paradigm.
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.
593 comments
[ 2.7 ms ] story [ 467 ms ] thread[0]: https://bonsai.red
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.
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" :)
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.
It's so funny, because I thought your comment would lead to: when it's 17:00 on a bad day, I'd rather debug some Go code that is perhaps mundane but easy to follow than a chunk of Haskell code of a colleague that drank too much category theory kool-aid.
Which goes to show that what one wants to debug at 17:00 on a bad day is very personal?
It really depends, it's possible to write mundane, simple functional code (though I think more common in OCaml and Erlang than Haskell) but much of the community is sort of very excited about all this higher-order stuff that might be great but is not quite as useful and obvious as the core primitives of algebraic data types and pattern matching. I imagine a lot of people probably felt similarly about the Design Patterns craze with OOP: it's not that OOP isn't useful, just that inheritance is maybe not what you want most of the time and not everything needs to involve design patterns.
I'd rather be debugging an OCaml program than a Go program for sure.
I agree that algebraic data types and pattern matching lead to better code. But even though they were introduced (?) by ML, there is nothing holding imperative languages from adopting them (see e.g. Rust).
Like I said. I'm not extremely religious about it, and I do think a lot of OOP design principles and code practices are slowly heading toward a more FP way of thinking. In that way I think it's sort of interesting that you mention Go, because with Go you seem to mostly work with immutable states and functions, rather than mutable objects, which is more functional than imperative programming, but maybe I just haven't worked enough with Go to know better. If you ask me, everything should frankly be immutable by default, but retrain the ability to become mutable like they do it in Rust with the "mut" keyword. I really, really, enjoyed working with that for the brief period I did. Anyway, I'm not sure I'm ever going to get into religious FP, I may very rarely use classes, but it's not like an abstract class can't be healthy for your 17:00 afternoon self once in a while.
But basically every best practice in regards to OOP that I was taught at university back 20+ years ago, the stuff they still teach today (I'm an external examiner a few times a year), has proven to be sort of useless in the real world for me. Maybe it works in more professional or competent organisations but it sure hasn't worked well in any place that I've ever worked, and yes, it does feel sort of dirty to examine people in theories I disagree with, but it's good money and a good way to keep up with both the CS world and possible hires.
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.
https://thethreevirtues.com/
Incompetence, I agree, sounds either self-deprecating or fake.
Being lazy and incompetent however is always fluctuating. There are days I'm lazy, there are days when I can work 12 hours and feel energized. There is also days where everything in my mind aligns and all the problems melt away. There is also days where I ram my head into the wall on some relatively simple problem.
The point being is that programming is something that no human can do without errors. So you want a language that provides as much low overhead guard rails as possible to stop yourself from shooting yourself in the foot. Even on the days you're lazy and incompetent.
That's how I see the statement they make.
For most of my coding work I take it a step further than this with the simple premise that the code you wrote professionally is not for you. It’s for whoever next has to work on it, no matter their level of expertise. Sure, maybe you ‘just know’ that the equality operator comes between bitwise-OR and logical-OR in precedence, but the code isn’t for you so maybe just use the brackets anyway.
Maybe, because it is all relative.
I know my flaws and compared to my (often unrealistically) high standards, I feel incompetent quite a lot. So I can say the above and mean it.
But if I get criticized as incompetent by someone way below my level, then yes, I might take offense and depending on the mood, also hit back (or ignore it).
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 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.
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
> 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?
It spurned a chain of events that you might find interesting.
I am interested in parallel software and multithreading, so that's the kind of software I enjoy writing.
The branches of a single threaded software can think of potential future interleavings.
Your post inspired me to think of programs as "circular" or not. That is, they return to a steady state and can function properly no matter how many times they run and whether or not the execution is interrupted at any point.
A program that is interrupted can leave the program state in an invalid state that cannot be recovered.
So I started writing a parser to parse an assignment language. I plan to track object identities membership over time and see if objects get stuck in processing - they stop moving.
Isn't that simply because you're a Java developer (as I understand from the rest of your post)?
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.
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.
What's the use case of all this copying and shunting stuff around and consuming massive amounts of memory?
What kind of programs would you write that would benefit from this?
But a more common case is how I get my records from my db. Only the ORM can create those DB record for me ( because I set it up that way )
When I return a response; I compose another immutable response object.
And only my service layer can don that. ( it won’t compile elsewhere)
Wrong. For instance, state can be stored in locals. Two threads accessing the same structure, one reading and one writing, the reader loads some state into locals and the writer then invalidates that state in the middle of the reader's computation, and the reader proceeds to computing a result mixing old state and new state thus leading to inconsistency. This ABA problem is well known and it just can't happen if the structure is immutable. And this doesn't even go into cache coherency issues.
I frankly don't think you appreciate the number of hazards just making data structures immutable actually addresses, and given you don't seem aware of the hazards implicit to mutable data structures, I suppose that's not surprising.
> What kind of programs would you write that would benefit from this
Pretty much every single program that accesses a relational database benefits from this. There are a few of those around n in case you didn't know. Perhaps you've heard of multiversion concurrency control?
Don't you use scope? I don't think I use more than 1 or 2 global variable per system.
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:
It's just that DoSomeTransformation() is not modifying the object, but instead returning a new instance of the same objects, with relevant fields having different values. The obvious drawback here is, for large data structures, there will be lot of copying involved - but that's where the languages with immutable objects are usually "cheating", e.g. by using sophisticated data structures masquerading as simple ones, as to only ever copy things that have actually changed (i.e. "distinct" objects end up sharing a lot of their data under the hood).I can't think of anything where I'd want that. Don't you end up needing infinite amounts of memory? Isn't it absolutely slow as balls copying all that stuff around?
Is it as fast mutable everything? No, some copy must happen, but is a mutable-standard languages as fast as raw hand tuned asm? Also (probably) no, but the trade offs are worth it. It likely matters a lot less than you think unless you're writing actually performance critical tight loop code vs just thinking about performance.
This way:
- passing an object around can always be by reference, since no one can change it
- depending on structures used, for changed bits you don't need to copy the entire structure, but simply shift pointers to old data or new data
- garbage collection can become trivial, and granular, since you know exactly what's new and old, and shifting data around becomes just moving pointers around (again, depends on implementation and optimisations)
There are downsides if you are not careful, of course:
- creating a bunch of new data that references old data will run out of memory, but this doesn't happen as often as you would think.
- sending data between processes/threads may result in copying that data (depends on implementation)
However, the upside is quite good: your data never changes under you. That is a call to func(data) doesn't sneakily change data. And all data becomes trivially thread-safe without mutexes or race conditions.
Why would I want to have a thing in memory, copy it to more memory, modify the copy, then free up the original memory every time? That just seems like a waste.
Var object=object() If (isValid(object)) // Do something
In which the isValid function modified the object in unexpected way and caused the issue. With mutability, I loose confidence in the code and literally have to read implementation of everything the object touches to be sure i understand what's going on. Much more relevant in bigger projects.
In theory the functional approach is more stable and predictable. But whether this really causes a lot of bugs in practice is another question.
You don't do that, runtime does that for you.
The main benefit is better assumptions about code you write. The prime example is how different languages handle object construction and modification:
The question is: what do `date` and `other` contain?Depending on the language the answer may surprise you. Some languages modify `date`. Others don't. And you never know which of the methods exposed on an object modify it unless you read documentation. Neither the compiler nor the type system can help you.
However, if objects are immutable, you are guaranteed that the original object, `date` is never modified. And if you need to use it again, you know it contains the same data without it suddenly changing.
This gives rise to another important property: you can send this to another thread without mutexes or without creating weird synchronization points like Java's AtomicInteger. Since the object cannot be mutated, threads don't have to fight for exclusive access to read it.
for example (pseudocode)
[0] https://en.wikipedia.org/wiki/Value_semantics[1] https://stackoverflow.com/questions/27084007/what-is-value-a...
would appreciate a correction!
This is "safer" for any code still operating on the original object, as it will not be changed unexpectedly.
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.
Not OP but imho, it’s because FP is "sold" as the perfect solution for readability and code maintainability. Just use FP and nothing can go wrong. That’s at least the impression I get when I read about FP.
The fact that one can write abysmal OOP code is nothing new.
To be fair that was the kind of nonsense that was being talked about OOP in the late 90's and early 2000's.
There are no silver bullets, and anyone who claims otherwise is flat wrong.
However most techniques have their advantages, when used well - and I'd say FP has more to offer than OOP in this context.
That’s because it is. FP is not immune to incorrect implementation. Both statements are true.
What I don't agree with the GP is specifically the claims on other languages:
""" Python with immense amounts of spaghetti, C with its hard to control safety, abysmal imperative or OOP code in Typescript ""
These are not sold as advantages for the respective languages. Python doesn't say it's easier for the programmer to write spaghetti. C's unsafe constructs are a known side effect of being closer to the metal (and optimizing for speed at expense of safety). Typescript's selling point is type checking, which is orthogonal to abysmal code.
But FP is just sold as making it easier to make abstractions all the way down. And for many of us FP-skeptics, that's not a selling point, that's a turn off. (And to reiterate my original point - this is the same for Java, OOP and OOP-skeptics)
Only twenty? That'd be a reduction.
If your stack trace doesn't have at least 30 calls of "thingHandler.invoke()", you're not abstract enough.
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'.
It feels egregious to implicate "the entire surface area of functional programming" here, when there are other obvious issues at play.
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.
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.
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.
Do you have a link to a good one?
[1] https://elixirschool.com/en/lessons/basics/pipe_operator
https://cs3110.github.io/textbook/chapters/hop/pipelining.ht...
But their scope can be limited.
This phenomenon seems to happen more frequently with FP or related tools and languages (RxJs abstraction soup comes to mind).
> RxJs abstraction soup comes to mind
Merely a side effect of being implemented in a language that wasn’t really designed for it.
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.
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.
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?
> 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.
This code is only complicated because you insist on following some abstract ideal. The actual way to solve this in python is:
Which is shorten than even your first code sample. If you directly translate your last example into sensible python, you get a nice example of some "simple linear code": The existence of statements in python clearly stands in the way of achieving ideals of pure functional programming. But I think aiming for such ideals is the exact opposite of the point OP was making.For example, if 'foo = g(x)' is defined in one module, and another module does 'f(foo)', then the data flow is preserved. If we try to force things to be the wrong way round, we'll get compiler errors like 'No such variable foo'.
Compare that to temporal ordering of statements; if one module executes 'g(x)' and another executes 'f()', how do we ensure the latter occurs after the former? How could the compiler tell us if they're the wrong way around? Very difficult.
Use "from" and "to", which talks about data dependencies rather than temporal dependencies. Most people assume dependency order implies temporal order, and then you introduce them to lazy evaluation to decouple even that.
“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.
"Time" doesn't matter; causality/dependency is what matters. That is modelled quite well by passing around values. For example:
The 'query' call depends on the result of the 'connect' call, so it must "happen afterwards". The 'connect' call depends on the result of the three 'lookup' calls, so it must "happen afterwards". The 'lookup' calls don't depend on each other, so they're concurrent (note that concurrent does not necessarily imply parallel).This form of data dependency is no different than, say, arithmetic: to calculate '3 × (2 + 4)', the multiplication depends on the result of the addition, so it must "happen afterwards". (The imperative equivalent would be 'push 2; push 4; +; push 3; ×;')
> Worth noting Haskell ended up reinventing the statement/expression distincion in the do-notation
Yes, do-notation "desugars" into plugging return values into arguments, like above. This gets a little tedious for things like 'IO', where we pass around a unit/null value to represent "the real world".
Still, a nice lesson from Haskell is that there's value in making things opt-in; e.g. many languages have Maybe/Option, but it's less useful in languages which allow 'null' anywhere; many languages have IO/Effect/Task/etc. but it's less useful in languages which allow I/O anywhere; etc.
What if instead we said
What is the order of the two calls to `query` actually matters, but how could the language know?The order of execution of code is actually important every time you have such a side effect, and languages that don't strictly define it make this type of thing very error prone.
The language can't "know the order" of these calls, since they are not ordered. No information is passed from one call to the other, hence neither is in each other's past light cone.
If you want to impose some order, you can introduce a data-dependency between the calls; e.g. returning some sort of value from the "INSERT" call, and incorporating that into the "SELECT" call. Examples include:
- Some sort of 'response' from the database, e.g. indicating success/failure
- GHC's implementation of IO as passing around a unit value for the "RealWorld"
- Lamport clocks
- A hash of the previous state (git, block chains, etc.)
- The 'connection' value itself (most useful in conjunction with linear types, or equivalent, to prevent "stale" connections being re-used)
- Continuation-passing style (passing around the continuation/stacktrace)
> languages that don't strictly define it make this type of thing very error prone
On the contrary, attempting to define a total order on such spatially-separated events is very error prone. Attempting to impose such Newtonian assumptions on real-world systems, from CPU cores to geographically distributed systems, leads to all sorts of inconsistencies and problems.
This is another example of opt-ins being better than defaults. It's more useful and clear to have no implicit order of calculations imposed by default, so that everything is automatically concurrent/distributed. If we want to impose some ordering, we can do so using the above mechanisms.
Attempting to go the other way (trying to run serial programs with concurrent semantics) is awkward and error-prone. See: multithreading.
See also https://en.wikipedia.org/wiki/Relativistic_programming
Note that you haven't specified the database semantics either.
Perhaps the connection points to a 'snapshot' of the contents, like in Datomic; in which case doing an "INSERT" will not affect a "SELECT". In this case, a "SELECT" will only see the results of an "INSERT" if we query against an updated connection (i.e. if we introduce a data dependency!).
Perhaps performing multiple queries against the same connection causes the database history to branch into "multiple worlds": each correct on its own, but mutually-contradictory. That's how distributed systems tend to work; with various concensus algorithms to try and merge these different histories into some eventually-consistent whole.
PS: There is a well-defined answer in this example; since the "INSERT" query is dead code, it should never be evaluated ;)
PPS: Even in the "normal" case of executing these queries like statements, from top-to-bottom, against a "normal" SQL database, the semantics are under-defined. For example, if 'query' is asynchronous, the second query may race against the first (e.g. taking a faster path to a remote database and getting executed first). This can be prevented by making 'query' synchronous; however, that's just another way of saying we need a response from the database (i.e. a data dependency!)
Trying to make all statements implicitly concurrent unless they have explicit dependencies is a terrible way to complicate your life. That in some cases you can optimize the code (or the CPU will do it for you) by executing it in certain other orders where it is safe is supposed to remain an invisible optimization.
It is obvious to everyone that distributed code, eventual consistency, and other similar non-totally-ordered examples are much harder to get right than procedural code. Even simple print-based debugging/logging becomes excessively complex if you get rid of the local total ordering.
Even most network-based computing is done over TCP (or, more recently, QUIC) exactly because of how enormously useful having a total order is in practice (even if it's just an illusion/abstraction).
> PS: There is a well-defined answer in this example; since the "INSERT" query is dead code, it should never be evaluated ;)
It's only dead code if you assume Haskell's bizarre lazy execution model. In virtually every other language, unless the compiler can prove that query() has no side effects, the INSERT will be executed.
There are no statements in the above examples, just expressions (composed of other expressions). Evaluation order of expressions is not well defined in "most popular languages", e.g. consider this expression:
> Trying to make all statements implicitly concurrent unless they have explicit dependencies is a terrible way to complicate your lifeI agree, that's one reason why I dislike statements (see my list in a parent comment)
> It is obvious to everyone that distributed code, eventual consistency, and other similar non-totally-ordered examples are much harder to get right than procedural code.
This is a category error. All code is distributed; the world is partially-ordered. "Procedural code" (i.e. serial/sequential execution) is a strategy for dealing with that. It's particularly easy (give each step a single dependency; its "predecessor"), but also maximally inefficient. That's often acceptable when running on a single machine, and sometimes acceptable for globally-distributed systems too (e.g. that's what a blockchain is).
Forcing it by default leads to all sorts of complication (e.g. multithreading, "thread-safety", etc.). Making it opt-in gives us the option of concurrency, even if we write almost everything in some "Serial monad" (more likey, a continuation-passing transformer)
While C and C++ are extremely commonly used, I would still say that the majority of popular languages fully define evaluation order. Even more so since most of these languages considered they are fixing a flaw in C. Rust is particularly interesting here, as they initially did not specify the order, but then reversed that decision afelter more real-world experience.
> "Procedural code" (i.e. serial/sequential execution) is a strategy for dealing with that. It's particularly easy (give each step a single dependency; its "predecessor"), but also maximally inefficient.
> Forcing it by default leads to all sorts of complication (e.g. multithreading, "thread-safety", etc.). Making it opt-in gives us the option of concurrency, even if we write almost everything in some "Serial monad" (more likey, a continuation-passing transformer)
You yourself admit that serial code is a strategy for dealing with the complexity of the world - it doesn't complicate anything, it greatly simplifies things.
Threads and other similar constructs are normally opt-in and used either to model concurrency that is relevant to your business domain, or to try to achieve parallelism as an optimization. They are almost universally seen as a kind of necessary evil - and yet you seem to advocate for introducing the sort of problems threads bring into every sequential program.
Thinking of your program as a graph of data dependencies is an extremely difficult way to program, especially in the presence of any kind of side effects. I don't think I've ever seen anyone argue that it's actually something to strive for.
Even the most complete and influential formal model of concurrent programming, sir Tony Hoare's CSP, is aimed at making the order of operations as easy to follow as possible, with explicit ordering dependencies kept to a minimum (only when sending/receiving messages).
> That's often acceptable when running on a single machine, and sometimes acceptable for globally-distributed systems too (e.g. that's what a blockchain is).
It's not just blockchain: TCP and QUIC, ACID compliance and transactions, at-least-once message delivery in pub-subs: all of these are designed to provide an in-order abstraction on top of the underlying concurrency of the real world. And they are extremely popular because of just how much easier it is to be able to rely on this abstraction.
Serial code greatly simplifies serial problems. If you want serial semantics, go for it. Parent comments mentioned Haskell, which lets us opt-in to serial semantics via things like `ST`. Also, whilst we could write a whole program this way, we're not forced to; we can make different decisions on a per-expression level. Having rich types also makes this more pleasant, since it prevents us using sequential code in a non-sequential way.
However, there is problem with serial code: it complicates concurrency. Again: if you don't want concurrency then serial semantics are fine, and you can ignore the rest of what I'm saying.
> Threads and other similar constructs are normally opt-in and used either to model concurrency that is relevant to your business domain, or to try to achieve parallelism as an optimization. They are almost universally seen as a kind of necessary evil
Such models are certainly evil, but not at all necessary. They're an artefact of trying to build concurrent semantics on top of serial semantics, rather than the other way around.
> and yet you seem to advocate for introducing the sort of problems threads bring into every sequential program.
Not at all. If you want sequential semantics, then write sequential programs. I'm advocating that concurrent programs not be written in terms of sequential semantics.
Going back to the Haskell example, if we have a serial program (e.g. using `ST`) and we want to split it into a few concurrent pieces, we can remove some of the data dependencies (either explicit, or implicit via `do`, etc.) to get independent tasks that can be run concurrently (we can also type-check that we've done it safely, if we like). That's easier than trying to run the serial code concurrently (say, by supplying an algebraic effect handler which doesn't obey some type class laws) then crossing our fingers. The latter is essentially what multithreading, and other unsafe uses of shared-mutable-state are doing.
- 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.
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.
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.
For me the question still remains: how is any of this better than good ol' reliable `for` loop?
I’d prefer a loop with ifs and continues/breaks.
It may be a lack of experience or habit, but then I can write code like that (and worse), and it doesn’t make any sense to me. Don’t get me wrong, not that I refuse to use maps or filters or folds, but not that I want to make a whole program of them either. They have their place where factoring-out a block of code feels stupid, but if I had something like:
I’d never bother with these function-al things. It’s not FP that is rich, it’s a traditional imperative syntax that sucks. Paradigm shift is not equal to usage out of [in]convenience.I'm learning FP and I see value with writing code with map, reduce etc as those are expressions instead of statements. Expressions guarantee you have a result of particular type (with possible side effects if you prefer), but statements DO side effects. The loop may or may not insert some value into resulting list because some branching happened or you forgot some else condition, with map you guarantee you get same number of objects of particular type back.
Plus that enables composition (like in C# LINQ) - by not extending some loop with different kind of responsibilities but just passing result to next function that modifies/filters the result.
https://fsharpforfunandprofit.com/posts/expressions-vs-state...
That's exactly why most FP languages have pipe operator, so it's incredibly easy to read.
orMeanwhile, a for loop is straightforward, you go from 0 to 10 and append a double or add a triple of x to an accumulator. I appended/added them, that’s what I get. It’s like my brain somehow follows {} scopes and understands their local envs with no effort.
If this syntactic style works for you without this friction, nice. But it doesn’t work for everyone and I suspect that this FP/PP is biased by this effect at both sides.
A mapped list/enumerable it was originally given. You don't think what you get when you add two numbers, don't you? Language just works certain way. Not understanding a simple building block of a language isn't a valid argument against it. All you essentially say is that you got so used to OOP concepts that anything else is hard to read. And it's ok, it's the same way for everyone... But it's not a valid argument to say that "fUnCtIoNAL bAd". The whole thing here boils down to what you already said - lack of experience.
My honest advice is - try to learn one functional language, like honestly learn and understand it, try writing something with it. It really does expand horizons.
Is the above code just:
filter cond xs
More like 1:2:filter cond xs.
You use GOTO all the time, even if it's wrapped in an abstraction that makes its intentions less clear.
That said, I believe most people who “boo GOTO” never actually used it to make sense of what could be wrong with it and how it feels really.
Anyway, I think that this analogy(?) is confusing and makes no sense.
Which inspired this great cartoon: https://twitter.com/nice_byte/status/1466940940229046273
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.
What kinds of problems, if I may ask?
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
which are quite funny to look at. That should beEncapsulation and extraction of complex functionality (yes, you should not do it for trivial one-liners).
In Ruby, classes get used a lot for this because, uh, why not, but you might as well just have a module method - it's just that the way modules are used in Ruby makes this slightly awkward because Ruby is after all biased towards "everything is an object".
But in, say, Haskell I might write instead:
(not that "Command.run" looks like super idiomatic code in Haskell or anything like that, probably you'd structure your code differently, but it's the general principle)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".
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.
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.
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.
> 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
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.).
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.
That's very much what Kotlin is aiming for.
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.
…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.
/shrug
Here’s the real question: do you think dense code is more maintainable?
Generally yes? More than a verbose multilayered abstraction, probably?
…but where do you draw the line? Map statements instead of for loops? Collapse all the white space onto a single 200 character line? Make everything one big regex?
Dense code means that every change has more impact, because there’s less code; it’s unavoidable: less code to do the same work means more impact from changing any part of that code.
That is why it’s difficult to maintain; because you can’t touch it without making side effects; you can’t reason about a small part of the code, because the logic is dense and difficult to unpack into small parts.
Certainly OOP can often be verbose and annoying, but that’s a different thing.
Code density and being functional are orthogonal; some OOP is too dense too. …but generally I’ve found that inexperienced people see density and strive for it, believing this makes it functional; but the truth is the opposite.
Good functional programming is often naturally concise, but most people don’t actually seem to understand FP.
They just seem think it means to put more “reduce” statements in the code, remove for loops and generally make the code harder to debug and denser.
…in my, limited experience, working with lots and lot of different folk at many different organisations.
When you have a abstract base class or an interface that defines some contract, it's creating a "shape" that defines how an instance of the class can be used.
I think that this might be why some folks have an affinity for OOP and some have an affinity for FP. OOP affinity might be tied to more visual thinkers. For me, I see the "shapes" of the code defined by the contracts.
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.
While I understand and agree with this meme[1], I think that's the other extreme, where everything is a Factory Builder thing.
Even so, I would rather too much information than too little, which is what FP programs tend to do. Over-abstraction is also a problem, in my view. Even in a LINQ lambda, for instance, I might write
rather than [1]: https://github.com/EnterpriseQualityCoding/FizzBuzzEnterpris...OCaml is designed for straightforward code centered on functions, modules, and a mix of imperative and immutable data structures; all with a _really_ simple execution model and a pretty nice type system. The core language has very few quirks and it's easy to predict what it does, and how efficiently.
There's not really any comparison with Haskell, which emphasizes heavy optimizations to make its terseness, based on lazy evaluation, work well in practice.
some very obvious examples:
- many, if not most, functions have sensible names instead of abnormally terse ones
- it's possible to make named parameters mandatory, and many do that - e.g. the base library
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.
Over the decades I've come to the conclusion that there's no such thing as a one size fits all sweet spot on this stuff. Different people are going to have different experiences with what they find straightforward or not. They will have different backgrounds, mental models, ways of perceiving the world. It all adds up. As a profession we need to understand this reality and find ways around it instead of getting into dogmatic arguments as if there's One Right Answer.
Common example I give - the GP complained about FP advocates wanting code to take up less screen space. I have come across many devs who struggle with concise code, and many others who struggle when code is not concise. Similarly, I have come across plenty of devs who start having trouble when code is spread out (sometimes that means within a file, across files, both, etc). I have also come across plenty of devs who have trouble when it's all pulled together.
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.
I don't think this is possible with JS right now?
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
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.
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.
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.
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:
When FP gets bolted onto a mainstream language after the fact, you get to program with: Then the FP skeptics think it's too complicated, and just want to go back to:But seriously I dislike the notion that FP is somehow a successor to OOP, like you have to keep piling abstractions onto OOP until it becomes FP!
If you do it that way, you end up with map() implementations like:
Ain't nobody got time to read that. Step back from OOP before putting in the FP and you can get map() looking like:Building FP functionality with classes sounds just as fun as building java classes with assembly.
The first "final", the "StatelessOp", the "StreamOpFlag.NOT_SORTED | StreamOpFlag.NOT_DISTINCT", the "int flags" are all here because of performance.
Also, Python is an OOP language, but a simple map implementation there is not much more verbose:
The only thing unique about the Haskell example, I think, was pattern-matching... And while that's nice, you don't really need that for FP.
But usually the issue is that people talk about OOP vs FP without specifying the scale, by example the Java stream API hide the mutations / side effects (not unlike Haskell monads do), so you can use
But internally it's stillIn other words, people only convert one way because it's the only way most people can convert.
ie.: you need to be very familiar with something before you can reasonably criticize it.
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.
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.
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.