1. 28
  1. 7

    This is a pointless article, really. There are so many ways of doing safe concurrency in Haskell, and yet, the author chooses a very unidiomatic unsafe way, which happens to highlight Rust’s type system’s fundamental strength. The one thing the whole language is built around.

    What’s at fault here is the secret function. You can’t just accept an arbitrary IO action and run it concurrently with any other arbitrary IO action. Haskell gives you so many ways to encode your expectations from your arguments, so secret should be more conservative about what it accepts if it’s planning to run it concurrently.

    1. 11

      I don’t think it’s pointless. Your argument applies to any language, even C++. C++ gives you primitives for composing safe concurrent programs but I wouldn’t say articles pointing out how hard it is to do so in practice are pointless. The main takeaway from the article is as you said, effectful computation is hard. I don’t think that’s pointless.

      1. 9

        The difference is that the IO ref he’s using causes a gagging response in a Haskeller, and it’s definitely a red flag that you’ll have to justify rigourously to anyone reviewing your code. Whereas in C++ you’re defining the equivalent of an IORef every second line, so an error like this can easily slip through. In a way, using an IORef is like an unsafe block in Rust.

        So, it’s not that Haskell gives you safe primitives, it’s that the natural, easy, idiomatic way to write Haskell is safe.

      2. 3

        I have a similar reaction to most articles that complain about Rust allowing unsafe things to be expressed. Haskell STM is a dream. Both languages give you plenty of escape hatches that can be misused, but we’re able to write such nice libraries abstracting over the dangerous parts because of them.

        1. 1

          I’ve never heard of anyone having a problem with Rust allowing unsafe things to be expressed. I think you’re misrepresenting the argument against how ‘unsafe’ works in Rust.

          The problem is that once you have ‘unsafe’ anywhere in your code, it becomes very difficult to control how much code must be written with that in mind. Changing some code not marked unsafe can create memory unsafety that wasn’t there before. That’s a really bad attribute to have.

          1. 1

            Yup, just like the escape hatches in haskell. We have two usable languages because of them.

            1. 1

              ‘Haskell does it’ is not an excuse.

            2. 1

              The problem is that once you have ‘unsafe’ anywhere in your code, it becomes very difficult to control how much code must be written with that in mind. Changing some code not marked unsafe can create memory unsafety that wasn’t there before. That’s a really bad attribute to have.

              This is also misrepresenting, I must say. unsafe blocks and functions are specifically not allowed to mess with the surroundings. Especially, if you take safe types in and give safe types out (borrows, lifetimes, owned data, etc.), you have to hold all their invariants (e.g. that &mut doesn’t alias). Also, unsafe code might express expectations on the direct caller (e.g. “this function could be called twice to produce two &mut references, which is why it is unsafe and you must make sure that you don’t do this”). But that’s all very localised.

              There’s bugs in implementing this, and they might bite you far down the road, but nothing that people have to keep in mind in the scope of a larger codebase.

              1. 1

                This is also misrepresenting, I must say. unsafe blocks and functions are specifically not allowed to mess with the surroundings.

                They absolutely are allowed to. If they couldn’t mess with their surroundings, they simply wouldn’t do anything.

                Also, unsafe code might express expectations on the direct caller (e.g. “this function could be called twice to produce two &mut references, which is why it is unsafe and you must make sure that you don’t do this”). But that’s all very localised.

                It’s not localised. Any module with unsafe has to be analysed as a whole. You cannot just look at the parts marked ‘unsafe’. If ‘unsafe’ is anywhere in the module, you have to look at the whole module, and the result of looking at the whole module might be ‘the behaviour of this module depends on the behaviour of other modules too, we have to look at all the rest of them’.

                People say things like ‘unsafe is good, because unsafe operations can only happen inside unsafe blocks’. And that’s true, in a sense, but it’s also false in a sense. Unsafe operations in the sense of ‘things that can have undefined behaviour’, yes. Unsafe operations in the sense of ‘things that can cause undefined behaviour later’, no.

        2. 2

          I’m not a Haskell expert… is there a way of lifting something into the type layer so that this bug would be caught by the type checker?

          1. 6

            Probably with linear types, which are proposed feature of GHC, but probably not in current versions.

            As I understand (vaguely), notion of “consumption of values” is required for that, which linear types provide.

            1. 1

              What invariant would you encode with the types that would prevent race conditions? Even with Rust the article shows that it doesn’t prevent race conditions if you do something silly like not putting the lock around the entire critical section.

              I’m going to pull some analogies out of my ass and say this is equivalent to the halting problem. If you can prevent all race conditions then you’re solving the halting problem. In general, Rice’s theorem basically says all non-trivial program properties are undecidable.

              1. 4

                Rust doesn’t allow you to write the code without the lock.

                1. 2

                  It’s better than nothing but you can still acquire and release the lock in the wrong places and wrong times. It fixes some problems but even with that you can still deadlock. Erlang famously uses immutable message passing to avoid basically most concurrency problems but incorrect message ordering can still create deadlocks. I’m not aware of any language that guarantees liveness and safety conditions purely with the type system.