Hacker News new | ask | show | jobs
by jerf 1544 days ago
So, as the trend of forcing functional programming into imperative languages continues on its inexorable path, I just can't help but bring the old joke to mind more and more often:

    "Doctor, it hurts when I do this."

    "Well, then, don't do that."
You mean, jamming a foreign paradigm into a language, where the advantages of the foreign paradigm are reduced and the disadvantages greatly magnified, hurts sometimes?

Have you considered... not doing that?

I simply can not apprehend the mindset that goes:

    1. Wow, map/filter/reduce sure is fun in this language over here
       designed to work with it.
    2. Now I'm going to use everywhere I go, no matter how much it hurts
       and no matter what compromises I have to make.
    3. When it hurts, I will blame anything and everything except myself
       for forcing a paradigm in where it doesn't belong.
The paradigm has to be good. It just has to! Even if it hurts! Even if I'm really not enjoying using it! Even if it's murdering my runtime and killing my performance and I'm in a language where functions take quite a lot of text rather than \x -> x + 1! Even if my code is hard to read and hard to abstract and hard to refactor. Even if the foreign paradigm is critically based on pervasive laziness my language doesn't have! Or some other foundation my current language totally lacks! Gosh darn it, I'm going to use this paradigm in this code if it kills me! Because it was so much fun... over there.

I'm not saying that it isn't sometimes useful to borrow paradigms in certain places even in languages that don't support it. When it doesn't hurt like this, by all means use it as you like. What I don't understand is this constant, if not growing, stream of posts about "my gosh, my forced FP programming in this not-FP language is kinda painful here... grimaces yup, it's pretty painful all right... grits teeth yup, this is really hurting me and making me really angry and isn't fun at all... but I'm so glad I'm using this style that is making things hard and hurting me and making me angry, wouldn't dream of doing anything else!"

I know Haskell. There are many lessons I've taken from Haskell and applied to other languages, like the virtue of very strongly separating IO code from logic, the virtues of taking extra steps to provide and use composability in your APIs, the virtues of certain things being immutable data. But taking these lessons from functional programming languages shouldn't mean you ape the surface solutions functional programming uses; you should be thinking about how to idiomatically bring the target virtues into the other languages you use. There will be compromises, but sometimes there will also be things the current language does better than Haskell. (Really quite a lot of things. Haskell's great but there's many reasons it hasn't conquered the world.) You shouldn't be so focused on importing the foreign paradigm that you completely throw away the virtues of the environment you're in.

I do not understand why so many of you are so vigorously adamant that you're going to program in the intersection of functional programming languages and imperative programming languages. It's an incredibly aggravating place to try to be. The (near) union, applied with the wisdom gleaned from both camps, is much more fun.

5 comments

I don't really see how this relates to the article. The author specifically mentions that he also encountered these problems in Racket (a functional programming language). Anyway:

>I do not understand why so many of you are so vigorously adamant that you're going to program in the intersection of functional programming languages and imperative programming languages

Because it actually works pretty well? The vast majority of programming languages today are multi-paradigm for a reason. In fact, almost all functional programming languages (Lisp, Scheme, Racket, Clojure, (S)ML, OCaml, F#, Scala) are multi-paradigm. Haskell is really the odd one among functional programming languages. The same applies to lazyness by the way, to claim that functional programming is "critically based on pervasive laziness" is simply wrong.

The idea that people use FP in a language like e.g. JS only because of an ideology driven obsession and that they constantly have to fight against the language just doesn't bear out in reality.

I'm not talking about using it when it's helpful. I do it myself all the time.

I'm talking about forcing it in when it hurts specifically.

It occurs to me that I may have accidentally answered myself at the end, when I referred to the concept of union vs. intersection. When you bring in helpful angles on your current paradigm from light cast by other paradigms, that's helpful. You're expanding your working set of options.

But when you get a bit of a good taste of those other paradigms, and then declare this is always the right way to do it, it may feel like you're expanding your paradigms, but now you're not. When you force sum types into a language that doesn't support them because they're just always the right answer, when you force lots of maps & filters in a language that can't do fusion and function calls are kind of expensive, when you force an algorithm in that depends on laziness to work, and so on, you are no longer working in a sort of union of paradigms. You're working in the intersection.

And that intersection sucks. A worst-of-both-worlds situation for sure.

You can tell, by the way people trying this are constantly screaming in pain about it.

When it hurts... stop.

Sure, it's best to work with rather than against the language, but for most languages support of functional programming is not binary, just like with OOP.

I'm pretty sure most of the complaints are generally of the "This FP thing would work well here, but the language is unfortunately not quite there" nature rather than "I will try to implement profunctor optics in C no matter the cost" square peg in round hole crazyness.

>when you force lots of maps & filters in a language that can't do fusion and function calls are kind of expensive

I think that's perfect example to show that it's really about degrees, because fusion is really a Haskell thing. Sure, it's a nice-to-have, but it's definitely not essential to functional programming.

And when people want sum types (which aren't even really FP, ALGOL had them first!), it's often because the idiomatic alternative just kind of sucks for what they're doing. Trying to "implement" sum types in a language without support is nonsensical, but complaints about lacking them are pretty understandable I think.

I’m pretty sure half the purpose of the article is to resolve the ambiguity: does it hurt because the paradigm is inelegant for this problem, or because I don’t know how to make it elegant?

Just stopping anytime you struggle is precisely how you learn nothing at all

There's genuinely good things that came from FP that creeped into popular languages:

* Algebraic data types

* Pattern matching (with exhaustiveness)

* Lambdas / Closures

* Type inference

I don't believe it's true that algebraic data types "came from" FP. The name did, but the concept has existed since before high level languages existed. Actually before computers existed since it's in Type Theory. And before that it's in ordinary human thinking for thousands of years.
As a usable programming construct though it did.

I mean, Lambdas came from the Lambda calculus which predates FP. Type inferencing was also formulated as part of typed Lambda calculus. Pattern matching definitely came about prior to FP; SNOBOL (and its predecessor COMIT) is probably the first language that really made it 'a thing'.

Very few things FP uses came from FP; they came from underlying Math and CS, and are themselves specific formulations and implementations that fit within the context of broader well understood concepts.

The question I am left with, as someone who has a bit of experience with functional languages but much more in imperative/OOP languages, is does it ever make sense to develop something like the GUI applications the author was talking about using a primarily functional paradigm ? Assume you get to pick whatever FP language you want so the issue here isn't with shoehorning FP techniques into a language that doesn't support them well, it's about whether there are certain types of programs that just aren't suited to a functional paradigm.
For GUI programming I found FRP to be a really good solution. And no, I don't consider React to actually do FRP.

The closest mainstream project to FRP is Angular using RxJS exclusively. You define how each component interacts with each other, and then you delegate to a runtime to actually perform the side effects (change the DOM in this case). Solutions like React + Redux/Elm involve a lot of plumbing that's not relevant to the issue: I just want to define relationships between components, not manually tread changes across a big state tree.

That is a very good question. The discussion often circles around pro- and against FP. Proponents of "pure FP" seem to suggest that it is the best solution for everything always. Is it?

If not it would be nice to find discussion about when it is and when not and why? What is Haskell NOT the best solution for?

In my experience Haskell is a bad solution when you need fine control over memory usage and control flow. For those cases you should probably use something like C, C++ or Rust. For everything else I think is a viable option.

The hard part is understanding how you should model your problem. I found that you could classify problems into two big groups: pipelines like compilers or web services where you get an input, do some processing and produce an output, and graph problems like GUI applications where the system is a living thing and events change how components should behave. The first group is easy, the second not so much. Excel is the best example of this kind of system: an user describes relationships between cells and lets the runtime perform the appropriate side effects. Ideally we would be able to write arbitrary programs using this same model.

It took me some time to understand how Haskell performs its "magic" but now I think I do. Anybody please correct me if I'm still getting it wrong...

Haskell main program in essence runs a big loop until it decides the program should exit. In Haskell syntax the loop is coded as a call to a tail-recursive function, which on a real hardware of course needs to be translated to do iteration. At the end of each loop it sets the new calculated value of the new state to be used during the next loop.

Using tail-recursion optimization it looks like our whole program executes a single top-level function-call of a recursive function, that is how you code it in Haskell.

This is great but somehow it feels like a trick. It is actually still doing iteration and state mutation internally while running. You could program that in an imperative language writing a top-level loop and iterating over it and modifying the statefull variables at the end of each loop.

And what happens if you cannot write your program as tail-recursive function? What if the problem domain requires you to use general recursion, which can not be optimized away like tail-recursion can?

No, that's not accurate. The final form of your Haskell program before it's compiled down to some assembly language (currently C-- or LLVM) is a graph reduction problem. The partially imperative part is the runtime system, which actually reduces the graph (= runs your program), but this is significantly more complicated than the sort of high level loop transformations you're talking about. Search "spineless tagless G-machine" for a (somewhat idealized) description of the core architecture of GHC.
What you are trying to describe is not Haskell the language but an implementation, and right now the de facto implementation is GHC. I really can't talk about how GHC works internally, but when it comes to the language you need to think your program as a value, just like `5` or `[1,2,3]` or `{ "name": "susan" }`. That is, effects are values, you compose those effects, and, your program being some kind of effect, is also a value. That's the reason behind `main :: IO ()`: your program is an `IO value`.

When it comes to "executing" your program, a runtime (like GHC's runtime) takes your program which is a value and actually performs the requested actions, dealing with mutation, loops, jumps, etc. This is a hack just as much as any other language dealing with variables instead of the stack, heap or registries directly.

In case you're interested, this idea of using some kind of interpreter which loops over your program and performs mutations and side effects is very popular in Scala: libraries like Cats and ZIO do this, allowing you to effectively get Haskell's IO. As far as I know, tail-recursion is not really needed for this, but when implemented on the runtime it allows the end user to write loops using recursion without blowing up the stack.

Right. I was thinking of how to write a game in Haskell or similar. I need to write some kind of loop to keep my program/game running indefinitely. And I need to avoid "blowing up" the stack by writing my program as a set of calls to tail-recursive functions. No?

The program must keep the previous state somewhere and then modify it based on the player's inputs and again store that new state somewhere somehow so it is remembered when next input comes. If loops which keep and modify state are not allowed, then I don't see how it could be done without tail-recursive functions.

Functional Reactive Programming (FRP) is a really nice paradigm for doing GUI in a functional setting (e.g. Haskell). I build games in Haskell., including UI. It's great.
Elm is a great example of UI done right. And it is 100% functional.
I think you're being downvoted because your tone is a bit hostile, so I'll bump you up because I think you make a good point.

I love functional idioms as well, but you have to know the cost of those idioms in a non-functional language. The first thing I did when I had to learn go was determine if I could use functional idioms (map, fold, etc.) and did some research and testing and found that, no, functional idioms are a really bad idea in go. It just isn't meant to be used that way.

For the record, I had to learn go because the team I joined used it.

Tried saying it politely. Still get downvoted. Thought I'd try a bit sharper on purpose.

Despite what it may superficially sounds like, it's an attempt to reach out and help people. If it hurts, you know, think about it. Maybe don't do something that hurts so much because it "must" be good.

FP techniques in imperative languages is a tool. A good tool sometimes. But it shouldn't be an aspiration.

↑ Knowing the price of everything and the value of nothing.

Why do you want to climb that mountain? Because it’s there.

If that's your defense, party on. If you want masochism, by golly you've found it.

But could y'all be more clear about labeling at such? You're fooling young programmers into thinking this is a good engineering idea. In engineering, we do not climb mountains just because they are there. Quite the opposite.

> If that's your defense, party on.

I’m not the author. But that’s one explanation.

This goes back to the old “this is Hacker News” explanation. Some things are just implemented for the heck of it. Some things are just done according to a certain paradigm for the heck of it. Or in order to see how far you can go.

Why not see how far you can go with pure FP before it breaks down? Or you have to make compromises.

And not everything on this site is about “good engineering practices”. (Not that I would file your original, mocking comment under such a professional label, either.)

On a second skim though the submission is more naive than I would have expected from a Racket programmer. Using imperative datastructures like arrays (contiguous chunks of memory) and then copying for every action is not really recommended. You should use purely functional datastructures if you really want to not do destructive updates.