Why study functional programming? (2012)
acm.wustl.edu
acm.wustl.edu
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 DonaldPShimodaThis 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.
Right, isn't the fundamental trade-off between programmer efficieny and runtime efficiency?
Immutable structures are easier to reason about, but almost always run slower than their mutable counterparts.
The holy grail would be immutable structures that are equally performant, but I'm not sure that's even mathematically possible in general.
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.
Haskell didn't invent linear types. Eg Clean had them for ages to do IO. (They didn't use Monads in Clean.)
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.
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).
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.
Also no one claims there's one immutable data structure that's faster than all mutable data structures. That's a ridiculous strawman argument.
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.
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.
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.
Yes. Yes it is. The structures have different performance characteristics for different operations.
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.
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.
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.
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.
This is a description of what a data dependency is; I'm still not sure what your point is regarding mutable data structures specifically.
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 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.
I get that you can imagine it. I'm saying it wouldn't work in practice.
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.
And why are you saying that requiring more memory would make a program slower? I don't see the link.
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.
I'd be interested to see benchmarks.
> 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.
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.
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.
[0] https://dlang.org/spec/function.html#pure-functions
[1] https://learn.adacore.com/courses/intro-to-spark/chapters/01...
// 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?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.
[1]: https://hackage.haskell.org/package/primitive-0.7.4.0/docs/s... [2]: https://hackage.haskell.org/package/base-4.16.3.0/docs/Data-...
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?
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?
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.
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.
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.
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.
https://medium.com/clean-code-development/stratified-design-...
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
routes/user::save()
-> models/user::save(params)
-> models/db::run_query(qry, params)
you might do something like routes/user::save()
-> models/user::create_user_instance(params)
-> models/user::create_user_query(user{andrew, [dogs, programming])
-> models/db::run_query(qry, params)
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.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.
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.
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.
https://github.com/willtim/recursion-schemes/blob/master/sli...
But ideally in the real world you don't go crazy with either.
Objects are excellent for data but OOP is a mess for control flow.
Immutability is great but everything-is-recursion is not.
And so on.
When performance comes up all of this goes out of the window, maybe. But I'm talking about good defaults for correct, readable programs.
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.
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).
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
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.
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.
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.
They are not. If your data is in objects, you have already demoted data as a first class member of your language (ie. Java)
I can't seem to wrap my head around OOP. There's too many concepts in OOP. In FP it's just data and functions for transforming that data.
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.
And there are best practices for every single language and paradigm.
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).
I also find the appeal to a physical object to explain a functional concept is rather amusing.
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.
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.
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.
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.
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?
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 honestly think the best way to explain the Monad typeclass is by just showing the code. It really isn't that complex:
instance Monad (Either e) where
return = Right
Left err >>= _ = Left err
Right a >>= f = f a
And then show how you can use `>>=` to chain operations on `Either` values. Then you show a second instance, such as: instance Monad (State s) where
return a = State $ \s -> (s, a)
ma >>= f = State $ \s -> let (s', a) = runState ma s in runState (f a) s'
Then explain how you can use `>>=` here to do something seemingly completely different.Damnit, now I've accidentally written a monad tutorial :(
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.
metaphors are good for beginners, to grasp the concept, they don't need to be perfectly valid.
for example the popular OOP metaphor a car is a vehicle becomes useless pretty much immediately.
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.
it takes 5 minutes to understand it, the jargon is complex, the implementation is not.
case in point
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.
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).
Does this not apply to every language ever?
Be good at it?
More like a thousand hours.
there is nothing that is really simple, but before being simple, everything looks difficult.
p.s. I love Rich Hickey and especially his keynotes at Clojure conferences.
I've watched simple made easy at least 10 times.
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.
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
But that explanation wouldn't make sense to a 5-year-old
In OOP it's just data and methods for manipulating that data.
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.
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.
I don't need infinite memory to make a finite number of modifications to an optimized persistent data structure.
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).
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."
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.
> Objects are excellent for data but OOP is a mess for control flow.
http://nomad.uk.net/articles/developers-who-hate-on-oop-don'...
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.
Why oppose a real pattern and a imaginary one?
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.
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.
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.
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.
Then Dijkstra would have written a disparaging article about it, and then worked the solution in Algol 60.
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).
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.
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.
[1]: https://www.poetryfoundation.org/poems/43290/the-second-comi...Guru: To not argue with fools.
Wanderer: I disagree.
Guru: Yes, you are right.
- Jet Li
Whatever is the motivation, I'll say that the more people learn FP the better.
It won't hurt.
And what language and learning tools would you recommend for this FP learning?
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).
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.
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.
I agree that the absence of liability enables this.
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.
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.
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.
Functional programming IS math, so it's super satisfying to learn intellectually. It is also a spherical cow.