Functional programming is finally going mainstream
github.com
github.com
I spent the first 10s wondering who this readme user was and how could they have such a different main page.
Then there's the orgs at the same level as users...
If you look at my code in "multi-paradigm" languages like typescript, there's not a heck of a lot of loops, mutations or side effects. I like expressions, and immutability, and higher order functions. They make things shorter and means there's less state for me to keep track of - or screw up.
But at the same time, I like objects. Almost all of mine are immutable. I went through a phase of using plain old data + a collection of functions that transform them, but found a good object where all of that was self contained simplified things a lot.
I also have to say that sometimes FP programmers don't seem to understand OOP a lot. I'm of the opinion that working on an old school opinion that a legacy Java codebase doesn't really make you an expert on OO.
So while I'm glad we're thinking about purity, and avoiding side effects, I'm kind of sad that there's an object backlash. They're fantastic contstructs.
Yeah I've gone down this thought experiment. Conclusion I came to is that while you could view it that way, there's something powerful about Just Having One Thing, not structs + functions. It's almost a mindset shift as well, like you're requesting the object to do something and don't care how.
Are there languages which let you use dot-syntax for "methods" which just wrap normal functions?
Yeah, Dlang has that.
https://tour.dlang.org/tour/en/gems/uniform-function-call-sy...
I definitely see what you mean, a lot of FP encourages very general abstractions rather than something domain specific. That being said, I think Elixir has a pretty good handle on using functions as sort of black-box interfaces you can change later. Not sure if that's a language thing or a culture thing though.
> Yeah, Dlang has that.
Ooo, that's really nice. Super simple but a definite improvement to readability. Reminds me of |> in Elixir (or ~> in Clojure), particularly the chaining part. It clicks differently though, and definitely makes things readable for many programmers. Now that I'm writing this out, doesn't Rust have something like this? Like functions strapped right to a struct?
Glad to hear D has it.
as OP said it would make functional languages more ergonomic (especially for OO folks) and most importantly enable code completion!
it's still a bit unergonomic to type but a very useful beginning!
Reminds me of hurting my fingers in c where you had to type '->' instead of '.' !
a.f(b) == f(a, b)
newData = f(oldData)
VS newObject = oldObject.f()This is just a side effect of functional programming but such behavior can be embedded into and made a fundamental part of your foundational data structures in an OO program.
And so on and on. All kinds of objects need updates for their states. You need to direct all this stuff, without forgetting things. Otherwise the state of your objects will become inconsistent and at some point it will show as some kind of bug. At that point you might find yourself debugging the whole thing, to find out when the inconsistency starts or what causes it.
No need for multiple threads or any of the kind. Complications from splitting the state up and parking it in various objects and having to tell the computer how to update these objects. Instead of looking at 1 function at a time, which only works on its inputs and gives an output, you have to remember to call all the appropriate methods across some reasonable part of your entity landscape.
newObject = oldObject.f().g().h().i().j().k()
VS newData = k(j(i(h(g(f(oldData)))))) newData = oldData |> f |> g |> h |> i |> j |> knewData = oldData.f().g().h().i().j().k()
wont work by default unless you also happen to return the object after every method (builder style) which looks weird and is not standard
In the function and data scenario, all that needs to be made sure is, that your functions have the return type, which the next function expects, otherwise it is a type error, regardless of whether the language complains or not. A function call chain implicitly assumes the types to be correct, otherwise you will likely run into an error.
The difference is, that in the functions and data scenario, you already have functions, which take data of some type and return data of some type. You do not change them in any way, while on the other hand with objects and methods you need to change the methods, so that they do return some fitting objects.
Immutable objects are a pretty old idea, smalltalk had select (map) methods that returned a whole new collection back in the 80s.
String next = original.substring(2);1. Write a bunch of functional code.
2. Marvel at how clean and neat things are.
3. Get customer reports of performance problems.
4. Spend a lot of time ripping out FP components and replacing them with imperative code.
An engineer uses what is available to produce an economically balanced solution. Done well, that may involve a few OO-ish bits, some functional bits, data-oriented bits, reactive bits, plus hybrid combinations of those and others. The problem dictates the form of the solution.
Every big-enough problem decomposes into a collection of very different subproblems, each of which so decomposes further. The right approach for the top level rarely matches that for most of its parts, nor each for the others, and likewise down the line.
Unless you want to use a different language for each level and subproblem, you will want support for all "orientations" in your language. Thus, a "functional" or "OO" language is an absurdity; likewise, a "functional", "OO", or what-have-you program. They make as much sense as a saw-oriented carpenter's toolbox or table, or a screwdriver-oriented machinist's toolkit or engine.
Programs should be like water, fluidly matching the shape of the problem solved. If that is 20% functional, 10% O-O, and the rest imperative, so be it. Specialization is for insects.
Everything is Turing complete: you can code a solution in any restricted vocabulary. But that means succeeding proves nothing and benefits nobody, but wasted your time and others'.
Use a language that does it all, and use it all. Not necessarily all on every program; but all styles get consideration for each part.
https://en.m.wikipedia.org/wiki/Total_functional_programming
Quick, does this program terminate:
int main() {}
? Can't prove it?Add to it, and show that it still terminates, uses finite memory, introduces no data races or undefined behavior, and maybe computes a step toward something useful.
Repeat until done.
For example, consider GOTOs. If 99% of your functions don't use any GOTOs but they other 1% does, you can't be sure how the flow of logic lead to any one function. Immutability works in a similar way. I have a far easier time debugging in concurrent code I'm unfamiliar with if it's written in Elixir than if it's written in JS/TS, even if the project in question uses Immutable.js heavily.
How much of that comes from belief rather than reality, and potentially some room to grow on your end? I can debug well written code in any language I jump into, but I've found poorly written and hard to debug morasses can be written in any language.
Poor naming, branch/loop structuring, scoping, data modeling/normalization, module structuring, logging, and dependency selection/integration have all contributed to debugging hassles far more in my production experience than any particular language.
You know… suggesting that someone that they’re delusional and uninformed isn’t a very charitable way to discuss anything online.
The belief came from personal experience from 2016-2018 while I was much more experienced with JS/TS and relatively new to Elxir. It definitely influenced me to keep exploring and learning things outside the JS ecosystem I’d previously focused on. I’ve grown as a dev since and so has my appreciation of guaranteed immutability.
Also working with other languages, particularly Rust, in the past few years has only further underlined the point. It makes a strict delineation between variables, references and arguments that can be mutated and those that can’t. The language went with 100% guarantees instead of 95% guarantees for a good reason.
> I can debug well written code in any language I jump into
The phrase “well-written” may be doing a lot of lifting here. Different languages and ecosystems make different practices easy and tend to nudge users in different directions. People who don’t like learning often enjoy the idea that any Turing complete language is equivalent from the programmer’s perspective but it’s just not true.
I think there's no reason to exaggerate what I said just because I don't love your pet principle as much as you do. Here's what I explicitly describe as being part of well-written (or poorly written) code, which has very little to do with FP vs OOP vs procedural:
"Poor naming, branch/loop structuring, scoping, data modeling/normalization, module structuring, logging, and dependency selection/integration"
>The phrase “well-written” may be doing a lot of lifting here
I think if you read the entire comment, you'd see it doing a lot less lifting because I explicitly detail what "well-written" is. And I think it is cross-language.
> The right approach for the top level rarely matches that for most of its parts, nor each for the others, and likewise down the line. Unless you want to use a different language for each level and subproblem, you will want support for all "orientations" in your language.
It is grossly impractical to use a different language for each fragmentary part of a problem. People do not, as a rule, do that. Trivial exceptions include Python and niche-language programs calling out to C-ABI libraries, programs sending SQL to databases, and web servers sending Javascript to browsers. But those are not about "orientation".
You might use two or more different general-purpose languages for different, independent problems, but that does not contradict my remark about the folly of languages that attempt "purity".
Everything else is syntactic sugar.
A language meant for use by professionals should get powerful features, without regard to any spurious notions of "purity".
It seems to me all useful programs are terminating (or, we truncate some non-terminating procedure by some convergence criteria). The question is only whether we have the logical tools to prove termination at compile time.
You seem to confuse algorithms, which by definition must terminate, with programs, which do whatever the hell you like (and sometimes things you don't).
> 4. Spend a lot of time ripping out FP components i'm less familiar with improving the performance of and replacing them with imperative code I have years of experience improving the performance of.
The cause of mist if these kinds of things will be dominated by familiarity for a long time.
Let’s be honest, most typical software written by the industry is not the like which has a single hot loop. They do a bunch of different things, usually with small number of entries. I don’t see how FP would be a hindrance to this typical workload.
Or use the functional code as a form of tests?
The most common issue is hidden O(n) situations, which don't show up except in production. So what we end up with is a mix of various paradigms worthy of Dr Frankenstein, and a debugging nightmare.
I've been in engineering long enough to see most trends repeat at least twice (Javascript UIs are ALMOST back to the point of the 90s for example). After awhile you learn to distrust anyone who evangelizses a "new" way that was actually invented in the 60s (and is likely being introduced as a panacea instead of with the original caveats).
Now that I'm a greybeard myself, I finally understand why greybeards were always so grumpy.
How are these faster in imperative code?
Imperative non-OO codebases become swamps of maybe (hopefully?) layered code, where each layer contains its own sinkholes and completely bizarre structure (or lack thereof). It's basically a bunch of mini-codebases that meet in an ad-hoc manner, making it very difficult to understand what the hell is going on.
OO codebases encapsulate a LOT better, but working around its straitjacket design becomes harder and harder as you go through rituals like dependency injection, interfaces, adapters, facades, class clusters, and byzantine inheritance hierarchies. Now, rather than a swamp of despair, you have pebbles of functionality. And while these were great when your codebase was 50k LOC, it's now a gravel pile where you can't figure out where anything starts or ends.
FP codebases like to push the details out of the way, making it easier to see at a high level what's going on. But as the codebase grows, you now have multiple iceberg problems, where the devils in the details begin to derail your code in production, and suddenly you find yourself needing to delve down rabbit hole after rabbit hole, chasing issues that weave and bob in utterly chaotic ways due to the lazy evaluation. Reasoning about what it will actually do in what time becomes more and more difficult (putting it in vulgar terms: You're basically exchanging topical complexity for temporal complexity).
TANSTAAFL
OTOH isn't that just - you need to know what the function you are calling is doing and how, and read the doc. Whereas in imperative you would implement it yourself, with the pitfalls of that.
If you are coding in Python and you are using a lot of numpy you need to know how to feed data in, how to get data out, etc. so maybe FP programming should treat the STD-lib more like an external library because the building blocks are a bit more high-level?
It also very much discourages mutation and favors pattern matching and pure functions, so I would say it is quite FP in nature.
And yeah, sometimes stepping down into mutability and such is necessary for performance.
When you are programming in FP, you are describing the program from another POV - taking the IO monad as an example, it just constructs a “list” of steps that should be taken at a given point vs just listing the steps in the program itself.
In pure FP languages, immutability is enforced due to you describing a given state - it simply doesn’t make sense to change something there.
There are two truths that ensure the dominance of imperative software:
1. At some level of software complexity, programmers MUST start to organize data into composite objects. They have to do this because working outside of well-defined problem domains is a recipe for buggy software and spaghetti code.
2. Copying memory around to enable to facilitate these immutable structures is SLOW.
I don't understand this point. All functional programming languages have algebraic data types and records, which are composite types. If this is not a "composite object", you'll have to clarify what you mean.
> Copying memory around to enable to facilitate these immutable structures is SLOW.
Slower than what and in what context? Append-only logs in relational databases are immutable data structures, and they enable multiversion concurrency control, which is faster and more scalable than locking when there's lots of contention.
Unqualified claims like "immutable data structures are slow" is just wrong.
I think they meant data structures, not data types. Things like lists of records that each contain further records.
type 'a list = Nil | Cons of 'aAll data structures have types. EVEN lists of records that each contain further records. A type or a data structure is an orthogonal concept to FP, because types exist in imperative programming and data structures also exist in FP.
Some commercial RDBMSs do use their logs for MVCC, though.
Personally I'm of the opinion that purity is impossible so if a paradigm needs purity it's going to be an uphill battle.
So it seems we can have out cake and eat it too!
[0] - https://www.microsoft.com/en-us/research/uploads/prod/2021/1...
Their papers and online docs are very good, I highly recommend them for more information!
Both working on making functional programming practical and very fast. Koka is more of a research language, also developing Algebraic Effects and making them practical, which is very cool.
You really think functional programmers don't build composite types?
2. Immutable structures aren't immutable at the runtime level, https://en.wikipedia.org/wiki/Persistent_data_structure
In practice, functional programming is a way of organizing imperative programs. Kind of like how OO is a way of organizing imperative programs.
Almost all applications written in Haskell use the IO monad quite a bit, for example. Large Haskell projects generally use a lot of C libraries, and sometimes even directly include C code for performance sensitive bits.
I don't agree at all!
Haskell is extremely pleasant for imperative programming. There's a learning curve for sure, but you get a lot in return. (STM is one small example). I would much rather write imperative Haskell than Python.
It does gets messy when you start writing really low level code (direct pointer manipulation, etc). And it sucks for small scripts (too much project boilerplate, small standard library). But those are the only real pain-points.
You can very successfully keep nearly all IO at the edges and do the heavy lifting in pure functions if you make it a goal.
On imperative code, execution is easy to follow. Instructions go one after the others, loops loop, function calls and local variables go on the stack, globals are always visible. Optimization aside, what you code is what the computer runs. You often have a debugger to step through your code, or some logging facilities, even if it is just a printf.
On functional programs, the idea is usually that you manipulate stuff, but you don't know what you manipulate. You apply a functions to functions making other functions until you end up with the function that turns the input of your program into the output of your program. Then you let the compiler do its magic, it is actually really good at that and not slow. But then you end up with something that doesn't look at all like what your wrote (computers are imperative), and if, in the end, the result is wrong, good luck finding where. The debugger will give you the state of the program in a program that has no state, and logging is a side effect in a program without side effects.
Functional programming is not bad, I love pure functions, but in the end, I think it is just a tool. A solution to a set of problems, but not enough to stand by its own for most projects. And it is evident from most modern programming languages. Most of them have some functional paradigms, and could likely be used to write pure functional code, but is better suited as something to use just when you need it.
Debugging in a strict FP language is pretty much exactly like debugging in an imperative language. Debugging in a lazy FP language requires somewhat different approaches, but still does not have the problems you are implying here.
> In the end, the result is wrong, good luck finding where.
This is not true. If this was true, nobody would be able to build significant things in FP languages.
> Optimization aside, what you code is what the computer runs.
> But then you end up with something that doesn't look at all like what your wrote.
No, you can trace code down through the compiler transformations and generally understand what is going on at each level of abstraction.
Compilers for imperative languages are just as "magical".
If you want an FP debugger the interface has to be able to step by step evaluate an expression. Since FP is just a giant expression, how is it evaluated step by step? Break points will be different and stepping through it will be different as well.
Take for example:
(1 + 2) * 3 + 2
Stepping through it with a debugger would look like this (imagine the expression transforming at each step): (1 + 2) * 3 + 2
(3) * 3 + 2
9 + 2
11
A break point would look like this (square brackets indicate a GUI marker) (1 + 2) + [(4 * 6)] + 1
A run until breakpoint will evaluate to this: (3) + [(4 * 6)] + 1
That's it. Debugging FP is about stepping through expressions. Debugging Imperative programming is about stepping through statements. And the unique thing about "stepping" through an expression is that the expression is getting reduced (aka changing) at every step.You need a complete UI overhaul for FP to work. This tends to be harder in terms of building a UI, because text by nature is a one to one mapping to procedural instructions as text Lines are equivalent to instructions. However for FP everything can basically be on one line, so text doesn't really fit and thus you actually need a more dynamic UI for a proper FP debugger.
I think you're doing something wrong.
Because your experience should be exactly the opposite. Imperative programming by distributing its state can only really be understood in context. You need to understand that instruction one after another, just like you said. You need to understand globals and the overall state.
In functional programming you understand code without context. Everything is local. There's nothing to trace.
> Then you let the compiler do its magic, it is actually really good at that and not slow. But then you end up with something that doesn't look at all like what your wrote (computers are imperative), and if, in the end, the result is wrong, good luck finding where
What does the compiler have to do with anything here?
You can print out whatever state you want, your debugger can provide you with whatever local state you want. There's nothing obscure or mysterious about functional code compared to imperative code. It's just a matter of not distributing your state widely.
The problem is people don't trust it's really local even though it is.
Pure functions are easy to debug. Run it, check the result against expectations. Step in if necessary.
I'm tired of seeing this analogy everywhere. Everyone knows it, there's nothing new here. When we talk about things like FP or OOP, the real opinions live at the extremes. Where one tool is definitively better than another tool or where some tools are complete garbage.
Saying that everything is a tool for different things or it's all apples and oranges says nothing about anything.
"Optics". No one writing serious FP code copies memory around willy-nilly.
The parent comment claimed that immutable data structures are slow, because you would have to copy them around, an O(N) operation.
I wanted to know if they considered that operations on immutable structures can be done without copying, and have the same algorithmic complexity as the mutable data structures, if they used persistent data structures.
a = [1, 2, 3]
b = a.append(4) // append: list->int->list
// Rest of the code never refers to a, but refers to b
In this code, you can prove that it's safe to move a's memory into b and thus append operation can be done without copying a. I'm not implying this is easy to implement, but it's clear that this is not a fundamental issue in functional programming, rather a deficiency in its current implementations.Maybe a "sufficiently advanced compiler" in the future will solve this, but it's not at all clear such a thing can be created in a way that's practical right now.
No idea what you mean.
Mutable state is not a problem in Haskell. You can have high performance libraries that provide mutable APIs that are safe to use. You can have code that looks more imperative if you want. If you really want to go nuts you can use linear types and solve such problems in a completely general way.
> Maybe a "sufficiently advanced compiler" in the future will solve this, but it's not at all clear such a thing can be created in a way that's practical right now.
There's no need for any advanced compiler. We have all of this today.
This has been possible in Haskell for a long, long time. A ton of languages do something very similar with strings (Python, JavaScript, etc.) Strings are immutable in both Python and JavaScript, but rather than using a "StringBuilder" class like in C# or Java, the compiler and runtime will optimize certain pure functional operations (like string concatenation) into a mutable operation on a buffer. If you are not aware of this optimization, you might find yourself benchmarking some simple string operations and be completely shocked at how fast they are. (There are a couple questions on Stack Overflow where people are confused about why their Python code is faster than their C++ code when benchmarked.)
I think the term "sufficiently advanced compiler" does a bit of a disservice. People have used that phrase a lot, not only when talking about high-level languages but also when talking about things like ISAs. It turns out that people are bad at predicting what kinds of optimizations compilers will be able to do in the future.
The other reason I don't like the phrase "sufficiently advanced compiler" is because these optimizations are often tied to something other than the compiler. They may come from advances in library implementations, advances in programming languages, or advances in language runtimes.
Take a look at how Haskell does it. You have array implementations with a pure interface but impure implementation. You have a system called "stream fusion" which lets the compiler take array operations that logically produce arrays as values, and transform these into simple operations that loop over the array. Stream fusion is a feature that doesn't really require a "sufficiently advanced compiler", but rather some relatively simple compiler features (all things considered, "simple" is relative) and the ability to annotate library functions with transformations that improve the generated code at the library functions' call sites.
I think of stream fusion as more of a "let's write good libraries" feature, and less of a "compiler magic" feature.
Then take a look at how Python does it. When you concatenate strings, the library code can check that the left-hand argument has no other references, and modify it in-place. All the compiler has to do is make sure that variables are killed as soon as possible, which is a very ordinary thing for compilers to do. It's not magic, there are ways to break this optimization from happening, but it does work.
And if you think about it, most things using the aws sdk are network i/o bound anyway, which is probably why they burn ram and generate garbage like it's free.
So immutable data structures (especially persistent ones) aren't a problem at all in many domains.
This is categorically wrong. You actually don't know what you're talking about.
In an functional programming language, because everything is immutable, the compiler just MOVES everything around. There is ZERO copying in a functional programming language. In fact there's no explicit command for it either. There is zero reason for you to copy a variable that is immutable so there's no copy() function.
What you're referring to is more of what happens when someone is writing functional code in a language that's not functional.
And even then, there is the question of whethwr it is a deep or shallow copy on receiving or write.
Right. and FP provides NO way to composite objects
> 2. Copying memory around to enable to facilitate these immutable structures is SLOW.
Who says you have to (multiple others have pointed to resources)
the entire point of some of the immutable structures is hidden in their implementations, such that they are fast exactly because they do NOT copy unnecessarily. the side effect gets called "functional", so there's a strange reversal of importance of (kinda misleading) buzzwords at play here.
The link(s) the others shared is a great article.
Color me skeptical.
timeslotRepo.findAll().stream().filter(timeslot->timeslot.getEndDate()==null).first().ifPresentOrElse(()->{}, ()->{new Timeslot(DateTime.now())}) timeslotRepo.findAll()
.stream()
.filter(timeslot->timeslot.getEndDate()==null)
.first()
.ifPresentOrElse(()->{}, ()->{new Timeslot(DateTime.now())})
https://en.wikipedia.org/wiki/Fluent_interface#JavaI was having a bit of fun at the expense of FP zealots.
if timeslots.stream().matchNone(Timeslot::hasntEnded) {
new Timeslot()
}
Would be an improvement I guess. The way I found this code it was also wrapped in a @Transaction so it creates a new object in the database with spring magic. There was a lot of other things wrong with the code, that was not my point though. Lots of people just think FP better and of course in theory they are right but most people don't think for themselfs.People are quick to act insulted whenever I give some critical remarks about FP.
When would you use a forEach function instead of a for each statement, I suggest maybe only use the function if the callback is actually a higher order function you get from somewhere else as I don't see the point of wrapping your logic in a closure.
Same thing with the use of observables/reactivity in UI programming, again it is illegal to question this orthodoxy. But then you take a look at how graphics programmers use immediate mode programming and fancy compute shaders to draw the entire frame every frame it makes me smile at how much simpler everything becomes.
Javascript was an ugly thing on the browser, I do hope that webassembly helps with that, but I highly suspect that webassembly will be for evil programming that is even harder to decipher in compiled-bytecode format.
Alas, the article is riddled with many of the same problems that FP evangelism is usually saddled with, but this time with slightly more colorful fonts.
1) do not say or show the symbol for "lambda".
2) immutable: such a harsh, harsh word.
3) Yup, there's the word "pure". Heil, comrade, happy sharia to you.
4) AHHHH, "higher order". Expanding the consciousness. Take a puff, man.
Well, at least he didn't say anything about monads, functors, or category theory. And there's no implementation of the fibonacci sequence.