8 ms·
The article utterly falls apart in its first paragraph where it itself acknowledges that the whole ML family including Ocaml has perfect support for mutation, r
by RandomThoughts3 2y ago
The article utterly falls apart in its first paragraph where it itself acknowledges that the whole ML family including Ocaml has perfect support for mutation, rightfully assume most Ocaml programmers would choose to not use it most of the time but then assume incorrectly that it’s because the language makes it somehow uneasy. It’s not. It’s just that mutation is very rarely optimal. Even the exemple given fails:
> For example, let's say you're iterating over some structure and collecting your results in a sequence. The most efficient data structure to use here would be a mutable dynamic array and in an imperative language that's what pretty much everyone would use.
Well, no, this is straight confusion between what’s expressed by the program and what’s compiled. The idiomatic code in Ocaml will end up generating machine code which is as performant than using mutable array.
The fact that most programming languages don’t give enough semantic information for their compiler to do a good job doesn’t mean it necessary has to be so. Functional programmers just trust that their compiler will properly optimize their code.
It gets fairly obvious when you realise that most Ocaml developers switch to using array when they want to benefit from unboxed floats.
The whole article is secretly about Haskell and fails to come to the obvious conclusion: Haskell choice of segregating mutations in special types and use monads was an interesting and fruitful research topic but ultimately proved to be a terrible choice when it comes to language design (my opinion obviously not some absolute truth but I think the many fairly convoluted tricks haskellers pull to somehow reintroduce mutations support it). The solution is simple: stop using Haskell.
- senorrib 2y agoCame here to write exactly this. Thank you.
- runeblaze 2y agoI second the conclusion as (a brutal conclusion, but still) to stop using Haskell. Haskell allows imperative-like code but the ergonomics for day-to-day big-tech engineering is far from good. The state monad or lens are excellent tools to re-create a controlled imperative language in a vacuum, and is frankly impressive how much mutation we can conjure up from purity, but the error messages or the required understanding of PLT-ish things makes it non-scalable to "real" teams.
- astrange 2y agoHaskell almost seems like it was intentionally designed to perform poorly on real computers, primarily because of space leaks and secondarily because the non-strict evaluation gets compiled into a lot of function pointer jumps, which branch predictors hate. I think it's funny that they make you write linked list code as a metaphor for generators, but it seems like it should be the other way round. (Also, it has exceptions which are a bad language feature, and typed throws which are a worse one.)
- initplus 2y agoIt seems that way because it kind of is. The early days of functional research were equally focused on designing alternative computer architectures that were more suited to functional paradigms. Now that hardware angle has not been very successful on the whole, and we are left with languages that end up feeling a bit out of place on the hardware we have today. Another thing to note is that there is a lot of untapped potential in fb compilers. It’s suffering from underinvestment.
- tome 2y ago> (Also, it has exceptions which are a bad language feature, and typed throws which are a worse one.) Can you elaborate? You can't stop people simulating exceptions with sum types, and if you have exceptions, why wouldn't you want them to be typed?
- IshKebab 2y agoSum types aren't a simulation of exceptions.
- tome 2y agoCan you elaborate?
- wizzwizz4 2y agoThey don't get stack traces, for one. (That's arguably the biggest problem with Rust: .unwrap() gives you a stack trace, but has problems; whereas ? erases your stack trace.) In principle, static analysis could identify unhandled exceptions, then trace the exception, then make that information available to the top-level "Err returned from main" handler. In practice, that's never going to happen in Rust.
- devmunchies 2y agoI use f# daily at my company and am actually glad that many dotnet api integrations use array buffers (e.g. a byte array for streaming) this forces me to optimize the f# code by thinking in terms of low-memory, mutable data structures when interfacing with external libraries.
- nickpeterson 2y agoYeah, F# in general is pretty mutation friendly given how functional it is.
- phillipcarter 2y agoYep, and moreover, the combination of most library design and general culture around the language reinforces the dynamic of using mutability only when it's needed or the most straightforward way to implement something, and contain that with immutable interfaces wherever possible. I like to think we helped with this several years ago when making official guidance: https://learn.microsoft.com/en-us/dotnet/fsharp/style-guide/conventions#immutability-and-mutation https://learn.microsoft.com/en-us/dotnet/fsharp/style-guide/...
- deredede 2y agoI mostly agree with your sentiment but this: > Well, no, this is straight confusion between what’s expressed by the program and what’s compiled. The idiomatic code in Ocaml will end up generating machine code which is as performant than using mutable array. I disagree with. There are different ways to get close to the performance of `Array.map` with lists (best case scenario you don't care about order and can use `List.rev_map`), but you will always have memory (and GC) overhead and so lists are strictly inferior to arrays for the presented use case.
- RandomThoughts3 2y agoThat’s not what the article is talking about. The proposed exemple is a traversal of a different data structure to collect results in an array. That’s a fold and will properly be tco-ed to something equivalent to adding to an array if you use list cons in the aggregation, might actually be better depending on how much resizing of the array you have to do while traversing.
- deredede 2y agoI think `Array.map` is a perfectly reasonable reading of "you're iterating over some structure and collecting your results in a sequence". But sure, in the `fold` scenario where you don't know the number of results in advance (you are more likely to know if you use imperative data structures, e.g. `Hashtbl.length` is constant-time whereas `Map.cardinal` is not), lists might be faster than growing arrays with copies. They are still going to use more memory, and they are unlikely to to be faster than a rope-like structure that grows with no copies.
- senorrib 2y agoIt isn’t. There’s no guarantee that .map will be processed in sequence. In fact, .map is usually a great candidate for parallelization.
- deredede 2y agoThe "sequence" in the problem statement does not refer to the order of operations but to the data structure storing the results. A parallel `Array.map` still computes a sequence, even though it may not compute in sequence.
- derdi 2y ago> Well, no, this is straight confusion between what’s expressed by the program and what’s compiled. The idiomatic code in Ocaml will end up generating machine code which is as performant than using mutable array. This cannot be true in general. There are machine code patterns for which arrays are faster than linked lists. The OCaml compiler, great as it is, won't turn linked list source code into array machine code. Therefore, there is idiomatic code in OCaml that will not be as performant as arrays. > It gets fairly obvious when you realise that most Ocaml developers switch to using array when they want to benefit from unboxed floats. This is one example why your statement above is not true.
- RandomThoughts3 2y ago> This is one example why your statement above is not true. You are misreading my comment. I’m not intentionally contradicting myself in two paragraphs next to each other (I’m not always the brightest but still). The point is that contrary to what the article states ML developers are not avoiding mutations because they are uneasy to use but because they trust their compiler when they know it will do good. Proof is that in other case they will use mutations when it makes sense to do so because the compiler does not do a good job. The first paragraph refers to the specific case my comment quotes just before: data structure traversal and storage of elements in a set.
- derdi 2y ago> The point is that contrary to what the article states ML developers are not avoiding mutations because they are uneasy to use but because they trust their compiler when they know it will do good. Proof is that in other case they will use mutations when it makes sense to do so because the compiler does not do a good job. It will do a good job, yes. Will it do the best possible job compared to some other algorithm or data structure? It can't. Not in general. And maybe not in the specific case either: > The first paragraph refers to the specific case my comment quotes just before: data structure traversal and storage of elements in a set. So, this: https://ocaml.org/play#code=bW9kdWxlIE15X2R5bmFycmF5ID0gc3RydWN0CgogIHR5cGUgJ2EgdCA9IHsgbXV0YWJsZSBsZW5ndGg6IGludDsgbXV0YWJsZSB2YWx1ZXM6ICdhIGFycmF5IH0KCiAgbGV0IG1ha2UgaW5pdCA9CiAgICB7IGxlbmd0aCA9IDA7IHZhbHVlcyA9IEFycmF5Lm1ha2UgMTAgaW5pdCB9CgogIGxldCBhZGQgZCB4ID0KICAgIGlmIGQubGVuZ3RoID0gQXJyYXkubGVuZ3RoIGQudmFsdWVzIHRoZW4gYmVnaW4KICAgICAgZC52YWx1ZXMgPC0gQXJyYXkuKGFwcGVuZCBkLnZhbHVlcyAobWFrZSAobGVuZ3RoIGQudmFsdWVzKSB4KSkKICAgIGVuZDsKICAgIGQudmFsdWVzLihkLmxlbmd0aCkgPC0geDsKICAgIGQubGVuZ3RoIDwtIGQubGVuZ3RoICsgMQoKZW5kCgpsZXQgdGltZSBmbiA9CiAgbGV0IHN0YXJ0ID0gU3lzLnRpbWUgKCkgaW4KICBpZ25vcmUgKGZuICgpKTsKICBsZXQgZmluaXNoID0gU3lzLnRpbWUgKCkgaW4KICBmaW5pc2ggLS4gc3RhcnQKCmxldCBsaXN0X2JlbmNoIGlucHV0ID0KICBmb3IgaSA9IDEgdG8gMyBkbwogICAgbGV0IHQgPSB0aW1lIChmdW4gKCkgLT4gTGlzdC5tYXAgKGZ1biB4IC0%2BIHggKyAxKSBpbnB1dCkgaW4KICAgIEZvcm1hdC5wcmludGYgImxpc3Q6ICAgICAgICAlZiBzZWNcbiUhIiB0CiAgZG9uZQoKbGV0IGR5bmFycmF5X2JlbmNoIGlucHV0ID0KICBmb3IgaSA9IDEgdG8gMyBkbwogICAgbGV0IGQgPSBEeW5hcnJheS5jcmVhdGUgKCkgaW4KICAgIGxldCB0ID0gdGltZSAoZnVuICgpIC0%2BIExpc3QuaXRlciAoZnVuIHggLT4gRHluYXJyYXkuYWRkX2xhc3QgZCAoeCArIDEpKSBpbnB1dCkgaW4KICAgIEZvcm1hdC5wcmludGYgImR5bmFycmF5OiAgICAlZiBzZWNcbiUhIiB0CiAgZG9uZQoKbGV0IG15X2R5bmFycmF5X2JlbmNoIGlucHV0ID0KICBmb3IgaSA9IDEgdG8gMyBkbwogICAgbGV0IGQgPSBNeV9keW5hcnJheS5tYWtlICgtMSkgaW4KICAgIGxldCB0ID0gdGltZSAoZnVuICgpIC0%2BIExpc3QuaXRlciAoZnVuIHggLT4gTXlfZHluYXJyYXkuYWRkIGQgKHggKyAxKSkgaW5wdXQpIGluCiAgICBGb3JtYXQucHJpbnRmICJteSBkeW5hcnJheTogJWYgc2VjXG4lISIgdAogIGRvbmUKCmxldCBfID0KICBsZXQgaW5wdXQgPSBMaXN0LmluaXQgKGludF9vZl9zdHJpbmcgU3lzLmFyZ3YuKDIpKSBGdW4uaWQgaW4KICBtYXRjaCBTeXMuYXJndi4oMSkgd2l0aAogIHwgImxpc3QiIC0%2BIGxpc3RfYmVuY2ggaW5wdXQKICB8ICJkeW5hcnJheSIgLT4gZHluYXJyYXlfYmVuY2ggaW5wdXQKICB8ICJteV9keW5hcnJheSIgLT4gbXlfZHluYXJyYXlfYmVuY2ggaW5wdXQKICB8IF8gLT4gKCkK https://ocaml.org/play#code=bW9kdWxlIE15X2R5bmFycmF5ID0gc3Ry... $ for len in 5_000_000 10_000_000 25_000_000; do echo "-- ${len} elements --"; ./a.out list $len; ./a.out dynarray $len; ./a.out my_dynarray $len; echo; done -- 5_000_000 elements -- list: 0.191551 sec list: 0.196947 sec list: 0.192806 sec dynarray: 0.301362 sec dynarray: 0.268592 sec dynarray: 0.266118 sec my dynarray: 0.163004 sec my dynarray: 0.142986 sec my dynarray: 0.143634 sec -- 10_000_000 elements -- list: 0.377447 sec list: 0.367951 sec list: 0.312575 sec dynarray: 0.607158 sec dynarray: 0.582378 sec dynarray: 0.538621 sec my dynarray: 0.319705 sec my dynarray: 0.296607 sec my dynarray: 0.286634 sec -- 25_000_000 elements -- list: 0.971244 sec list: 0.953493 sec list: 0.922049 sec dynarray: 1.515892 sec dynarray: 1.319543 sec dynarray: 1.328461 sec my dynarray: 1.119322 sec my dynarray: 0.971288 sec my dynarray: 0.973556 sec -- 50_000_000 elements -- list: 1.852812 sec list: 1.848514 sec list: 1.505391 sec dynarray: 3.065143 sec dynarray: 2.941400 sec dynarray: 2.672760 sec my dynarray: 2.115499 sec my dynarray: 1.963535 sec my dynarray: 1.995470 sec -- 75_000_000 elements -- list: 2.942536 sec list: 2.910063 sec list: 2.354291 sec dynarray: 4.567284 sec dynarray: 4.342670 sec dynarray: 3.979809 sec my dynarray: 2.528073 sec my dynarray: 2.225738 sec my dynarray: 2.226844 sec A simple dynamic array implementation (my_dynarray) beats a list over a wide range of lengths. But not at all lengths! OCaml's built-in Dynarray is not competitive, but that's because it wants to make certain strong guarantees. To be clear, I agree with your general point that we can do just fine writing nice clean pure functional OCaml code for most of our code and can hand-optimize where needed. But your very specific claims rub me the wrong way.
- deleted 2y ago[deleted]
- ufo 2y agoOne thing that many people miss is that Haskell's monadic style is a direct consequence of lazy evaluation. It all started because they thought lazyness was nice, and wanted to make a language that brought that front and center. But then they found out that they had to come up with a new way to do side-effects, because traditional side-effects don't work when the order of evaluation is unpredictable.
- fire_lake 2y agoI think this is historically wrong. Monads didn’t land until later in Haskell, no?
- tome 2y agoI don't think GP is contradicting that.
- p_l 2y agoThey did, but they also did land explicitly to make I/O suck less in lazily evaluated language instead of magic main function signature working to provide explicit ordering. There's a reason why Monads aren't exactly monadic and why IO was the original monad in GHC -as well as why non-lazy, non-super-pure languages never really go for Monads
- eru 2y agoActually, lots of those more pragmatic languages go for monads, they just don't use them for input/output. The way JavaScript handles async is pretty close to monadic. Error handling via the local equivalent of Maybe / Either is monadic. Tuples are monadic. Sequences can be monadic. Etc. Some languages have a flexible enough type system to expose this (like Haskell), some don't. Some like Rust generally don't expose monads to the type system, but their users are aware enough of the shared monadic structure that you can see it reflected in the naming conventions for functions that do essentially the same thing (in a monadic sense) but for different structures.
- bmacho 2y ago
- pcstl 2y agoCan you provide evidence that code which is "as performant as using mutation" is generated? Mutation tends to be very hard to beat.
- brabel 2y agoIt's literally impossible on a CPU. Some people claim FP languages can make optimisations that are based on the fact that values are immutable and which other languages can't, and that's definitely true. But the problem is that those optimisations are almost never actually made by real programming languages, and when they are, they're still slower than a low level imperative language like C or Rust in very nearly every case. Come'on FP hackers, prove me wrong!
- RandomThoughts3 2y agoNo one needs to prove you wrong because that's not where the goal post actually is. You could craft hand written assembly code which will be faster than optimised C code most of the time yet you don't. Plenty of programmers are perfectly writing imperative code in Java doing a ton of necessary boxing and unboxing. It's all a trade off between performance and usability. The fact is that the Ocaml compiler does a good enough job with functional code that its performance is actually comparable to imperative solutions most of the time.
- p_l 2y agoIf you compile your immutable program with LLVM, literally one of the cure steps is transforming it into functional form that does not allow mutations. This is called Single Static Assignment form and its denial of mutation is crucial to optimization, from common expression removal, to efficient register allocation, and all sorts of control flow analysis.
- eru 2y ago> If you compile your immutable program with LLVM, literally one of the cure steps is transforming it into functional form that does not allow mutations. You probably wanted to write something like 'If you compile your _mutating_ program with LLVM, [...]'?
- kazinator 2y agoAnd not even always how the code is compiled. Canned runtime library routines can you destructive techniques to produce their outputs. The program doesn't see an aggregate object until the function returns it. (If we set aside lazy structures for a moment, but those are actually another example of something that can be built destructively under the hood. As you probably more deeply into the object it is mutated to make more of it materialize.)
- injuly 2y ago> which is as performant than using mutable array. I get what you're trying to say, but that is provably false. As great as the OCaml compiler is, it currently is not capable of the aggressive optimizations that GHC can do with lists. More often than not, the compiler mostly won't have enough static assertions to reliably generate machine code like that in a real world application (unless explicit mutation is used, of course). > Functional programmers just trust that their compiler will properly optimize their code. Precisely. This is why having safe local mutation as a language level feature can give more control to the programmer. We no longer have to rely on the compiler to correctly guess whether a routine is better expressed as an array or a cons list. > The whole article is secretly about Haskell. and ML, Koka, Clean, Mercury. The article is about allowing local mutation without breaking referential transparency at the language level. "Stop using haskell" is a very shallow conclusion, IMO.
- antonvs 2y ago> The article utterly falls apart in its first paragraph where it itself acknowledges that the whole ML family including Ocaml has perfect support for mutation Was the article updated since you wrote this? I don’t see the text you’re referring to. > Well, no, this is straight confusion between what’s expressed by the program and what’s compiled. You’re getting at an important point here, but then you seem to fall into this same trap when you write: > … the many fairly convoluted tricks haskellers pull to somehow reintroduce mutations Monads started out as a way to represent the semantics of effects in a mathematical context, to support formal representations of the semantics of programming languages that were more tractable from the perspective of analysis and proofs. Even mainstream compilers ended up using related techniques, like static single assignment, for which an equivalence to continuation-passing style exists, and they did this for the same kinds of reasons: tractability of analysis and to support automated transformations. The use of monads for writing ordinary code - as opposed to language semantics - in Haskell exploited these techniques, allowing effects to be expressed in a purely functional way. But at its root, this is a rigorous way of expressing scenarios that require effects, it’s not just some sort of “convoluted trick”. There are benefits to doing this that go beyond just a hack to implement effects in a pure language. Which is why it’s unlikely that people who understand these issues will “stop using Haskell”, despite the learning curve barrier it seems to cause (arguably because people tend to learn to program in ad-hoc ways, which Dijkstra notoriously bemoaned.) But many of the most powerful languages have such a barrier, it just takes different forms depending on the nature of the language.
- RandomThoughts3 2y ago> Monads started out as a way to represent the semantics of effects in a mathematical context I mean, that statement is untrue but even then I wasn't talking about monads here (monads are not a convoluted trick as far I'm concerned). I was thinking of lenses. > Which is why it’s unlikely that people who understand these issues will “stop using Haskell” Plenty of people who value being able to analyse program and do proof don't use Haskell. The heart of the debate is "Is being pure worth the cost?".
- halter73 2y ago> The fact that most programming languages don’t give enough semantic information for their compiler to do a good job doesn’t mean it necessary has to be so. Functional programmers just trust that their compiler will properly optimize their code. While everyone has to trust their compiler will make reasonable optimizations to some extent, there becomes a certain point of complexity where it becomes difficult to intuitively know if a "sufficiently smart compiler"[1] will properly optimize which is problematic. I realize you're arguing Haskell is worse than Ocaml in this regard, but I'd argue it's harder to reason about how functional code will be translated into machine code than comparable procedural code in general. [1]: https://prog21.dadgum.com/40.html https://prog21.dadgum.com/40.html