6 ms·
I'm kind of surprised to hear that writing to stdout is a source of concurrency problems in a language that's considered to be functional. Surely if you can pa
by heavenlyhash 8y ago
I'm kind of surprised to hear that writing to stdout is a source of concurrency problems in a language that's considered to be functional.
Surely if you can pass your IO handles to all functions that need them, you can decide on a mutexing/buffering strategy at the top of your program, wrap the standard IO interface with a delegate that does so, and pass it on. Then, for all libraries called thereafter to use it consistently isn't just a no-brainer, it's an outright given, isn't it? There's no global (impure, non-functional) handle to stdout, is there?
- foldr 8y agoHaskell'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.
- nomel 8y agoMaybe I'm confused, but isn't this expected, desired, behavior?
- chriswarbo 8y agoIt'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" together (which, due to laziness, may happen far away from where/when the functions are called; see 'lazy IO'). For sequential IO we can combine things with Applicative and Monad, which gives us a definite order, but using these in a concurrent setting would cause too much synchronisation and determinism to be useful. I've not done enough concurrent Haskell to know how the various alternatives stack up; although I did play with Arrow many years ago, before it fell out of favour (seemingly for Profunctors?).
- foldr 8y agoI 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.
- chriswarbo 8y ago> 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.
- mitchty 8y agoBeing functional doesn't mean interaction with the outside world is going to lose its difficulty. Stdout being buffered basically means you have to sort out how to get consistent output just like in most other languages. Even if you do as you say, you can still bypass it by writing to stdout directly outside of your stable buffering mechanism. But at that point the language isn't to be blamed here.