Live data from Hacker News

Haskell's Missing Concurrency Basics (2016)

snoyman.com

21–27 of 27 posts

Re: Haskell's Missing Concurrency Basics (2016)

#21
I had some sympathy for this situation, until I saw that the concurrency was being specified via a function called `mapConcurrently`.

IMHO this is perfectly acceptable behaviour for a `map` function, since that name has gained the connotation that its purpose is to transform one 'collection' (Functor; whatever) into another, by pointwise, independent applications of the given function. Providing a function/action which breaks this independence (by writing to the same handle) breaks this implicit meaning. Heck, I'd consider it a code smell to combine interfering actions like this using a non-concurrent `map` function; I would prefer to define a separate function to make this distinction explicit, e.g.

    -- Like 'map', but function invocations may interfere with each other (you've been warned!)
    runAtOnce = map
When using `map` functions (which is a lot!) I subconsciously treat it as if it will be executed concurrently, in parallel, in any order. Consider that even an imperative languages like Javascript provide a separate `forEach` function, to prevent "abuses" of `map`. Even Emacs Lisp, not the most highly regarded language, provides separate `mapcar` and `mapc` functions for this reason.

With that said, I recognise that there's a problem here; but the problem seems to be 'mapping a self-interfering function'. If we try to make it non-interfering, we see that it's due to the use of a shared global value (`stdout`); another code smell! Whilst stdout is append-only, it's still mutable, so I'd try to remove this shared mutable state. Message passing is one alternative, where we can have each call/action explicitly take in the handle, then pass it along (either directly, or via some sort of "trampoline", like an MVar). This way we get the "concurrent from the outside, single-threaded on the inside" behaviour of actor systems like Erlang. In particular, it's easy to make sure the handle only get passed along when we're 'finished' with it (i.e. we've written a complete "block" of output).

Re: Haskell's Missing Concurrency Basics (2016)

#22
post #12

Earlier quoted context omitted.

Haskell's being functional is pretty much irrelevant here. The process has one stdout. If functions that write to file handles don't acquire a lock, then the output of different threads will get mixed up.

It's easy to split hairs about what "being functional" means, but the way that Haskell implements IO is certainly a byproduct of this. In particular, my preferred mental model of Haskell doesn't include "functions that write to file handles"; but rather, these would be pure functions which return IO "actions". This distinction is often inconsequential, but this case seems to rely on how we combine those "actions" tog…

Monadic I/O (as I'm sure you know) just means that every I/O effect takes a world-state as an implicit parameter ("the world just before this action"), and returns a world-state ("the world just after this action") to thread into the next effect. In a concurrent program, I/O effects from multiple threads (and the outside world) may be interleaved or executed concurrently. Crudely, the world you changed a moment ago isn't necessarily the world you're about to change again. :)

Monadic I/O on its own doesn't make any guarantees, or impose any requirements, about locking external resources. If stdout is locked (from within the monad, or not), then that's the state the world arrives in for your effect. If not, then it's not. They are orthogonal concerns.

Re: Haskell's Missing Concurrency Basics (2016)

#23
post #20

Earlier quoted context omitted.

It's easy to split hairs about what "being functional" means, but the way that Haskell implements IO is certainly a byproduct of this. In particular, my preferred mental model of Haskell doesn't include "functions that write to file handles"; but rather, these would be pure functions which return IO "actions". This distinction is often inconsequential, but this case seems to rely on how we combine those "actions" tog…

I just don't see how monadic IO is relevant to this issue. putStrLn either acquires a lock or it doesn't. As it happens, it doesn't. It just as easily could (without any change in its type). The same issue arises in any language that supports threads.

> I just don't see how monadic IO is relevant to this issue

Monad is effectively Haskell's definition of time: `A >> B` means A followed by B, `B >> A` means B followed by A. Yet Monad can't express concurrency; for that we need something else (e.g. Arrow).

> putStrLn either acquires a lock or it doesn't

It's not that simple: there's no point aquiring a lock after the text has been written; or relinquishing a lock before it's been aquired. To specify what we want, we need some way to sequence actions, and that's exactly what Monad is, i.e. we want `putStrLn` to be:

    getLock >> writeTheString >> freeLock
Then we need to consider the sequencing that's going on in the middle, since we need the first character of our string (e.g. "foo") to appear before the second, and so on. Hence we actually get:

    getLock >> putChar 'f' >> putChar 'o' >> putChar 'o' >> putChar '\n' >> freeLock
Hence we need Monad (or at least the definition of >> for IO) to orchestrate a single sequence of actions. I find it a little unsatisfying that to orchestrate multiple sequences we might just defer to some complicated parts of the runtime system like "locks" and "threads", written in C :(

As an alternative, we might imagine an "execute concurrently" combinator like `` to complement `>>`, where `A B` evaluates non-deterministically to either `A >> B` or `B >> A`. Yet this isn't particularly good, since `(A >> B) (C >> D)` can only evaluate to either `A >> B >> C >> D` or `C >> D >> A >> B`; there's no possibility for traces like `A >> C >> B >> D`, so we lose a lot of the reason to use concurrency.

One way to fix this is to define another way of sequencing, e.g. `A ->> B`, which behaves like `A >> B`. We can then pick one of these, e.g. `->>`, to interact with ``, such that:

    (A ->> B)  (C ->> D)
will evalutate non-deterministically to:

    A >> (B  (C ->> D))
or:

    C >> ((A ->> B)  D)
With a setup like this, we can then reframe the problem with `putStrLn` by saying that `putStrLn "foo"` evaluates to:

    putChar 'f' ->> putChar 'o' ->> putChar 'o' ->> putChar '\n'
Whilst we want it to evaluate to:

    putChar 'f' >> putChar 'o' >> putChar 'o' >> putChar '\n'
From this perspective, the problem is absolutely about monadic IO :)

Can we implement `` with threads and `>>` with locks? Sure; but we can also think about these things in a "more Haskelly" way, with combinators, equational laws, etc.

Re: Haskell's Missing Concurrency Basics (2016)

#24

Earlier quoted context omitted.

It's easy to split hairs about what "being functional" means, but the way that Haskell implements IO is certainly a byproduct of this. In particular, my preferred mental model of Haskell doesn't include "functions that write to file handles"; but rather, these would be pure functions which return IO "actions". This distinction is often inconsequential, but this case seems to rely on how we combine those "actions" tog…

Monadic I/O (as I'm sure you know) just means that every I/O effect takes a world-state as an implicit parameter ("the world just before this action"), and returns a world-state ("the world just after this action") to thread into the next effect. In a concurrent program, I/O effects from multiple threads (and the outside world) may be interleaved or executed concurrently. Crudely, the world you changed a moment ago i…

I get what you're saying, but I don't like to think of these as orthogonal: locking is a way to force actions to occur in a particular order, so is monadic IO (the 'world-state' is a dummy data dependency, preventing later actions from getting called before earlier ones; it doesn't "really" contain the whole state of the world ;) )

Re: Haskell's Missing Concurrency Basics (2016)

#25

Earlier quoted context omitted.

Monadic I/O (as I'm sure you know) just means that every I/O effect takes a world-state as an implicit parameter ("the world just before this action"), and returns a world-state ("the world just after this action") to thread into the next effect. In a concurrent program, I/O effects from multiple threads (and the outside world) may be interleaved or executed concurrently. Crudely, the world you changed a moment ago i…

I get what you're saying, but I don't like to think of these as orthogonal: locking is a way to force actions to occur in a particular order, so is monadic IO (the 'world-state' is a dummy data dependency, preventing later actions from getting called before earlier ones; it doesn't "really" contain the whole state of the world ;) )

I get what you're saying too. :) I think one reasonable semantics for monadic I/O is that it sequences effects in a single thread; and effects from other threads are part of the world-state, not part of the monadic context. Another reasonable semantics is what I think you're describing: monadic I/O in a multithreaded program should sequence effects across all the threads (e.g., mutexes on shared resources). It would be nice to have both options available -- maybe similar to how you can plug a strategy into Haskell's Control.Parallel.Strategies monad.

https://hackage.haskell.org/package/parallel-3.2.1.1/docs/Co...

Re: Haskell's Missing Concurrency Basics (2016)

#26
post #20

Earlier quoted context omitted.

I just don't see how monadic IO is relevant to this issue. putStrLn either acquires a lock or it doesn't. As it happens, it doesn't. It just as easily could (without any change in its type). The same issue arises in any language that supports threads.

> I just don't see how monadic IO is relevant to this issue Monad is effectively Haskell's definition of time: `A >> B` means A followed by B, `B >> A` means B followed by A. Yet Monad can't express concurrency; for that we need something else (e.g. Arrow). > putStrLn either acquires a lock or it doesn't It's not that simple: there's no point aquiring a lock after the text has been written; or relinquishing a lock be…

>I find it a little unsatisfying that to orchestrate multiple sequences we might just defer to some complicated parts of the runtime system like "locks" and "threads", written in C :(

Ok, sure, but that's how IO Haskell in Haskell actually works. There's arguably a principled reason for that. The IO monad is there to solve a problem that's particular to lazy languages: that it's difficult to reason about the order of evaluation and hence of side effects. That's conceptually completely different from the problems raised by concurrency, which are pretty much the same regardless of whether your programming language uses strict evaluation or lazy evaluation. Fundamentally, there is no sane way for >> to guarantee that no other thread within the process is competing for the same resource. >> can't know about every resource out there. Its instance for (IO a) can, however, make real guarantees about evaluation order, without knowing anything about what various IO actions are actually doing.

One can come up with all sorts of schemes for concurrency combinators (and Haskell has no monopoly on defining combinator libraries), but in the end, to avoid two threads writing to stdout at the same time, there is going to have to be some code somewhere that acquires a lock on a file handle. That code has to live in the relevant library functions, not in the implementations of the combinators.

Re: Haskell's Missing Concurrency Basics (2016)

#27
post #7

Earlier quoted context omitted.

Correct. Erlang (and therefore Elixir) stdio is implemented by means of sending messages to a process that does low-level writing to a console. When a single message is served, the other are queued up. Therefore, when two processes (let's name them A and B) want to output something at the same time, the result is going to be AAAABBBB or BBBBAAAA (depending on the order in which the messages arrive in the mailbox), bu…

...where "AAAA" and "BBBB" are two distinct messages, and not two sets of four messages each ("A", "A", "A", "A"). When I first read your example, it sounded like you were saying that the IO process would exhaust all messages from one source process before processing any messages from the other, no matter what order they arrived in.

Yes, correct. I wasn't clear enough.
Post reply on HN