Hacker News new | ask | show | jobs
by RandomThoughts3 689 days 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.

11 comments

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

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

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

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

Oops, this had a performance bug. Instead of:

    if d.length = Array.length d.values then begin
      d.values <- Array.(append d.values (make (length d.values) x))
    end;
the array reallocation should actually be:

    if d.length = Array.length d.values then begin
      let new_array = Array.make (Array.length d.values * 2) x in
      Array.blit d.values 0 new_array 0 (Array.length d.values);
      d.values <- new_array
    end;
otherwise we allocate about a third more memory than needed. It's telling that even with this performance bug the dynamic array was broadly better than lists.

New results for the previously slowest cases:

    -- 25_000_000 elements --
    list:        0.977002 sec
    list:        0.963903 sec
    list:        0.950473 sec
    dynarray:    1.476165 sec
    dynarray:    1.281724 sec
    dynarray:    1.343222 sec
    my dynarray: 0.872558 sec
    my dynarray: 0.755902 sec
    my dynarray: 0.753746 sec
    
    -- 50_000_000 elements --
    list:        1.914777 sec
    list:        1.886989 sec
    list:        1.542614 sec
    dynarray:    2.922376 sec
    dynarray:    2.783559 sec
    dynarray:    2.537473 sec
    my dynarray: 1.725873 sec
    my dynarray: 1.545252 sec
    my dynarray: 1.515591 sec
    
    -- 75_000_000 elements --
    list:        2.827154 sec
    list:        2.835789 sec
    list:        2.318733 sec
    dynarray:    4.354404 sec
    dynarray:    4.150271 sec
    dynarray:    3.781488 sec
    my dynarray: 1.887360 sec
    my dynarray: 1.929286 sec
    my dynarray: 1.814873 sec
This turns an uneasy head-to-head into a clear win for dynamic arrays. Honestly, how could it be otherwise?
> Honestly, how could it be otherwise?

It can't be otherwise if you're just assuming a straightforward compilation model where your written roughly reflects the assembly code that's generated. This just isn't the case with better compilers though.

For instance, Haskell can often perform rewrites or fusion passes that entirely eliminates the loop and all intermediate data structures, effectively giving a near-infinite speedup compared to alternatives. You typically can't perform such optimizations when side-effects are in the middle of the computation, for numerous reasons.

You could have backed this statement up with a benchmark, and you chose not to.

(You are right on the general hand-waving level that such optimizations are sometimes possible. But there are no fuseable intermediate data structures in this particular problem.)

But they are still very much in the same order of magnitude... pretty impressive that these solutions are all in the same ballpark, I would've expected much bigger differences.
The dynamic array spends most of its time copying old elements as it grows exponentially. If you pre-size it to the right size, you eliminate this copying, and the difference becomes 5x. In practice you often have an idea of the size, at least as a rough estimation, so you would win by a larger margin. But that was not part of the spec.

Also, other ways of organizing dynamic not-quite-an-array-but-not-quite-a-linked-list data structures exist. It could be a dynamic array (or linked list) of dynamic arrays to eliminate repeated copying of the oldest elements.

No, not this. You are doing a map. A traversal of a data collection is not a map but a fold. The exemple in the article is specifically about collection which you would do by consing to a list (mostly free once optimised) in a fold_left (so tail call optimised to a loop). See my other comment.

Also, List.map is not optimised in Ocaml. It uses a naive implementation and not a tail call. You have to use rev_map to get good perf.

> A traversal of a data collection is not a map but a fold. The exemple in the article is specifically about collection which you would do by consing to a list (mostly free once optimised) in a fold_left (so tail call optimised to a loop).

The exact wording in the article is "let's say you're iterating over some structure and collecting your results in a sequence". You are interpreting a lot into this. Also, your description is of a map (reversed, and expressed as a fold). Anyway, where is your benchmark?

> Also, List.map is not optimised in Ocaml. It uses a naive implementation and not a tail call.

You are again contradicting yourself. Previously you were praising OCaml's optimization capabilities and now you are questioning them. Specifically in a case where there is a magic optimization that is explicitly motivated by List.map: https://ocaml.org/manual/5.2/tail_mod_cons.html . A magic optimization implemented using, guess what, mutation.

> The OCaml compiler, great as it is, won't turn linked list source code into array machine code.

Why not? If the compiler can see that you have a short-lived local linked list and are using it in a way for which an array would be faster, why would it not do the same thing that an array would do?

> Why not?

Because it doesn't. Doesn't mean it couldn't, if it tried hard enough. But it doesn't, as a statement of current fact.

Well, it doesn't make the substance wrong. This paragraph rightly summarizes it:

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

The substance of the statements I quoted was wrong. As I wrote elsewhere, I do agree with the broad statement that the combination of functional and imperative features in OCaml works just fine. But if the semantic information you give to the OCaml compiler is "linked list", it will use a linked list rather than a data structure that might be better for the task at hand.
I 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.

That’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.
Works if you are building one list, but what if you are building multiple? What's suggested on the OCaml site and what's taught in most of academia is to use a recursive function with accumulator arguments that are reversed before returning to make the function tco:able. I doubt OCaml can optimize that pattern well, but idk.
You can benefit from TCO while building multiple lists.

  let rec f evens odds = function
    | [] -> (evens, odds)
    | x :: xs  ->
      if x mod 2 = 0 then f (x :: evens) odds xs
      else f evens (x :: odds) xs
OCaml optimizes this fairly well and will compil it down to a single loop with two variables. If you reverse the lists there is going to be additional loops (and corresponding allocations).

However OCaml also provides the "tail mod cons" optimization that allows to get some of the benefits of TCO without needing to reverse the list (this is implemented as a program transformation that uses mutability under the hood), and that one will only work if you are building a single list.

I think HN ate some characters because that code doesn't look valid. But yeah, that's how you do it. In my opinion it is not pretty (e.g., what if you have some mutable context?). I also don't see how OCaml could turn the cons operations into dynamic array appends.
> I think HN ate some characters because that code doesn't look valid.

The OCaml compiler disagrees with you ;)

It also won't ever turn a cons operation into a dynamic array append.

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

It isn’t. There’s no guarantee that .map will be processed in sequence. In fact, .map is usually a great candidate for parallelization.
The "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.

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

Yep, 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/...

Yeah, F# in general is pretty mutation friendly given how functional it is.
One 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.
> One 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.

But this is not true, is it? I thought that Haskell solution for side effects was just tagging the functions with side effects, and make the compiler handle functions with side effects as regular programs, no laziness, no memoization, no reordering. 'Monad' is just a mathematical structure that regular programs obey, also 'do notation' is just a style that allows Haskell programmers to write imperative style code.

Monads don't have anything to do with laziness but historically the need for them arose because of laziness. It's the first thing explained in the introduction of Tackling the Awkward Squad:

"Call-by-need (or lazy) languages, such as Haskell, wear a hair shirt because their evaluation order is deliberately unspecified. Suppose that we were to extend Haskell by adding side-effecting “functions” such as printChar. Now consider this list

  xs = [printChar 'a', printChar 'b']
(The square brackets and commas denote a list in Haskell.) What on earth might this mean? In SML, evaluating this binding would print 'a' followed by 'b'. But in Haskell, the calls to printChar will only be executed if the elements of the list are evaluated. For example, if the only use of xs is in the call (length xs), then nothing at all will be printed, because length does not touch the elements of the list.

The bottom line is that laziness and side effects are, from a practical point of view, incompatible. If you want to use a lazy language, it pretty much has to be a purely functional language; if you want to use side effects, you had better use a strict language.

For a long time this situation was rather embarrassing for the lazy community: even the input/output story for purely-functional languages was weak and unconvincing, let alone error recovery, concurrency, etc. Over the last few years, a surprising solution has emerged: the monad. I say “surprising” because anything with as exotic a name as “monad” — derived from category theory, one of the most abstract branches of mathematics — is unlikely to be very useful to red-blooded programmers. But one of the joys of functional programming is the way in which apparently-exotic theory can have a direct and practical application, and the monadic story is a good example. Using monads we have found how to structure programs that perform input/output so that we can, in effect, do imperative programming where that is what we want, and only where we want. Indeed, the IO monad is the unifying theme of these notes."

https://www.microsoft.com/en-us/research/wp-content/uploads/...

Haskell showed that a monadic bind is a nice solution to chaining together IO operations once you have boxed them inside their own type and boxing IO operations inside their own type was a nice solution to the issue arising from being lazy by default.

Haskell was actually widely successful in showing that, no doubt about that.

The oldest mention of Haskell Monads that I ever had in my hands starts the description with "this nice hack from the Haskell group at University of Glasgow to make I/O nicer without bragging laziness"
I wouldn't say that's an accurate description of how Haskell handles side effects. In fact I wouldn't say that Haskell has side effects at all.
Well, it depends on exactly what you mean by side-effects.

First, obviously you have unsafePerformIO, so that everything can have side-effects.

Second, you have side-effects like using memory or using the CPU. You are not supposed to worry about those. Though a more serious side effect you do have to worry about is non-termination. Haskell doesn't track that in its type system.

You are right that the way input/output is handled can be described not as _side-effects_ but as _effects_ of interpreting values of the IO datatype.

I think this is historically wrong. Monads didn’t land until later in Haskell, no?
They 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

Actually, 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.

I don't think GP is contradicting that.
Can you provide evidence that code which is "as performant as using mutation" is generated? Mutation tends to be very hard to beat.
It'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!

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

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.

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.

> 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, [...]'?

Yes. Unfortunately I wrote the answer on the phone, and completely missed the (way more common in my writing) word replacing the right one :/
> 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.

> 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?".

> 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

I 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.
Haskell 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.)

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

> (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?

By simulating exceptions, do you mean a `Result t e` type (which Haskell calls `Either l r`)? You can use these and the Functor/Applicative/Monad hierarchy to handle errors.

What is presumably talked about is that Haskell also has actual exceptions, generated by calling e.g. `error` or `undefined`.

The semantics of these are.. interesting, mostly thanks to lazy evaluation. For example, `fst (5, error "second") ` is safe to evaluate because the second half of the tuple is a thunk and does not get evaluated. Additionally, there is, to my knowledge, no way to handle exceptions in pure code, presumably due to the undefined evaluation order.

That said, I'm not sure what the alternative would be, because a function like !! (list indexing) can fail and dealing with its fallibility would be a big burden on the programmer.

> By simulating exceptions, do you mean a `Result t e` type (which Haskell calls `Either l r`)?

Yes, exactly.

> The semantics of these are.. interesting, mostly thanks to lazy evaluation. For example, `fst (5, error "second") ` is safe to evaluate because the second half of the tuple is a thunk and does not get evaluated

Correct, and the semantics of loops is also.. interesting. For example `fst (5, last [1..])` is also safe to evaluate.

> What is presumably talked about is that Haskell also has actual exceptions, generated by calling e.g. `error` or `undefined`.

Well, I'm not sure, that's why I asked. I'm trying to understand what astrange meant by "it has exceptions which are a bad language feature, and typed throws which are a worse one". (Throwing exceptions from pure code should be left to such cases, that are impossible to recover from, in my opinion.)

> there is, to my knowledge, no way to handle exceptions in pure code, presumably due to the undefined evaluation order

Correct

> That said, I'm not sure what the alternative would be, because a function like !! (list indexing) can fail and dealing with its fallibility would be a big burden on the programmer.

Indeed. Even more so, what is one supposed to do when an invariant has been violated due to a programming error and there's no way to make progress? Haskell's exceptions are essential. It's even better when they're used in a well typed, well scoped manner, such as provided by my effect library Bluefin

https://hackage.haskell.org/package/bluefin-0.0.6.0/docs/Blu...

That's why I wanted to understand more about what astrange meant. It doesn't match my understanding!

Hm yeah, I'm not sure what astrange's point was, other than probably dissatisfaction with the combination of exceptions and Either (which I think is awfully named, as it implies a certain 'equality' between l and r).

I haven't used Bluefin, but don't Bluefin exceptions suffer from the same issue where it, essentially, 'infects' the program flow? For example, `fst (5, error "second")` seems like it would translate to `fst $ (5,) <$> throw e "second"`, where you would also need to pull an `e` from somewhere. More realistic, something like head would probably be quite awkward:

head e [] = throw e "head: empty list" head e (x:xs) = pure x

You now need to pass in the exception handle, and the return value is now `Eff es a` instead of `a`. This means that you cannot just use the result, so you will likely need to fill your program with monadic stuff like do-notation, <$> and <$>, complicating the program flow and likely also reducing laziness.

Sum types aren't a simulation of exceptions.
Can you elaborate?
Sure, exceptions are very different to Result-style error handling - even checked exceptions. Here are some differences:

* Errors must be explicitly listed as part of the function signature. Checked exceptions and the equivalent for exceptions but they are rarely used in practice. I think Android uses them, but they were so unpopular in C++ that they removed them from the language!

* The syntax to catch and handle errors is very different and more more verbose for exceptions. It can also make flow control a real pain in some languages where you can't declare a variable outside the try body (e.g. references in C++).

* Result errors need to be explicitly handled whereas exceptions are silently propagated by default.

Even though they're similar enough that you could translate one to the other in most cases, they're different enough that saying one is "an emulation" of the other is just stupid.

In my experience Result-based handling is far superior with two exceptions:

1. In functional code like map & filter where it can become quite awkward to explicitly deal with returning errors.

2. It's hard to get a stack trace from where the Err was created rather than from where it was unwrapped. Less of a problem with exceptions which record a stack trace from where they were thrown (in most languages anyway - C++ is an annoying exception).

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

And 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.)
Came here to write exactly this. Thank you.