Live data from Hacker News

Two Vexing Problems in Functional Programming

matthewbutterick.com

71–80 of 217 posts

Re: Two Vexing Problems in Functional Programming

#71
Interesting thread. Although most of the comments are "you're holding it wrong", I think the author brings up good points, which are roughly along the lines of : the FP language tutorials don't tell you how to write real programs. Now, granted the problems cited will show up at some level of scale in non-FP programming -- you can't just rummage around in some huge nested data structure inside a C++ server application for example because you'll end up holding dangling pointers among other things. However, perhaps you can get much further with non-FP languages by following the basic tutorials.

Also interesting to note that modern UI application frameworks such as React/Redux don't mutate state in-place. They tend to generate some kind of mutation object that is "played" against the in-memory stored application state. Probably similar to the FP-kosher solutions suggested here.

Re: Two Vexing Problems in Functional Programming

#72
post #62

Naming things, and cache invalidation :). Now seriously: while reading this, and especially the first problem, my head was shouting "immutable data structures! immutable data structures!". The author surely must have encountered IDSs during their 10-year tenure with Lisp. I'm curious about why they thought they would not fit the bill for the problems they are writing about.

Did you read the article? The entire discussion was on the tension between immutable data structures and cognitive complexity on one side, and machine performance and mutability on the other side.

Re: Two Vexing Problems in Functional Programming

#73
post #24

Earlier quoted context omitted.

I love the Purely Functional Data Structures book (so many cool data structures and ideas!) but does really "solve" it? For e.g. I double checked and the PFDS for an array (the author's example) would be a skew-binary random access list with O(log N) cost for random access, whereas an "imperative random access array" is O(1). Of course, the skew-binary random access list has persistence (you get to keep references to…

The thing is, functional and imperative programming are mathematically equivalent - to the point that you can express imperative code in a pure functional language, if that's what you really need. If you want a O(1) random access in a functional language, you can formally define it; the problem with this is that you may end up embedding a whole computer in your program to do it, if your program heavily depends on sid…

If you used the State monad for the author's interpreter example, and you did it in Racket (your "state of the world" is a cons-list), what would be the run-time of the resulting interpreter? Similar question for streams (although I'm not that familiar with streams, so I don't know how they would apply here, or if they can be implemented without changes to the base language)

Re: Two Vexing Problems in Functional Programming

#74
post #47

Earlier quoted context omitted.

That's a good point. I wonder if the problem is that efficient persistent arrays aren't in racket's standard library (there are some third-party libraries implementing them though). Since racket isn't a "pure FP" language, they include cons-arrays and mutable vectors, and I imagine they felt that there wasn't a need to include efficient persistent arrays in addition to those.

Sort of - consider when Lisp originated. The heritage/age comes from a time before the research in persistent data structures happened, or even the issues with mutability were fully known. Because of that, idiomatic Lisp tends to be mutable, and no one is writing a new Lisp who isn't already familiar with another one. That's not to say immutability isn't worthwhile there, or that no Lisp standard libraries support it…

I don’t know about that; clojure was specifically created with persistent data structures in mind, and it’s definitely as much a lisp as racket is

Re: Two Vexing Problems in Functional Programming

#75

These problems have lots of other well-known solutions. For large objects: Persistent data structures with path-copying. Batching small mutations (and using mutation internally in a way that doesn’t leak out). Using the type system somehow to distinguish cases where copying is not necessary (experimental in certain new languages). Structuring your program around a “data store” that is mutable. For the “references” pr…

Persistent data structures exist and work very well but they still remain way slower than alternatives that don't support persistence. This is especially bad if you want the data structure to be shared across multiple threads. Just iterating over a tree now requires taking and releasing oodles of locks. Add in the GC overhead and you've got a meaningful problem. Yes, you aren't constructing an entirely new 30k byte a…

> Persistent data structures exist and work very well but they still remain way slower than alternatives that don't support persistence.

Depending on your definition of "way" this is true. A lot of persistent vector-like structures can have quite good performance, but if your runtime is dominated by iteration and random updating, you will still get a speed up by using raw arrays, i.e. guaranteed contiguous blocks of memory.

> This is especially bad if you want the data structure to be shared across multiple threads. Just iterating over a tree now requires taking and releasing oodles of locks.

This is not. The whole point of immutable data structures and concurrency is that iterating over them does not require any locks because the structure won't change! So concurrent reads require absolutely no new code and certainly no locks. For concurrent writes, the usual strategy is a single CAS-like operation at the top-level to swap out the old immutable structure for a new one (e.g. Clojure atoms, Java AtomicReferences, Haskell IORefs, etc.). But that's a single thing for the whole structure, not every level.

In general concurrent update in the functional programming world is handled by CAS not locks.

Re: Two Vexing Problems in Functional Programming

#76
I think that copying an entire array just to modify one bit doesn't really demonstrate the flaw of FP as much as it is just bad programming. The problem isn't really with the paradigm, per se, rather it is the understanding of each problem domain.

I hate to be pedantic, and I know this sounds like a no true scotsman argument, but I really feel FP discussions always devolve into this because people have so many different levels of understanding of what FP is supposed to mean.

I think that many people who espouse FP like the "aesthetic" of FP. They like that map/filter/reduce look "cleaner" than nesting ifs, breaks and continues in loops. They love how pattern matching on Discriminated Unions is so much clearer than imperative switch statements or OOP-style polymorphism, where to see the behavior of a single function you need to look through 5 different files. This is the kind of evangelising that stops at "cute little values" and toy examples.

But (subjective opinion here) I think what the more experienced FP proponents are trying to promote are in fact just sane, good programming practices made explicit, but they call it FP too (thus "no true scotsman").

For example, for the OP's bf problem, is copying a 30k byte array for every operation a good idea? Of course not! Is isolating the "stateful" operations (operations that modify this byte array) to small sections of code, possibly even gathering all the modifications that need to be made and only applying them after the function returns, rather than littering stateful operations throughout the code, so that it becomes much easier to figure out when and why some state changes, a good, "FP" idea? Hell yeah! Is changing your data structure to a persistent data structure so you can retain your "op: Buffer -> Buffer" mental model while preserving more performance? Possibly so! (Though I think for such a problem the mutable buffer is the obviously correct data structure; a persistent DS doesn't give much benefit since each operation takes the whole array and returns the whole array before the next operation is executed)

In conclusion I think focusing on the principles of good programming are much more important than focusing on the external appearances of each paradigm. You can architect clean, loosely coupled components with little shared state in OOP as well as in FP. We ought to focus on teaching that.

Re: Two Vexing Problems in Functional Programming

#77
> Indeed, Perlis’s quip gestures toward the ulti­mate tension within all program­ming languages: how to connect the gossamer realm of human ideas to the grubby reality of a computing machine. Arguably the whole point of program­ming languages is to insu­late us from those details, and allow us to express ourselves natu­rally. But the farther we drift from reality of the machine, the less effi­ciently we can use its resources.

I would call it challenge, rather than tension, and I don't think expressing ourselves naturally is the only goal. We also want to express a solution that is tractable, that we can understand and have confidence in. "Natural" imperative programming can become intractable when mutating state; the goal of functional programming is to find a different way that is more tractable for humans, without worrying about if it feels "natural." If there's one thing programming (and mathematics!) has shown us, it's that anything we can do will eventually feel natural if we do it enough.

(We produced multiple generations of programmers for whom C-style for loops were the most natural thing in the world, almost the benchmark for what felt natural in a programming language. The "map" function was weird-ass shit to them. I know because the experienced programmers I worked with called it weird-ass shit when I discovered it in Python as a wee junior in 2000 and wondered why Java and C++ didn't have it.)

I don't think functional programmers consistently write code that is more tractable than imperative code, not yet. (I think some of them aren't trying, honestly.) But I think functional programming languages will evolve to better support that goal. It has only been in the last 10-20 years that the majority of working software engineers have fully recognized the difficulty of managing shared mutable state, generating widespread demand for functional programming languages for practical applications like web services, data processing, user interfaces, and so on. The trade-offs for those applications are different from the trade-offs for academic research, and the trade-offs for most industry programmers are different from the trade-offs for researchers who are immersed in the mathematical foundations. The academics have shaped functional languages for decades, but working software engineers will have more and more influence in the future.

Re: Two Vexing Problems in Functional Programming

#78
post #58

Particularly the "parent-child" example feels like a non issue to me. You probably want to keep multiple versions of a glyph anyway to support things like undo, in which case explicit parent update is a requirement, not an annoyance. No reason you can't have both: g2 = updateGlyph(g1) f2 = updateFontInGlyph(f1, g2)

I think the crux is that any complex data structure f1 using glyphs would not need to be updated explicitly in an imperative program through a function like updateFontInGlyph; the runtime would do it as part of its semantics. Of course that's a double-edged sword, as you may get undesired side effects as well as the desired ones like this.

There are ways to achieve such "automatic updating" of items in a functional data structure, but you need to build them explicitly instead of relying on the runtime, which gives you more responsibility but also more control. The most direct approach as compared to imperative code would be to build the data structure on top of mutable state-based streams, though there are other possibilities like monotonic or immutable structures.

https://blog.waleedkhan.name/monotonicity/

Re: Two Vexing Problems in Functional Programming

#79
post #10

Earlier quoted context omitted.

↑ 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.

Re: Two Vexing Problems in Functional Programming

#80

Personally I never really understood the benefit of thinking in functional terms about things that are conceptually more naturally thought of as persistent objects. For example when the author picks "parents" or "children" or in general maybe any entity of any kind, I don't understand what functional thinking gets me. Sure instead of manipulating mutable objects I can think of every entity being 'one thing' at 'one m…

I’ve encountered three benefits to functional programming:

- immutability and function application perspective “play nice” with distributed applications, both for distributing work and idempotence

- in a similar vein, the “everything persists, apply functions” perspective works nicely in data science for finance, where you need auditable calculations

- and similarly, but more broadly, if you’re implementing a workflow engine, then the functional perspective is a clear winner: you’re literally writing a program to manipulate and apply functions!

Friends have told me a fourth:

- composition and lenses are nice for UI

Post reply on HN