The recent focus on adapting functional programming is a reaction from the shift to the dangerous quadrant of the immutable-mutable/unshared-shared "Magic Quadrant" chart.
Before multi-core, multi-threading, it didn't matter how or when you decided to update your local variables, they were YOUR local variables... unshared.
With threading, and multiple cores, we all just blindly moved right to the mutable/shared quadrant... the one in bright red... which effectively changes the laws of physics of your programs. Effectively every piece of code now runs in it's own time and space, and if you don't coordinate things correctly, it's like killing your own grandparents.
It took me a long time to understand why you'd want to refactor your code until everything was immutable[1], but now that I get the lesson, its something I won't forget.
Functional programming avoids mutable data, and thus, intentionally or not, works well in a world of shared data. Because pure functions have no side effects, they are timeless - they need no synchronization.
Spend a few nights watching everything Kevlin Henney has said in the last few years, and you'll have a much better handle on things.
[1] - https://youtu.be/APUCMSPiNh4
[Edit] Incorporate wording suggestion from DonaldPShimoda
> Functional programming is a reaction from the shift to the dangerous quadrant of the immutable-mutable/unshared-shared "Magic Quadrant" chart.
This makes it sound like you’re saying that’s why functional programming was created, which isn’t the case. I assume you mean that this is why it’s gained popularity but I don’t really agree with that - it’s certainly valuable but most functional programmers will never touch threads. I’d argue that programs without state are just easier to reason about and debug and for most software this is where the value comes from.
It's not possible to do always but it feels like it should be possible in special cases. If you can prove that a value V2 is ultimately derived from a modification of V1, and V1 is never referenced after creating V2, then it should be possible to invisibly convert this to an in-place mutation [1]. In the case of applying a bunch of text normalization rules to a string, for example, I care about the final string but not the intermediate states. It could be mutation underneath and I'd be none the wiser, except that the runtime performance would be better.
In Scala you can add a "@tailrec" annotation to recursive functions. The annotation means that the function is guaranteed to recurse efficiently (without growing the stack) OR that the compiler will give an error telling you why it can't be guaranteed. I could imagine a similar annotation like "@withmutation" for functions where you want to guarantee efficient in-place mutation at runtime. As it is, when I run into performance pain points in Scala I redo the critical bits with (manually written) while-loops and imperative mutation.
[1] Unless you later attach a debugger and expect to be able to observe V1 and V2 simultaneously. But I presume this sort of issue already comes up and is somehow dealt with regarding other compiler optimizations.
Immutability is something that happens at the level of the programming language. Runtime efficiency is a concept that, unsurprisingly, lives at runtime.
When eg a Haskell program runs, its supposedly immutable datastructures get mutated all the time. For example laziness is one way that happens.
Another example is that a (pure) Scheme program will overwrite the same memory addresses over and over again while it is running, just like a C program would. Conceptually, no mutation happens, it's just that the garbage collector constantly frees up memory that will be get re-used immediately.
Is there data supporting the notion that immutable structures are slower?
There are many cases where there won't be a single difference in performance and there are a bunch of cases where immutable structures will be way faster than mutable ones.
> There are many cases where there won't be a single difference in performance and there are a bunch of cases where immutable structures will be way faster than mutable ones.
Have you maybe written this backwards? It's literally not possible for this to be true.
Mutable data structures have strictly more operations that they're allowed to do, how would they be slower? At very worst they could ignore modifications and be exactly as fast as an immutable data structure.
When you need a state that you can rollback, an immutable data structure lets you share the parts of the structure that didn't change between states. That's what ZFS does and that's also how many transactional databases store data.
A mutable data structure would need an entire copy at each state change or it would need to store deltas and apply them backwards when you rollback. Both options are significantly slower than the immutable data structure (where rollback means updating a single pointer).
Those immutable data structures of ZFS are implemented in C and C++, where everything is mutable. You're actually confirming kadoban's point: Mutable data structures can pretend to be immutable or partially immutable, so there can never be an immutable data structure that's faster than all possible mutable data structures.
And I'm pretty sure that if ZFS creates an updated version of some immutable node, under the hood it does this via something that resembles a memcpy() followed by a mutable modification of one or more struct members.
> You're misunderstanding ZFS' persistent data structure. It's the whole point that it's not doing a copy then modifying something.
I'm talking about the internal implementation of those data structures, not about their API. Neither C nor C++ have built-in immutable data structures, so those persistent data structures with their immutable API are implemented using mutable C structs or C++ classes.
> Also no one claims there's one immutable data structure that's faster than all mutable data structures. That's a ridiculous strawman argument.
No, you're misunderstanding what I said: For any given abstract functionality or interface (like a key->value map, or a graph composed of vertices and edges), there exists a mutable data structure which is at least as fast as all immutable data structures that provide the same interface. That's because the set of mutable data structures is a strict superset of (i.e., contains all) the set of immutable data structures.
> Neither C nor C++ have built-in immutable data structures
First of all, yes they do, you can use const on structs and objects and methods to ensure your C/C++ data structure is immutable.
But this is moot anyway:
> so those persistent data structures with their immutable API are implemented using mutable C structs or C++ classes
ZFS isn't something that's in memory, it's a file system… It's a data structure on disk, it's language-agnostic.
> That's because the set of mutable data structures is a strict superset of (i.e., contains all) the set of immutable data structures.
In theory, that's true. It's just a useless fact.
We use immutable data structures because they have benefits. Using the same data structure without immutability may just render it worse as a software tool. So people don't use immutable data structures with the immutability removed.
Immutable data structures are shaped by trade-offs like any other design.
> It's literally not possible for this to be true.
The most obvious operation that's much faster for an immutable data structure is cloning.
Cloning, say, an array in a mutable system takes O(n) time. Cloning in an immutable system is just returning the pointer (or whatever pointer + length struct you're using to represent an array).
> Cloning a mutable array is also O(1) if you use copy-on-write.
Note that this is just an example of the idea that mutable structures can achieve anything immutable structures can, by just treating them as if they're immutable. While you're using copy-on-write, your mutable array can be cloned in constant time and updated in linear time, presenting the exact characteristics of an immutable array.
The mutable "equivalent" algorithm is much nastier, though, since, after you do the cloning, you'll need to create the new object (the slow, O(n) clone) as soon as you want to update either the new structure or the old one. In the functional paradigm, this is also the case -- but you're always generating a new object that you're about to use, as opposed to generating a new object because something else somewhere else in the code was watching you. For example, if you have an array, and you update it (forcing it to be realized), and then you update it a second time, you can't rely on the first time you updated it to prove that the second update will be fast. Maybe it was cloned between the updates.
When you use a mutable data structure, and your code is concurrent, you'll force the CPU to put locks everywhere in your code so that several cores won't see stale data in their internal caches.
Even when you aren't writing concurrent code, mutability will prevent the CPU from using its internal concurrency to execute it faster.
Also, immutable data structures can include caches that are extremely simple whereas mutable data structure would face the problem if cache invalidation if they tried.
And a mutable data structure that would operate as if it's immutable? That's a nice recipe for an epic disaster.
> When you use a mutable data structure, and your code is concurrent, you'll force the CPU to put locks everywhere in your code so that several cores won't see stale data in their internal caches.
Do you mean to say the compiler will insert locks? As far as I'm aware, on AMD64/Intel64, the processor won't lock the bus for ordinary loads and stores unless an instruction has the lock prefix. The processor is otherwise perfectly happy to let different cores observe loads and stores to different addresses in different orders (except for stores being reordered past each other).
> Even when you aren't writing concurrent code, mutability will prevent the CPU from using its internal concurrency to execute it faster.
For an out-of-order superscalar machine, mutability has nothing to do with it; register renaming takes care of that. The things to watch out for are tight dependencies chains.
If you're referring to loads and stores, store-forwarding and coalescing can spare you from having to hit the cache repeatedly for reads and writes to the same location.
> Do you mean to say the compiler will insert locks?
I think you're right, it's the compiler that'll insert the locks according to the semantics of the language.
> For an out-of-order superscalar machine, mutability has nothing to do with it;
Yes it does, because it creates data dependencies. If a piece of code B loads the contents of memory where a piece of code A writes that's before B, then you can never execute B before or in parallel with A.
> If a piece of code B loads the contents of memory where a piece of code A writes that's before B, then you can never execute B before or in parallel with A.
This is a description of what a data dependency is; I'm still not sure what your point is regarding mutable data structures specifically.
A mutable data structure will have such data dependencies all around. An immutable data structure cannot, because it will never be written in after creation.
Take your favorite immutable data structure. Fix it in your mind.
Now, call it mutable, but don't change a thing. Congratulations, it's now a mutable data structure that's exactly as fast as your favorite immutable one.
_But_ you now have extra opterations you can perform to optimize slow parts if you can. You have strictly more power than you started with, because you can modify data. You don't have to, but you can.
Just don't make any bad optimizations. If some attempt to make it better instead makes it worse, go back to the starting point and try something else.
The key is that you can start with any immutable data structure you like and change it as much as you want until it stops performing as desired. If that's errors (inability to reason about it correctly) or some other issue, doesn't really matter.
The point is that this procedure applies to anything. Immutable data structures are a strict subset of mutable ones. Every immutable data structure is open to moving any number of steps, in any direction, in the space of data structures. It's _purely_ extra freedom that immutable data structures don't have.
If you want specific ones, first things that would come to mind for me is to start with any immutable tree impl, and start adding small mutable ~caching things on top. Batch up updates a bit to try to avoid repeated deep traversals maybe. You can do this without adding much complexity at all, and it's very flexibly for whatever operations or access patterns you want to improve.
Searching wide and far for a data structure that's that niche in implementation is not really sensible, I'd probably have to implement one myself. If the sketch above doesn't satisfy you, that's about the end for me. Have a good one.
You seem to forget that the CPU and memory system, on which you're executing your immutable data structure implementations, provide a fundamentally mutable interface. So the compiler of your immutable language will still have to insert locks and atomic instructions (although not necessarily the same or at the same places).
> So the compiler of your immutable language will still have to insert locks
No it won't, not at the myriads of places they're needed when there are mutations all over the place.
Of course there are mutations in FP, so of course sometimes we need write locks and use things like CAS.
The point is not that FP doesn't use them, but that it uses them far less because they are needed only in a very limited number of places.
When I write code with STM, for example, the only uses of locks or CAS are in the STM engine, not in my Haskell code, and not even in my compiled code, because my code only ever reads memory that's immutable, and immutable data structures.
When I write pure code, there are no locks at all. And that makes it trivial to execute in parallel, either out of order or on several cores.
The most obvious case I'm aware of is with numerical simulations in scientific computing. I worked on molecular simulations in graduate school. The core data structures are large (many-gigabytes) arrays, and almost every element of the array is updated on every iteration. Everything that requires more memory or even more memory accesses is going to be slower. In business applications you're usually not updating everything in a data structure all the time, so I can see how clever immutability may improve performance.
But a huge mutable data structure means you can't cache the content between cores of a CPU or between systems in a cluster. So you're describing a scenario where mutability incurs a serious performance penalty.
And why are you saying that requiring more memory would make a program slower? I don't see the link.
In one of these simulations, the logic is something like "calculate all the new positions and velocities of the molecules in a box, updated by one micro time step." It's implemented by updating a bunch of matrices in-place by mutation, often using linear algebra subroutines provided by a high performance BLAS like OpenBLAS or the one from Intel's MKL. The matrices are large enough that you'll have to page to disk if you return a new matrix from an old matrix instead of updating the matrix in-place. That's why requiring more memory makes it slower.
At the hardware level, transforming a numerical array M1 into M2 by mutation in-place requires a certain number of memory reads and writes. Leaving the old matrix M1 untouched and returning M2 as a new value (in the immutable style) means that you require at least as many memory accesses as before, but cache locality is likely worse because you're not updating the same address region you are reading from. So I'd expect numerical simulations based on immutable programming techniques to run slower than current simulation tools (like NAMD, AMBER, GROMACS) that perform mutation in-place.
> Functional programming is a reaction from the shift to the dangerous quadrant of the immutable-mutable/unshared-shared "Magic Quadrant" chart.
I might rather say "The recent focus on adapting functional programming is a reaction...". Functional programming itself is nearly as old as the modern computer (McCarthy's first LISP paper was published in 1960), but your phrasing kind of suggests that the entirety of FP is a recent innovation.
Functional programming itself is really old. Practical functional programming for scale is the new trend.
What's new is the scale of software being developed right now. Developers simply can't afford to create mutable and shared state as if their program was just another desktop program, executed on a computer disconnected from the internet.
Sooner or later we learn that focusing on the human value of code (I mean, making the code clear, concise and simple to read) pays off way better on the long run rather than optimizing for CPU cycles or memory footprint.
Your global variables were / are shared with yourself, even in no-concurrency case. With typical control flow being far from linear, it's easy to surprise yourself with unexpected state mutation.
Right. Pure functions make it considerably easier to reason about code, even in the absence of concurrency.
It's a shame D's pure modifier (to statically guarantee purity) isn't available in many other languages. [0] SPARK Ada has something similar in that functions must always be pure, forcing procedures to be used for impure functionality. [1] edit I think that's mistaken, SPARK appears to permit functions to read globals but not to modify them.
edit John Carmack has written about this kind of thing. From [2]:
> if a function only references a piece or two of global state, it is probably wise to consider passing it in as a variable. It would be kind of nice if C had a "functional" keyword to enforce no global references.
Giving me flashbacks to inherited codebases from old-school FORTRAN coders...
// 500 variables shared by everything
main loop
"task" 1
"task" 2
"task" 3
...
"task" n
So t121 is now supposed to only be used by task 3, but "t" as a prefix means it is temp. However, we don't want to change all the references. So just don't use it everyone else, please?
What are the advantages of complete immutability when we can write compilers that specifically prevent shared mutability? You're avoiding the dangerous quadrant but also excluding a valid one. I'd definitely prefer FP's solution over Python's GIL, but it still feels like an unnecessary compromise.
Most functional languages allow mutability when you need it, they just default to immutable data. Haskell might be the exception, where everything has to be immutable all the time
This is not true, Haskell of course allows you to mutate state. It just forces you to declare it: For example, there's a MutableByteArray type whose operations only work in a specific Monad (because Haskell...) [1].
There's also more basic stuff like IORef which represents an assignable variable [2]. Again, you're constrained to use this in IO contexts.
Etc.
Haskell basically just forces you to do that thing someone else in this thread mentioned: Write functional APIs, allow side effects but make them explicit and confined to very specific places.
Everything does not have to be immutable in Haskell. It's only that mutability can only exist in IO or some other context that deals with it. The language enforces purity, not immutability.
I've worked in two functional codebases so far and I'd say that in both cases the mutation of shared state was just moved to database and its transaction semantics. The FP code itself was about mostly stateless server-side processing. So, FP wasn't doing anything for us on that front. The main selling point for it was that it makes program more composable, more concise and easier to refactor (as you're effectively working with higher-level abstractions).
>> in both cases the mutation of shared state was just moved to database and its transaction semantics. The FP code itself was about mostly stateless server-side processing. So, FP wasn't doing anything for us on that front.
Sure it was. While it didn't force devs to stop using mutable state, you just called out that it -did- force them to move that state out of the service and into something with ACID guarantees, and write their server code in a way that was far more stateless. That sounds like a win to me?
You can also do something like that by using a vanilla ORM and/or batteries include web framework (which includes that) in any popular language of your choice (Spring, Rails, Django, etc).
Sure, if you ensure that every bit of state is going to the ORM, and the ORM caches nothing. Of course, that tends to ensure that what is and what isn't side effect free is massively obfuscated, which seems like you're including some new negatives, but I will totally agree there are other ways to ensure state goes to the state store; I already did.
For most standard web applications, it is true that the RDBMS stores all durable application state; it's also the case that you avoid ORM caching because it introduces more problems than it solves.
Saying "that tends to ensure what is and isn't side effect free is massively obfuscated" really depends on the ORM; if it's an ORM that tries to make an RDMBS "quick" like an object, then I agree (this is something I hate about Django's ORM), but if it's a query builder style ORM then I disagree, as it basically creates a DSL that wraps around the SQL which is already purely functional.
What I'm getting at is that we shouldn't conflate what should take credit for creating sanity in how applications persist state. I think that credit should go to the RDBMS because it exposes the power of the benefits you ascribe to FP, to any programming language that can bind to an RDBMS. If that is the case, what do you really need FP for in general purpose line of business software engineering?
I’ve been bitten by mutable data changing behind my back even without concurrency. It can happen in code that is too magical, eg by having various listeners trigger that aren’t explicit in code (for example “pre_save” hooks in an ORM I have to use in work). It can also happen in deeply heated call chains (my code calls code that calls code that calls code… that mutates data it was given access to at some prior point).
The change is technically in your call stack, but it’s indirect and not necessarily related to your code.
If it were immutable data, then some distant code that holds a reference can’t mutate it unless that data is explicitly marked as mutable.
To me, immutable data is a guarantee that a variable keeps its value, unless I explicitly say it can change (eg by wrapping it in a mutable “box” whose content can be changed). This way, I always know whether I’m giving something write access or only read access. Technically this can be achieved without immutable types (eg constant parameters, pass by value or just documented API promises) but having immutable data as per of the language gives an extra layer of guarantee that’s harder to violate and therefore more predictable.
That is why I like immutable by default, even in single threaded code.
This resembles a situation when I got a new boss who once tried to write a 2-threaded app and got burnt, so from then onward he mandated to never use multiple threads (despite me using it safely on 65000+ threads before). A better answer would be "most devs aren't skilled enough to use many threads/mutable shared objects, so lets mandate immutable for basic devs so that they don't get burnt". But why would you prevent proven experts from operating in the dangerous quadrant? Does everything have to become dumb to make it safe?
That dangerous quadrant has a track record of chewing up and spitting out "proven experts". I'd put your question in the same class of question as "Look, I know memory management in C is just dangerous, but do we have to make everything dumb to make it safe?" False dichotomy; the solution to everything isn't to "make it dumb", but the proven experts have a long track record of being chewed up and spit out and the answer isn't to tell them to just Git Gud either. We tried that. We tried that a lot. It doesn't work.
The solutions to concurrency pretty much all involve staying out that quadrant as much as possible. Immutability isn't the only solution. Another is to confine all variables to one thread. Erlang, for instance, superficially uses immutability to achieve this, but a deeper reason it achieves it is that there is simply no way for a "thread" (what Erlang calls "process") to modify a variable that another thread can see; this is sufficient. (See what Elixir does.) Rust implements a super-rich system of variable ownership that allows you to implement the constraint that no variable can be unexpectedly just modified by an arbitrary thread in a far richer fashion than just hard-locking all memory to a particular owner thread. This is also sufficient. I program a lot in Go, and while it lacks compiler support for forcing you to stay out of that quadrant, my code runs at high concurrency rates precisely because I've learned how to stay out of there, and my code looks very Erlang-ish when you look at the concurrency patterns in play.
new boss who once tried to write a 2-threaded app and got burnt, so from then onward he mandated to never use multiple threads
An unfortunate overreaction, I agree.
Does everything have to become dumb to make it safe?
I don’t think trying to avoid shared mutable state is dumb. It’s deliberately choosing a safer default, just like not allowing all references to be null or not accepting the string "123" where an integer like 123 is needed.
Sometimes it can be useful to have shared mutable state, or to make a reference nullable, or to accept different types of data and quietly reinterpret them the same way internally. But these are inherently more dangerous styles of programming — they create opportunities for mistakes that are otherwise systematically prevented — so it seems reasonable to encourage the safer alternatives unless there is a good reason not to use them, and to make the more dangerous choices the ones you have to make explicitly.
The question is if the "unsafe" one really doesn't offer much benefit. If the unsafe one leads to a 3x speed up or a 10x lower memory consumption, then I'll use it.
Functional programming is representing the program state as a single expression, so that the compiler could do funky maths stuff in reordering things in it. It has little to do with immutability or functions per se.
I've been taking a kind of middle ground lately: build functional APIs. Move your side effects as far outward as possible and make your library's interface take everything it needs and return everything it computes. No HTTP calls, no database access, no random number generation. For example, don't have your library build the query and then run it, have it built the query and return it so it can be run at a higher-level.
I know this seems obvious, but it was a breakthrough I had with organizing the way I built things a few years back and it has been serving me amazingly well. I think I originally stumbled onto the idea through a talk on domain-driven development.
The result is that you get a lot of the benefits of functional (easy testing, portability, etc) but you don't have to deal with all the oddball patterns of recursion or currying or things that, yeah, sure, makes sense on some theoretical level but makes me want to gouge my eyes out when I try to read it. I've tried fully-fledged functional programming in a few cases and it doesn't click with me. I appreciate and understand that some love it, but in the end if your exposed public API is functional, the internals are less important.
To me the important thing is the mindset: move your side effects as high up the chain as you can.
The term for this that I have come across is "stratified design" and goes back to the book Structure and Interpretation of Computer Programs by Abelson and Sussman. I have no idea why this approach isn't more well known (especially compared to typical "layered" design approaches) as the benefits are so great!
I tried learning Haskell for fun while working as a Python dev. Understanding importance of managing side-effects definitely influenced how I changed my approach to code organization at work.
Is there a good open-source example of this paradigm? I'm interested by the idea but struggling a bit to see how it would actually be implemented for something like, say, a regular REST/JSON API.
It's a cryptographic identity protocol. It's architected such that the identity is not a document, but rather a DAG of transactions that build the document (more here: https://stamp-protocol.github.io/).
This core library uses the "functional" api aspect I'm talking about. It lets you create and run DAG transactions, but doesn't save them or generate keys for you or anything like that. When you create a transaction, you have to pass in all the data required to complete it, and when it completes, it returns back a full transaction set (or an error).
There are some utility functions in there that are not entirely functional, but the gist of it is that 95% of the things you do have no side effects and have to be dealt with outside of the library itself.
As far as a REST API, you're dealing with a large number of inputs/outputs (side effects) so this paradigm might be limited. However, you can still think in terms of side effects (HTTP request, HTTP response, database, third-party HTTP calls) and compartmentalize your logic from those operations into functional units: if I `POST /users?name=andrew&likes=[dogs,programming]` then instead of going
Here, you flatten out your call stack, functionalize your calls (create_user_instance and create_user_query will ALWAYS return the same values given the same params) and move your final database call up the stack (as high as you can afford to). Obviously calling the DB in a route is dumb, but hopefully you get the general idea.
You may find it interesting to study FP a bit more, because recursion, when understood, is pretty universally felt as way more readable than its imperative alternatives.
I also usually find currying extremely practical and easier for me when it's available.
Most of us that only use FP in our day job don't do it out of masochism, AFAICT. I certainly don't.
Yes, recursion can be used to define a function inductively, just as we would in maths. However, recursion is the "GOTO" of functional programming and we prefer to used structured programming if possible, e.g maps/folds/unfolds etc. That would be a better reply for the parent post.
Of course I use combinators more often than I write recursive functions. But if someone finds recursion less readable, I still think they're missing something crucial.
And I don't think recursion is a GOTO that should be avoided. It's a useful tool that's easy to read and reason about.
I agreed with your parent post and upvoted you, but too many inexperienced with functional programming claim it is all (or too much) recursion. I think this should be challenged.
Yes recursion is a useful tool, just like GOTO and is sometimes necessary or more practical. But it is better to e.g. use a fold if you can because that further constrains and aids reasoning. For example, a fold will always terminate unlike general recursion.
I recommend looking into recursion schemes, if you aren't familiar with them.
I used to think this until my new project where everything is functional, everything is reactive, everything is a lambda. Each function is very simple but the whole thing is impossible to trace.
I've seen functional code like that as well, and it's truly awful. If all FP was that way I'd avoid touching it with a 1,000 foot pole. Most of the time though it's a developer who writes bad code no matter which language/paradigm they're in, but FP does give them tools to make it even more unreadable
I have seen this before, but I think the problem is the code, not the paradigm. If you bend your mind a little and think of stateless functions as a chain of (OOP) method calls, function arguments as class instances and namespaces those functions as classes themselves, then there is little reason to think that it should be impossible to trace, given equal familiarity with the tooling.
Check out DOP https://blog.klipse.tech/dop/2022/06/22/principles-of-dop.ht... and Rich Hikey's critique of OOP (~'objects are custom languages on top of data'; they force you to learn specific semantics to manipulate plain data structures and make it tough to reuse your code).
> (~'objects are custom languages on top of data';
how is that a bad thing - that's literally the point of the activity of programming, creating domain-specific sublanguages to manipulate data in a way that is readable by domain experts of this specific data
The argument is that this default forces you into creating DSLs for basic data manipulation (it's all [] and {} in the end).
You end up with 'custom' logic with custom naming that increases the overhead of using your codebase. That's a long way of saying your code is less reusable.
In DOP/FP land, you create functions to manipulate basic data types and your domain-specific knowledge ends up encoded as basic data types (say you want validations on a model, you'd encode those as a {} and create functions to validate vanilla []/{}).
Objects are great for modeling data as long as you still think of it as data, and you can write good data processing programs using OO language constructs.
OOP as an approach to programming gets it backwards, though. OOP encourages you to think of the objects as primary and the data representing them (on the wire, in a datastore) as a secondary, inferior manifestation. This is the opposite of true. The software's value is created by its handling of data on the wire, reading data from other systems and writing data to other systems. OOP objects provide no value except by reading and writing data. If you start believing that the objects provide the value and the data exists to support the objects, as OOP encourages, then you start to suffer from all kinds of delusions.
Once you start thinking that way, you start wondering why Objects, rather than (from Java) records, or (from most every other language) structs/tuples.
That is, why bundle functions with the data and encapsulate it as a new type that ties them together? It invites the very thing you're saying not to do; if you want to think of it as just data...then make it just data. Nouns only; no verbs. Even if you have a verb as data, treat it as a noun (i.e., a higher order function).
It's all translatable; you can break an object into functions and data, and create an object bundling functions and data, but, as you say, it's about how you think...and objects do not encourage you to think about the underlying data, but about the abstraction. And abstractions are leaky, don't translate well into new domains, and are much harder to communicate (both at an API level and between humans) than data.
The semantics of objects are pretty straightforward, but the assurances that they provide on top of your data is their main value add in this context.
Yes, things like protobufs, or equivalents thereof provide you with a list of auto-generated low-level assurances, but there are often bits of non-trivial business logic about data modification/reading that cannot be easily expressed outside of a object implementation in a Turing-complete language.
They also clearly express a concept of, and when used correctly, boundaries for data ownership, which for non-ephemeral data can be important.
Funnily enough, I disagree — I find that the overwhelming majority of problems with OOP come precisely from trying to use it with data. The only thing it’s good for is precisely control flow at the macro level.
I started with mainstream procedural and OO languages like C++ and Java. I felt like I had a pretty good understanding of OOP by the time I left university. Later, learning a little about FP took my OOP to a whole new level. I really recommend reading Sussman and Steele if you haven't already, it's basically a constructive proof showing how to represent objects in a functional language. Unfortunately the title is so boring that generations of CS undergrads skipped reading the paper, so the zeitgeist forgot. Hewitt was another important researcher at the MIT AI lab at that time credited with discovering the Actor Model, which basically combines objects with a concurrency model. I've seen him post on HN occasionally =)
So many people think "OOP == classes", and I think it's really a shame that (in many ways superior) alternative representations were relegated to the sidelines for so long. I see Java's relatively recent inclusion of algebraic datatypes as a tacit admission that contemporary computing requires different primitives, and I expect to see a shift in best practices towards objects represented as immutable data structures combined with effectively-pure functions. Just like the FP folks have been saying all along!
> This work developed out of an initial attempt to understand the actorness of actors... Sussman suggested the experimental approach of actually building an "ACTORS interpreter"... When it was completed, we discovered that the "actors" and the lambda expressions were identical in implementation.
I don't know if you're being serious or not, but you can learn a ton of FP without having to understand monads. Also asking to explain "objects" is too ambiguous. an "object" in json is very simple. OOP on the other hand involves a mountain of different concepts, design patterns, best practices, etc. I would guess GP is referring to that stuff.
You can write OOP code without knowing a single pattern and whilst laughing at Factory classes you read in someone else's code. You might have to reinvent the wheel sometimes and face some difficulties talking with people that know every single pattern.
And there are best practices for every single language and paradigm.
Yes, I can definitely explain monads to a 5-year old.
See, a monad is like a box. You can put something in the box and close it very easily. But once it's closed, you can't open it anymore. The action of closing the box is called the wrap operation.
What you _can_ do though, is tell someone else to do something to the thing inside the box. For example, if you put a toy in and close the box, you can ask a friend to go ahead and add a new one in. Or remove the toy. These are monadic functions.
That's it. That's literally it. It's just a pattern of hiding the data and letting monadic functions be the only ones that deal with it.
In the case of the Maybe monad for example, you'd have the unwrap function that tries to get whatever is inside out, but it might not be able to (if the box is empty).
Not going to lie, I don't understand that explanation. It honestly sounds more like you are describing a form of encapsulation, as the contents of the box are hidden and someone else has the ability to open said box to get access to it.
I also find the appeal to a physical object to explain a functional concept is rather amusing.
Not sure if you are looking for an explanation of Monads, but Monad tutorials are a long running joke in the haskell world. They are almost exclusively written by people for whom the concept just clicked and they either fail provide context for someone without that experience or they fail to actually describe the concept correctly.
I'm not gonna give you an explanation but I will say that IMO the best way to actually learn about them is to just look at the typeclass and a bunch of instances of it and how they are used. If you ignore all the theory (and the "theory") and just look at the code you will find that they aren't that hard to sort out from a usage perspective.
Apologies, I seem to have deleted a sentence where I think I can make my understanding fit that metaphor. I don't think they are completely unapproachable, but I am not at all clear that a physical metaphor works. And, I really wanted to shine a light on the amusing appeal to physical objects in a thread that is about the short comings of object oriented programming. :D
No need for apologies! I 100% agree that the physical metaphor doesn't work and said so in another nearby comment.
If you go back to Phil Wadler's original paper on implementing monads in haskell, he doesn't talk about category theory or boxes or anything. He lays out a handful of common things you might do in programming but which seem totally unrelated. For each one he implements a solution and then reveals that all these solutions fit the same interface.
At some point, it dawned on me that my problem wasn't that I didn't understand monads, it's that the idea's so simple I couldn't convince myself I understood it, and that what I actually didn't understand was why anyone gave a shit about them.
Like if a bunch of smart people keep going on and on about something called a "floobiz", and every time they try to explain it to you, it just looks like a fucking cup. The problem isn't that you don't understand what a cup (floobiz) is, it's that you don't get why they keep going on about them like they're something special.
> I also find the appeal to a physical object to explain a functional concept is rather amusing.
The challenge was to describe it to a 5 year old. 5 year olds are better with concrete thinking than abstract reasoning. This is what made using the turtle in Logo a genius move, as it allowed younger children to write programs by conceptualizing a physical turtle that can move, rather than thinking in terms of an abstract function that mutates data.
Fair, and I don't necessarily disagree. I think this leads rather close to object oriented software being easier to reason about concretely than functional programming, though. And since most people are trying to get a concrete job done, that feels like a significant claim.
For turtle geometry, it is also important to acknowledge the metaphor and shortcomings of a more traditionally analytic framing of drawings. Specifically, X and Y coordinates for a drawing are surprisingly hard to work with. For example, I challenge you to describe the fractal snow flake in an easier way than using directions as a metaphor. Same for the dragon curve. This can be seen akin to picking a different coordinate system, I presume?
Well, for IO, it seems to me that maybe it still works. You're putting the entire external world in the box, but you're still (kind of?) doing the same thing.
You want to assemble a 16-piece puzzle, but there's too many of your friends around for you all to work on it at once, it would be a disaster. So you invent a game. The rules are:
- Everyone only gets to put a single piece. There's 16 of your friends, so it works great.
- You all sit in a row on the floor, and the puzzle gets passed from the last to the next.
- Nobody can talk. All you can do is put a piece in and pass the puzzle.
You then realize that there's actually 17 of you (you have 16 friends and forgot to count yourself) so every turn one of you sits at the end, receives the finished puzzle and checks it.
It works great the first few times (you have _a lot_ of puzzles), but then something happens. One of your friends lost a piece. They can't talk, so they just panic and the turn ends.
What you need to do is change the game a bit so this doesn't happen again (it's better to get no puzzle than a panicked friend). So you add two rules:
- If you can't add your piece for some reason, you pass a piece of paper saying why instead of the puzzle.
- If you receive a piece of paper, pass it to the next person.
With everyone equipped with the paper and the piece, the next turn starts. Your friend lost their piece again, but there's no problem: you've got a lot of puzzles, and more importantly, your friend knows what to do. They write ‘I lost my piece’, and give the paper to the next person.
You, sitting at the end, receive the piece of paper and know exactly what happened.
Congratulations, that's the Either monad with a puzzle as the Right, and a piece of inscribed paper as the Left, and the ‘bind’ operation is the rule set that explains how you communicate with the next person.
You can use the same kind of explanation for State, except you come up with a game that uses the same object as the state and every ‘friend’ makes something different that the next one needs.
Note: Saying you lost your piece out loud would be like throwing an exception. I guess you can re-use this as an explanation for that.
> I'm sorry, I know you put a lot of effort into this but I find it rather convoluted.
Don't worry about it! I'm very happy to receive criticism.
I tried to do it in this way because the task was to explain it to a 5-year old. I haven't (yet?) met a person that young who could understand an abstract language like Haskell. It's just not happening.
My explanation is probably indeed way too convoluted, but I still believe monads can be intuitively explained to a 5-year old somehow.
IMO the metaphor is already getting too frayed at this point.
I'm not opposed to metaphors in general, but metaphors are lot like abstractions. If you don't get the right one then the details leak everywhere and you may as well have not used it to begin with.
objects are a very leaky abstraction that usually brings people to build taxonomies, mostly unrelated to the parent.
reuse of code, which is the selling point of OOP,also comes often short with a miriad of very specialized subclasses that have nothing in common anymore.
OOP is not horrible, but it requires a good amount of discipline to get it right, while FP has less concepts and you can (usually) silo the "and now for the tricky bits" (cit. Robert Virding) in a small core.
the rest is simple pure functions that have also the benefit of being stupid trivial to test.
Also, you don't have to be purely functional nowadays, and that helps a lot.
If only I learned FP sooner, my life as a programmer would have been so much better.
You're point about testability is a good one that I feel is often missed. Trying to test functions in OOP code is often impossible because they presuppose some state in the "object" and if it's not initialized properly, it doesn't work. Then they also often make side effects. So you end up building a huge test harness to build up some state, run one test, and tear it all back down again only to rebuild it from scratch for the next test. It's doable of course, but it's easy for a simple CRUD app to have a 15 minute test suite and for tests to become flaky because of some unexpected state change done in some unrelated test on the other side of the code base. That's a special form of hell
Monads are indeed way simpler. Just a few beautiful laws. The problem is that they take a loooooooooooong time to really internalize. Saying it takes "5 minutes" is empty in the same way that "learn C++ in 24 hours" claims are (or you're hella smart!).
There's a quote that floats around that's like "as soon as you understand monads, you lose the ability to explain monads." It takes a ton of leg work to make them click. They're simpler, but definitely not easy (in the Rich Hickey sense).
Ok, so you understood flatmap and therefore monads in 2 minutes. Now you need something that needs nested monads. That's where major complexity suddenly appears - your code is no longer simple and is full of artificial constructs like transformers just to do simple things. Moreover, anyone who reads your code must know all language constructs you are using or they are lost and can't fix any issue in your code.
I don't think that is a fair comparison. Monad is a typeclass so it would be like explaining some specific Java Interface (and all the context for Interfaces) to a 5 year old.
The Haskell (since you brought up Monads) equivalent to explaining Objects would be like explaining Records or Sum types, which would be very easy to explain and don't require talking about inheritance like Objects would.
Some functions return plain values. Other functions return fancy values with the plain values inside. There are different types of fanciness. One simple kind is called option. An option may have a plain value inside it or it may be nothing. Imagine looking up a phone number in the phone book. You get the name wrapped in an option or nothing because the person is not listed. Another fancy kind of thing is a Future. It runs some operation like looking up something in a database. When you call the function it just returns this fancy thing and at some point the database sends a response and that response is a plain value inside it. If you want the plain value you have to wait for the future.
Well quite often you have functions that return fancy things and you would like to chain them together; lookup some user and then lookup their account. Instead of waiting for the first one and getting the plain value out then passing that to the second one and waiting for that, you could write some code that chained the two functions together for you. That composing of functions that return fancy things is called join or flatmap. If you also have a function that can put any value into a fancy thing (this is called unit or pure) you now have what is called a monad.
A monad is a description of actions to do and their order, in place of the actual actions. This description can be passed around and eventually acted upon.
Objects (not Alan Kay’s original ones but say Java) are abstractions that model state and behaviour together. Objects can share common behaviour through interfaces
I heard following explanation of monads somewhere online: "monads allow us to write imperative code in the functional setting. e.i. chain computations".
But that explanation wouldn't make sense to a 5-year-old
> I can't seem to wrap my head around OOP. There's too many concepts in OOP.
Which ones? You're familiar with structs, right? OO is just structs with a little magic & sugar. Not even that much.
Objects, methods, properties, instances, classes:
Imagine if, when defining a struct type, you could put references to functions on it, such that any struct of that type would contain those same fields with references to the same functions you put in the type definition. Then, if you create a struct of that type, the compiler and/or runtime will helpfully and magically appends an extra argument to those function signatures, assigning it some conventional name ("this", perhaps) and, if you call such a function "on" a struct of that type, the compiler/runtime will quietly, in the background, pass a reference to the struct you called the function "on" in that last argument slot, so that within the function you can make use of the function's "parent" struct (as, perhaps, a variable named "this").
The struct type with slightly-magical function references is a class.
The fields on the struct containing references to functions with the magical "this" argument appended when invoked, are methods.
A struct of that struct-type is an object, or instance of the struct type, if you will.
Fields on the struct are properties or members or whatever you like to call them.
Static:
What if you could tell the compiler/runtime not to bother appending that "this" argument to some of those functions you attached to a struct type definition? Or to have a given field on a struct type definition always point to the same location for every single struct of that type, so that they all essentially share a single variable? That's what "static" means.
Inheritance:
What if you could tell the compiler/runtime that it should associate one or more other struct type definitions with the struct type you're currently writing, and that if it can't find a given field (including ones that are refs to functions, aka methods) on a struct of this type, it should check an associated struct of the other type(s) and only error if it can't find it there, either. With the result that a struct type so constructed effectively contains all the fields of the structs associated with it, unless a duplicate exists on that child struct type, in which case that takes precedence.
That's basically inheritance. It's all about setting up and manipulating those kinds of relationships & precedence for lookups. That's all.
Abstract, et c.:
Just ways to have the compiler enforce constraints and requirements on a struct type definition.
Final
I do solemnly swear this is a constant, not a variable.
Now, there are implementation details under the hood for all this, but that covers actual usage, terminology, and concepts pretty well. You don't need to dig into the details of e.g. vtables (one tool for efficiently settling those inheritance-leveraging field lookups) unless you're implementing OO itself.
Immutability is slow, so if you are somebody squeezing the maximum FLOPS out of a CPU, you might want to stay away (sorry, 1.3x slowdown in your best case is not acceptable, and "leave it out to the compiler" is not a valid argument).
It's like with project management - the iron triangle of speed, quality, price, pick two. With each programming paradigm you pick a different set of attributes that might or might not be the best for what you need and they enable you on one thing and constrain you on another thing.
Immutability can be fast or slow depending on the implementation. There are tons of copy-on-write implementations that blaze past mutable implementations. When the compiler knows that a particular variable/piece of data will never be changed, it opens up a ton of optimizations that can be used to make it fast. With mutable memory it's on the developer to optimize, rather than the compiler. Most of the time, the latter is better at it.
With infinite memory and "no cost memory allocation" you'll be likely correct, but in real world production loads you'll hit those limits quickly and then the immutability is likely getting in the way. Imagine just keeping a large (possibly compressed) bitmap (TBs in memory) to keep track of subscribers in your queue which is what many distributed queues use - how would you do it in immutable fashion? Copy the whole bitmap on each small change? There are simply many problems where immutability is the wrong answer if you want anything resembling performance.
Absolutely, that would be one of the cases where "most of the time" is not "this time." Although for most developers/applications, I would imagine keeping TBs of bitmap in memory isn't a common task. Fetching some rows from the DB and transforming them into some sort of HTML or JSON probably is
The problem is that developer has no idea if the implementation is going to be performant for their use case. The mutable compilers normally have less magic and thus are easier for the developer to understand and optimize. FP is really just a higher level language, that's why they don't teach algorithms classes using FP languages.
There is often some "redefinition of terms" going on with dedicated functional programming folks in order to assert their claims as you just demonstrated. There was some benchmark where mutable Java was shown to be around 30x faster than immutable OCaml and the discussion went to something like "no, no, that's a wrong comparison, compare it with immutable vs mutable OCaml, that is then just 2x slower! And the next compiler revision will remove it! 100% sure about it!" etc.
Why can't other modes of thinking coexist with your view? FP is OK, so is OOP, so is imperative, low-level machine code, "loop programs" (see Dennis Ritchie dissertation), logic programming etc. I like them all but hate it when somebody tries to push me one way telling me all other ways are wrong.
> Why can't other modes of thinking coexist with your view? FP is OK, so is OOP, so is imperative, low-level machine code, "loop programs" (see Dennis Ritchie dissertation), logic programming etc. I like them all but hate it when somebody tries to push me one way telling me all other ways are wrong.
Why do you think GP is saying or doing that? I think you've put a lot of words into their mouth that aren't there, and then got yourself worked up about it, enough to accuse them of ignorance and small-mindedness (at best).
I see absolutely nothing in their comment that even weighs in favor of FP, let alone saying other modes of thinking can't coexist. For all we know GP is a rabid OOP fan but sees the value in immutability (which FP certainly does not have a monopoly on).
OK, my apologies, that comes as a baggage from previous discussions with FP experts that routinely dismissed any opposing ideas and the OP's answer struck the same chord.
Sorry I don't mean to sound like an evangelist, I just meant that there are misconceptions out there about immutable data structures (functional programming aside: the two are not so intimately involved that they're equivalent).
I hope you can understand why benchmarking OCaml against Java isn't necessarily fair, since they have two completely different implementations (the latter being possibly the most mature set of JIT compilers and runtimes ever built). A more interesting comparison are immutable DAs in languages like C++, for example the immer library which has some very impressive performance characteristics for the problems it solves. And other applications like immutable ropes used in text editors which have extremely high performance compared to mutable variations. At the same time it's not a silver bullet, plenty of other immutable DAs have issues that are non-negligible and applications domains where in-place mutable DSs are required like hard-realtime.
Software engineering is about tradeoffs and not dogma, and that's what I was getting at. A take like "immutable is slow" is the counter dogma to "immutable is perfect."
I mean there's other options too. Go for example is mostly imperative(?) - it doesn't encourage heavy OOP or pure functional programming. Rust has more functional features but it's still neither purely functional nor heavily OOP. Julia isn't OO at all but it doesn't emphasize FP either.
Those are just three languages that are all (1) modern, (2) generally well received, (3) neither OO nor heavily functional and (4) still very different from each other.
> If OOP is causing complexity in your program it's either the wrong tool for the job or you are using it poorly.
No.
The whole point about OOP is that it increases complexity. It uses higher levels of abstraction to organize the program.
Sure you might end up with a program that is possibly easier to understand and maintain and maybe even shorter but always more complex than the strict procedural equivalent. Simply cause you increased the levels of abstractions one needs to go through to understand the program.
That is, of course, not OOP specific. Introducing any form of abstraction introduces extra complexity. If you use the abstraction right, the problem becomes easier to handle but you always pay a cost.
Though the have a point with
> If you have used OOP well and the program still turns out more complicated than if it was written in a procedural style then you may as well use procedural code.
The art of software engineering is finding a level of abstraction that is appropriate for the complexity of the problem that is to be solved. Don't over-engineer but also don't under-engineer your software.
One of the bigger advantages of OOP that I rarely see discussed is the natural namespacing that you get. In other paradigms you end up polluting the global namespace more easily, harming autocompletion and discoverability.
The problem with programming paradigms is that people tend to follow them obsessively, to the point where breaking from the paradigm seems automatically bad. For example, the four "pillars" of OOP are kind of arbitrary in my opinion, so I don't see an issue with breaking them, but sometimes writing a class is a better way to handle a collection of inter-related variables & functions. I don't think that makes me an OOP "adherent". It's not really possible to determine a priori if one theory will always be better than another when it comes to writing programs.
Agreed. My favorite example of this is domain-driven design. I love the core concepts - get everyone on the same language, make sure the code follows directly from business rules, etc. But you open the book and it's a flurry of absurd terminology that does nothing but obfuscate whatever the author is trying to talk about.
Then that terminology gets tossed around by hardcore adherents, and you end up more confused about software development than before you learned about DDD. It's a shame.
It may be because programmers are often also attracted to mathematics, and so they think everything in programming must be very carefully defined, and everything must follow from axioms. In reality, programming has a lot more in common with a practical skill like carpentry - you use the best tools & methods available depending on the product you're trying to make.
You use a bit of math in carpentry, but carpentry itself is not like mathematics. There's no deductive proof of the best way to make a chair out of oak.
Correct, more generally the problem is often absolutism, and presenting or applying ideas as absolute truths.
All of the ideas have merit, they have cases where they work wonderfully, and a lot of places where they do not. This issue extends beyond programming patterns and paradigms, not only into the rest of technology but life in general. You cannot unsee this, you will start to see "worked for me"s being presented as "this is the solution to all things" everywhere.
My first criticism to observation based conclusions anyone suggests is usually "that's subjective" and the following criticisms are usually exploring the parameters of that subjectivity, my partner is so tired of me saying this, because everything is subjective - I shouldn't need to say it, but we seem to live in a world where it's increasingly necessary to point out.
Oftentimes we read a comment of the kind "bad or good code can be written in any language or paradigm" in response to writings of the ills or virtues of said language or paradigm. Thus implying that we really shouldn't care of the choice of tools but rather concern ourselves with the individuals using them? This seems like an easy out. Too easy. If it is bad code that we need to worry about, no matter what the syntax or semantics, then how do we do that? Is bad code, like porn, something that we only recognize when we see it, impossible to clearly define? If that's the case, then we really need to figure out some better way to guide us. The mention of Kevlin Henney is particular here in that he has made presentations specifically identifying examples of bad code and how they are made better. If no language or paradigm can help steer us in the right direction--a proposition that I do not believe--then there best be some clear way to tell us how to avoid the pitfalls other than "I know it when I see it."
It's the same reason to learn alternatives to anything: to have a wider breadth of knowledge. Your thought process becomes more open and can better way alternatives because you actually have alternatives.
My compiler course in college was in SML and it was excellent for teaching more more than just compilers. It taught me a lot about recursion and the idea of composing a problem in terms of itself, which has been incredibly powerful in my career.
Being exposed to other great concepts like algebraic data types (sum types/Rust's enums) just helps expand my way of thinking when I write PHP code at work.
The idea of using a function as a fundamental building block for abstracting ideas has also been fantastic. Mostly in the form of using functions as arguments to alter behavior.
As with everything, there's a balance. I don't write pure FP, nor do I want to, but the ideas are huge. Same with OOP. OOP has some good ideas, but I don't lean on the words of the GOF as if they were GOD. Pulling in the best of all worlds has been really huge for my life as an engineer.
Dijkstra once explained how his process for writing code is to state\solve the problem in the most obvious and natural language possible. If a compiler\interpreter existed for that language, then he's done, he now has an executable solution to the problem. If not, then he recursively solves the problem of translating each construct of this non-executable-but-convenient language down into other constructs, if those constructs are executable, well and good, if not, he has to break them down further.
Learning new programming languages, paradigms, architectures, approaches and formalisms aids this process because it gives you more mental building blocks to use in your journey from non-executable specifications to executable implementations. Even if you never write haskell, learning it makes your brain evolve a haskell-like pseudocode in which to think and express problems, and this can come in handy when the problem is most naturally expressed in haskell. If a haskell implementation exist, great, if not, transform the haskell formulation gradually into whatever executable form available. This, for a certain class of problems - let us rather unimaginatively call them 'haskell problems' -, is better and more enlightening than attempting to directly solve them in executable format.
------
One common saying is "You Can Write Fortran In Any Language", another one is "Any Sufficiently Complicated C Program Contains An Ad-hoc Informal Implementation Of Common Lisp".
The fundamental truth that both of those aphorisms hint at is that programmers are human compilers, the programming language they write in is their target language, the "assembly", and the description of of the problem they are solving is the source language they are compiling. Here's the thing though : just like actual compilers, programmers don't have to compile the source language all in one go. Machine compilers often go through several detours and meander through different intermediate representation of the code being translated before outputting the final target. The equivalent of this for programmers is the Dijkstra process, describe your problems in a hierarchy of languages that ends, not begins, with the programming language you happen to use.
If the problem is best described as a Fortran program, write it (in your head) as a Fortran program then implement whatever necessary of Fortran in the actual available language to write the program. If the problem is best described as Common Lisp program, then think about it in your head as a Common Lisp program then implement the necessary parts of Common Lisp in C to write the program (The full quote is only saying this is bad insofar as it happens non-deliberately and haphazardly, if it's deliberate then it's just good design). Programming Languages are notations/pseudocode/ways of thought/mental models/semantic repositories of meaning, they can be useful even if there is not a single executable implementation of them in sight.
Obvious reason to study FP is: when, inevitably, someone appears who believes they are smarter than you due to their love of FP, you can more effectively defeat them.
From my personal (and rather limited) experience, there are two kinds of people who are into FP. First are those who will act like christian missionaries or competitive vegans and tell you all about it in the most obnoxious way possible every time they see you. And the rest are those who you wouldn't even know are into FP.
Obviously exaggerating for a comedic effect here, as there are a few people in the middle. But the median of my personal experiences is definitely very well described by just those 2 commonly present types above.
You joke, but your "to shut the obsessed ones down easier" reasoning for getting into FP was one of my primary reasons for doing the same (along with just actually liking FP paradigms and learning quite a bit of cool stuff from it).
Being self-taught, once we got internet back in the days I spent a lot of time on IRC for help with programming. As I grew more experienced I spent a lot of time on IRC helping others.
After a decade or so, I formulated a theory based on my myself and those I helped on IRC. It seemed many (most?) programmers starting out followed a similar trajectory where, as they became competent programmers and grew confident in their skills, would latch on to some way as the way.
Then, over time, as they got a lot more experience, they'd realize that often there are many different approaches with different merits and tradeoffs, and not get so dismissive to other approaches.
Could it be your "two kinds" are those that just became good at FP, and those that have been good at it for a while and grown in experience?
Anyway, not saying this is any profound insight, just something that struck me at some point during my journey.
edit: Not all would grow beyond that initial stage. Some plateaued shortly after. But for myself and many of those I followed over time seemed to fit this.
I learned a ton through IRC, too! And I'd give a strong +1 for this pattern. Personally, I think it applies generally to a lot of fields. Once someone has sufficient experience and wisdom, they inevitably come face-to-face with how little they truly know.
Ideally, that humility is paired with a general sense of wonder and curiosity. I think, once you realize how little you know, it gets so much easier to (re)embody the childlike curiosity and desire to learn more. I don't think it's a given, though -- and sometimes (at the risk of arrogantly declaring myself to be humble), I find it pretty hard not to be overwhelmed by the massive volumes of reality that I'll never get to learn about.
William Butler Yeats had a slightly different -- but far more beautifully phrased -- take on it in his poem, The Second Coming: [1]
The best lack all conviction, while the worst
Are full of passionate intensity.
In such a situation you can also chose not to engage. The FP nerd might perceive this as you giving up, but a strong, mature personality like yours allows you to know you are the true winner.
If you learn FP, in a real FP language, it will bring discipline into the way you write code. All code you write, once you "get it", functional or non-functional may become an order of magnitude better.
I'm curious - would you also say, "If you learn OOP, in a real OOP language, it will bring discipline not the way you write code." And if you wouldn't say that, why not?
And what language and learning tools would you recommend for this FP learning?
Because there is an essential complexity to programming in general that you cannot escape. Avoiding functional programming is like navigating a dark cave by bumping into obstacles when you have an assistive technology that can guide you.
I have a feeling the term "functional programming" will be a negative term by the end of the decade.
Not "functional aapproaches", just "functional programming".
In the same way that now "Object-oriented programming" is a negative term (but OO techniques in isolation where appropriate by context are totally fine).
> I have a feeling the term "functional programming" will be a negative term by the end of the decade.
And what do you base this feeling on?
Functional programming has been on a very slow and steady rise since... the 1950s. In fact the site that you are writing this on is written in a functional programming language.
FP has a lot going for it and it is gaining traction bit by bit in all kinds of domains where previously this was not the case.
I'm not sure if we should lump Erlang/Elixir in with functional programming (I think they should be but others may disagree because of the Prolog ancestry), but Clojure is definitely there as is Haskell, F# and so on.
Reliability in software is rapidly becoming a key item, as more and more real world processes are directly influenced by software accidents have the potential to have very bad consequences, and functional programming is very good at completely avoiding certain classes of bugs. Couple that with mature eco systems and success stories such as WhatsApp and I think we are getting closer to seeing FP become mainstream.
What really would move the needle is if software engineering were to be held to the same standard as regular engineering: liability. Sooner or later this industry will have to grow up and all the band-aids in the world won't help to achieve that if it isn't addressed at the foundation.
Most so-called software engineers fail to do something crucial in engineering: they have zero knowledge about the scientific state of the art about programming.
I agree that the absence of liability enables this.
That is interesting: yes, it is true. That's why you see these endless (poor) re-inventions of the wheel. If the state of the art would at least be commonly known and well communicated we could avoid some of that. Not all of it because another mantra is that re-inventing something (poorly) is a useful exercise. I agree with that on principle if you then end up using the blessed version in your actual work but more often than not it is the homebrew and rickety version that the whole house of cards will then end up depending on.
Experimenting around the state of the art or reimplementing the state of the art ourselves while being informed by it would be fine. I guess it would be a great exercise, as you said.
At my university the first programming class that every CS major had to take was functional programming. Many people would come in with no experience and FP would be their first introduction to the field. It was a very controversial course among students due to its perceived usefulness (or lack thereof), but as someone who already was familiar with imperative languages I really enjoyed it.
Because writing functional code forces you to define your program in mathematical terms and this improves your ability to reason about your program.
Imperative code is very convenient when you're describing processes that access a lot of shared global state at the lowest level, but it becomes difficult to keep track of everything when you have a lot, and math doesn't know how to handle that much complexity.
But math can help with simpler things, like unary functions that aren't allowed side effects. And if you opt-in to only using unary functions without side-effects, you get to use most of category theory for free.
Simplicity is the ultimate sophistication, and functional programming requires simplicity.
Functional programming is at last mainstream. The most popular JavaScript frameworks React and Vue has adopted functional rather than OOP approaches.
Before long we will see how functional programming fares when applied by average programmer under real-world constraints and the consequences for long term maintainability.
Like when OOP hit the mainstream, I predict we will see some disappointment set in. Due to the laws of the hype-cycle, this will lead to embarrassment and backlash, but in the end expectations will stabilize, and functional will be considered a tool in the toolbox rather than a panacea.
I find it beautiful, especially recursion. The fact that you can just let the computer take care of the problem in some strange and invisible separate dimension is pretty magical to me.
It seems to me that if you're going to use functional programming, then you do need to have a way to maintain state and manage threads if you're going to run genuinely useful software.
So a platform oriented toward functional programming, such as Erlang's BEAM virtual machine, which gives you the thread management and architecture principles to really maximize the power of functional programming, makes the most sense to me.
It's not used by many, but it's used by niche environments that have a lot of money, especially in Fintech. When you want your code to be extremely reliable while really fast, there's nothing like languages like OCaml or Haskell.
That lowly imperative programming: hard to prove your program is correct.
Functional programming: easy to prove your program is correct, hard to prove its' execution will fit in the known universe.
Functional programming IS math, so it's super satisfying to learn intellectually. It is also a spherical cow.
Modern languages assimilate ideas from all paradigms. Besides, FP isn't some theoretical obscure stuff. I'd except most programmers to know some rudiment of Scheme or ML.
Before multi-core, multi-threading, it didn't matter how or when you decided to update your local variables, they were YOUR local variables... unshared.
With threading, and multiple cores, we all just blindly moved right to the mutable/shared quadrant... the one in bright red... which effectively changes the laws of physics of your programs. Effectively every piece of code now runs in it's own time and space, and if you don't coordinate things correctly, it's like killing your own grandparents.
It took me a long time to understand why you'd want to refactor your code until everything was immutable[1], but now that I get the lesson, its something I won't forget.
Functional programming avoids mutable data, and thus, intentionally or not, works well in a world of shared data. Because pure functions have no side effects, they are timeless - they need no synchronization.
Spend a few nights watching everything Kevlin Henney has said in the last few years, and you'll have a much better handle on things.
[Edit] Incorporate wording suggestion from DonaldPShimoda