Ask HN: When is pure functional programming beneficial?
The way I think about Haskell programs is locked-in statements that tightly sit next to each other (i.e., function compositions) and nothing "leaks", as in no mutations are allowed (except in monads, like IO).
But I wonder if this is mostly a matter of taste. In small programs, the end result of a Haskell program is the same as a Python program. Is there a threshold after which Haskell's purely functional paradigm shines the most?
84 comments
[ 3.6 ms ] story [ 113 ms ] threadMixed initiative systems (say UI code where you call into the framework and the framework calls into you) can go either way. Sometimes you can formulate the part of your code in a pure functional way which tames the chaos, if you can’t or that formulation is unnatural the chaos tames you.
System w/ immutable data structures lose a factor of two or so in performance in some cases. FORTRAN programmers won’t accept them, distributed “big data” systems will spend a lot of time marshaling and unmarshaling data and won’t accept any slowdown in their parsing code. The same could be true for something like that SAT solver.
You can unit test without mock objects because you're following functional patterns.
How easy it is to draw fractals depends on your tools. If you have turtle graphics with affine transformations (rotation and scaling) it is so easy to write simple recursive programs to draw space filling curves and such.
It's used as an example of basic recursive programming largely because its an example that can be returned to with memoization. That there is a better way to do it that can be shown later is a reason for selecting early examples in a curriculum.
Similarly, it may be faster to use the repeated squares approach to get a large value fib, and I don't know any system that can get that from the recursive definition, either.
If you use the iterative / recursive form, runtime performance isn't great. If you use a lookup table, performance is great, but space might suffer (but then, how many values do you really need?). If you want to use something better, you've got to write it yourself. Maybe one day an optimizing compiler will figure it out, but it's more likely to figure out you never call it with N > 10, and precompute, and that's fine.
This goes back to a ton of the talk on early compilers and systems. They would push that you should lean heavily on the system to move from definitions to optimal solutions. (This later got changed heavily to leaning only on the compiler, but the same idea applies.)
I am a bit confused about this. The following code is pure and I can't think of an easier way to do it imperatively:
I was definitely leaning on some of the abstractions that you get in the likes of turtle geometry to show how this works. Many of the programs you can write using those primitives are very interesting, and far more unwieldy in any other paradigm.
At large, I view it as the difficulty of forcing Cartesian coordinates. For a large number of problems, things get far easier to state in polar coordinates. Such that your choice of abstraction and representation matters. A lot.
https://www.shadertoy.com/view/XdcGzH
I'm going to guess that the shader version is a bit larger than the naive turtle version. Not that it isn't doable, but is tough evidence to hold for the benefits of the expressive power of functional code. :D
It doesn’t look like “functional code” but it really is since the shader returns a pixel color for x and y. I bet there is some squarish kind of fractal that could be code golfed with but manipulation.
At any rate, thanks for the link! Will try another computer later to see if it can load.
Suppose you are writing a "CRUD" app that writes to a relational database, how do you apply functional programming to that? The whole point of an application like that is that it makes side effects.
In some cases you can break those problems down into functional pieces. Consider Python drivers for a product like
https://www.arangodb.com/
One major problem is that you want drivers that work synchronously and asynchronously, the structure of the average api call is something like
To make that async all you have to do is stick "async" before the def and put an "await" before the do_post. 95% of the API calls have a structure like the above. Most of the complexity lives in encode_parameter(), generate_rest_url() and decode_response() are all perfectly functional functions that are easy to test. Code gen the API stubs and you get sync and async switching almost for free. Contrast that to the option of mocking do_post."Pure" functional programming is an extreme which probably isn't suitable for many real-world programming tasks. Functional style is typically what I do, I use lists and maps and folds quite a bit but still end up updating a database.
In general there is the pattern of building systems that do execution control such as the methods used to create control structures in Lisp, the Executor in Java, etc. One way to use monads is to accumulate a list of operations that need to be done and actually does the operations when you "unpack" the monad at the end. Such a monad could do many of the things a compiler does since it has the operation list at the outset and doesn't just blast from one statement to the next. This blends into the techniques used in Fluent DSLs.
That way you don't need to Mock, but you can input a "connection" during testing that does what you expect.
If you say "well now I have to type extra parameters everywhere", this is usually solved with partial application. I.e. you create findings from your functions that have the Db Connection already baked inside.
https://fsharpforfunandprofit.com/posts/low-risk-ways-to-use...
https://fsharpforfunandprofit.com/posts/partial-application/
However what you might want to do (and this is based on IIRC!) is have a typeclass for your database interaction. Then the real DB type and the mock one can both implement that typeclass and you have basically dependency injection.
There are other ways like free monads but I never got my head around that.
You could also create a monad transformer which is a bit more straight forward. They let you compose monads, so you can chain little bits of side effect. Like “here is something that can talk to a database and write logs and that is all it will do” … but actually it is pure! It thinks it is doing those things but they can be mocked.
So, I would say the advantage of a pure functional programming language is that it enforces and encourages writing pure code. You can still write in the style to some degree in python, but you don’t get all of the idioms language support to make functional programming really nice.
I notice a difference in my code when I write in a pure language.
There are some cases where you really want in place mutation— graph algorithms are good example—but for most other things, I feel like functional programming wins because of how you can think about your code, and less about code actually can or cannot do.
Haskell is syntactic sugar on top of the Von Neumann architecture (likewise Python, Rust, COBOL, Smalltalk, etc.)
The Von Neumann architecture is inherently stateful. Anything with volatile memory is.
Functional programming is an idiom, not a technology. To the degree functional programming is a technology, it is one for analog computers.
Good luck. My time has been wasted trying to be clever.
It does, IMO, give you less foot guns related to state though. While I still actually like other paradigms, functional programming really limits how clever you can be with state. You can only pass things around immutability via parameters (At least with Haskell specifically) instead of making a confusing taxonomy of objects with unique APIs, for example.
I totally agree (and have seen both scenarios throughout my career).
That said, all else being equal[0], refactoring the purely functional code is, usually, easier, more reliable and less scary.
[0] - among other things, this assumes the person doing the refactoring knows, in this example, both Python and Haskell equally well.
https://kristiandupont.medium.com/mutable-state-is-to-softwa...
I find personally that there are some areas where FP shines -- anything that is or could be a CLI that takes some input and returns some other output is well suited. A GUI program is the manipulation of state, and it is poorly suited, though many sub parts might not be.
If you're manipulating state that doesn't drive any outputs, then your program doesn't do anything useful. I think they're not all that different.
And the same for time. Some programs require proving that you meet the timing constraints. Recursion does not make that easier. (Neither does a language that can allocate behind your back, nor one that can run a garbage collector.)
So the rules for such environments usually say: No recursion, and no allocations after initialization. They say this because those rules make it easier to prove correctness.
There are techniques of factoring out recursion (e.g. "recursion schemes") that can help eliminate these problems, but some things are just easier to reason about imperatively.
- You can safely make more assumptions about how your program works - You can rely on your type checker like a to-do list when extending your program
These two benefits require more discipline, but they make extension and support brain-dead easy.
For the record, you don't need Haskell to enjoy these benefits, you can get them in Python or TypeScript too, as long as you're disciplined in how you design your system.
Are you _literally_ writing a simple script to do a single task which you won't save into version control? Pick whatever style will be fastest to write + run. But if you'll need to re-read it in 6 months to figure out how to re-use a portion in a different context, or the next engineer will need to migrate something out from under it in a year, or whatever else, then picking a style based on readability, testability, and analyzability can be worth it -- provided the rest of your team is on the same page.
As an active user and package maintainer I can't count the number of times I've decided to rebuild an old project that I haven't touched in years and it still working, while in the same session working on another project using up to date libraries.
There's also straight.el for Emacs[1] which has finally made Emacs config maintenance far better for me.
[0] https://nixos.org/
[1] https://github.com/radian-software/straight.el
In F#, an experience report came out where 350k lines of C# were rewritten in 30K of F# code. They also went from 3k null checks to 15 lines of null checks (Plus much more). Zero bugs were reported in the newly deployed system.[0]
Now with that said, there are exceptions where purely functional programming languages shines less:
- Places where the ecosystem is not quite as mature. If you're building a server and have to interact with Cloud services in Haskell, you'll have a bad time.
- Any kind of system where you need to do manual memory management, so systems programming, is badly suited for purely functional programming.
[0] https://www.youtube.com/watch?v=MGLxyyTF3OM&t=863s
But what is the set of problems you are usually solving? Any problem set that involves a lot of mutable set is not well-suited for functional programming, in my opinion. And maybe problems where efficiency is important, although I don't know enough to have an opinion on that. Some areas that come to mind are:
- systems programming (which you said)
- GUI programming
- games
- large simulations? Maybe if you can reuse the before and after buffers FP would work okay there.
GUI's have a surprising amount of state, which is why immediate-mode GUIs haven't really taken off. (Yes, there's Dear ImGUI, but it actually uses the name of the control to store retained-mode state under the hood) It sounds really good when you think about buttons and sliders, but then you come to text editing or list-box selection...
[0] https://elm-lang.org/ [1] https://en.wikipedia.org/wiki/Functional_reactive_programmin...
Functions handle state perfectly with scopes and closures.
Why does a server that interacts with cloud services not fit well with functional programming?
I've written plenty of non-FP code that interacts with less mature systems, and the problems I've always run into have been related to changing APIs and behaviors of the remote systems, not anything inherent to the language or paradigm I'm using.
I was wondering what it is about FP that makes it less suitable for unstable environments.
Not discounting the rest of your statements (I love FP for all the reasons), but I really despise - and discount - statements like the one above. I feel they are disingenuous:
"This large-ish program that grew over time with evolving needs and uses was rewritten at a later date once the requirements and use-cases were fully understood and - amazingly - we were able to greatly simplify the code!"
It probably could have been rewritten in BASIC and still resulted in fewer LOC. That's not to discount the null checks, "zero bugs", or other factors. My favorite part of programming in Haskell is that 99% of the time if it compiles it just works, and I can refactor code + add features without concern.
This has benefits when working in large, long-lived systems where engineers can't keep everything in their head, and people come and go over the years. It is easier to reason about, to refactor, and to test, when you know that the "blast radius" of your function is just the return value.
http://sarabander.github.io/sicp/html/3_002e1.xhtml
Functional core, imperative shell is the compromise where you collect state in shallow parts of the code, where they are easier to spot and thus easier to reason (correctly) about.
For example, I've worked on a lot of Scala code which represented optional values using `Option[T]` (~= Haskell's `Maybe t`); represented possible failures using `Try[T]` (~= `Either Exception t`); and I even used the Cats library to make code polymorphic (i.e. for all `M: Monad`, or `M: MonadThrow`, or indeed `Applicative`, etc.). The older ScalaZ library is a popular alternative to Cats which does similar things. Concurrency is currently quite diverse in Scala: we mostly used `Future[T]`, but there are a bunch of alternatives out there like `Task`, etc. Likewise there are other Scala libraries/frameworks which model side-effects differently, e.g. Zio uses algebraic effects.
However, since Scala is not pure by default, we can't count on any of these fancy types to actually prevent the problems they're meant to address. For example, we can only use `Option` alongside `null`: the latter can't be avoided, since Scala isn't pure; in fact `Option` should technically introduce even more null checks (since `x: Int` might be `null`; but `y: Option[Int]` might be `null` or `Just(null)`!). Likewise, `Try` can only be introduced alongside the possibility of any exception being thrown at any time. And so on.
In contrast, we might have a project e.g. in Haskell, where `IO` appears in all of our types. Such code might use all sorts of confusing effects (e.g. early returns, mutable variables, concurrency, etc.), and may blow up in all sorts of ways, just like Scala, Python, etc. Yet at least they're labelled as such, and we're able to write other values and functions which actually do what they claim (modulo totality, unless we're using Idris, Agda, etc.).
What we call functional programming today is often reducing the problem down to the easy parts. Which is great and should not be discounted. But don't think of it as any more valid of a solution than anything else. And pure numerical models of the problem can often be just as fruitful in solution speed, while being utterly foreign to most programmers.
OO gets lambasted as you try and model each individual thing in a problem. In my mind rightfully so. That said, it is very common for us to model things in a way that is very "object" based. Consider the classic model of an elevator system and how that is coded by most people. You will have a set of elevator objects with states that they currently represent.
But, you could also do this fully numerically with a polynomial representing things. Expand your toolset to use generating functions, and you can even start building equations that can count the number of solutions at any given state. Still just a symbolic model, but very very different from the OO or even FP style of model that most programmers will write.
Better for the problem you are solving? Probably not, during the exploration side of things. But get the problem into a formulation that you can translate into a SAT style, and you can feed it to SAT solvers that are far more capable than programs most any individual can write. Translate the solution back to your representation for display to the users. (Or just general use in your program, as you move to the next thing.)
Related concepts that are relevant at a high level is the comparison between declarative code and imperative. Code that is functional tends to be declarative and in a way "is what it seems to be." There is also a transitive property that allows the makeup of a thing to also be its own runnable representation which makes portability or idempotent things easier to achieve.
I dare say that original "OO" in terms of "message passing" is functional in nature, but object-oriented somehow became something it is not.
Erlang is probably the best representation of a functional language ideal in my own personal opinion when weighing in the ecosystem and capabilities of the language and tooling.
There's a point of view that if you haven't done it correctly, you haven't done it at all. There's another point of view that you just fix bugs as they found.
In other words, is your program a calculator or a robot?
https://mostly-adequate.gitbook.io/mostly-adequate-guide/
See discussion: https://news.ycombinator.com/item?id=22654135
This property is true to varying degrees outside of pure FP too. For example in Rust I find that expression-orientation makes programs fairly easy to compose, modulo thinking about ownership.