[HN Gopher] Concurrency in Haskell: Fast, Simple, Correct
       ___________________________________________________________________
        
       Concurrency in Haskell: Fast, Simple, Correct
        
       Author : ingve
       Score  : 181 points
       Date   : 2025-04-14 10:47 UTC (3 days ago)
        
 (HTM) web link (bitbashing.io)
 (TXT) w3m dump (bitbashing.io)
        
       | haskell17373 wrote:
       | It's maybe interesting to note that the `async` library in use
       | here is very simple and easy to understand. Nearly every function
       | is one or two lines. Likewise `TQueue` is extremely simple (and
       | easy to prove correct) thanks to STM, and also generally has good
       | performance.
        
         | zozbot234 wrote:
         | A lot of the complexity here is just hidden in Haskell's
         | runtime, which implements async processing based on green
         | threads, besides other features such as GC. Though to be fair,
         | the software transactional memory (STM) featureset is quite
         | unique to Haskell since it relies on the availability of pure
         | functions to ensure correctness. It's kind of hard to imagine a
         | full equivalent to it in other well-known languages.
        
           | juliangamble wrote:
           | Quibble: Both Clojure and Scala have a Software Transactional
           | Memory implementation, and the original Clojure Ant demo
           | showed this.
        
       | mikojan wrote:
       | In another life I will be a Haskell programmer
        
         | Avicebron wrote:
         | Yeah me too, I'll invest in bitcoin early, live like a hermit
         | off a coast somewhere, and school kids on HN, "cabal hell! I'll
         | scream, no, Conda hell with powershell hooks in vscode you
         | ingrates, my llm"
        
         | revskill wrote:
         | Why not python ?
        
           | linguae wrote:
           | I'm not the OP, but static types, Hindley-Milner type
           | inference, algebraic data types, and pattern matching can be
           | quite ergonomic. I have also come to appreciate functional
           | programming and how it makes reasoning about and testing code
           | easier.
        
             | kqr wrote:
             | Yeah, just last week I updated the numeric precision of a
             | value across an entire user journey in a big enterprise
             | application, spanning many functions and data types. Thanks
             | to Haskell, I could do it in a day.
             | 
             | In any other language I've used (barring maybe Ada) that is
             | a refactoring that would take at least days, if not weeks,
             | to manually track down all the places it interacts with the
             | system directly or indirectly, because mixing e.g. int with
             | long long is not a type error.
             | 
             | In Haskell, I change the type and the compiler spits out a
             | list of locations that needs to change. This is repeated
             | for a few iterations until all transitive interactions are
             | worked out. Done!
        
           | tasuki wrote:
           | No one _wants to_ be a python programmer. It 's a practical
           | language to get things done. It isn't a language to make you
           | feel proud of yourself nor about the current state of our
           | industry.
        
             | Philpax wrote:
             | The concept of "the Pythonic Way" indicates to me that
             | there are people who are proud of being Python programmers.
             | 
             | May God have mercy on their souls.
        
             | sgarland wrote:
             | I've enjoyed making it go faster by finding quirks, but at
             | this point it's mostly become "OK, what else can I offload
             | to C?"
             | 
             | I should really learn Rust. Or Zig. I tried Nim (best of
             | both worlds, Python-esque code that compiles to C!), but it
             | wasn't nearly as fast as my Python + C for my specific use
             | case.
        
               | knighthack wrote:
               | What exactly do you write, where your Python+C is faster
               | than Nim which compiles to optimized C?
        
               | sgarland wrote:
               | Generating millions of rows of synthetic data for testing
               | RDBMS.
               | 
               | Tbf, I didn't spend much time trying to optimize the Nim
               | code once I got a working PoC, so it's entirely possible
               | that I could've made it faster.
        
               | cb321 wrote:
               | You may no longer be interested in this kind of thing,
               | but if you are there might be some ideas of note over at
               | https://github.com/c-blake/nio/blob/main/db-bench.md (in
               | particular the demo/gbyGen.nim program).
        
         | whateveracct wrote:
         | It's a lot of fun!
        
         | runeks wrote:
         | I fear that in another life I will be a JavaScript programmer
        
         | kreetx wrote:
         | Choose this life instead!
        
         | cosmic_quanta wrote:
         | There's no time like the present. Feel free to reach out if I
         | can help you along your journey
        
           | tasuki wrote:
           | Not the person you're replying to, but I'll bite:
           | 
           | I've written low thousands of lines of Haskell. Similar to
           | mikojan, I love Haskell in theory, but ended up not enjoying
           | it as much in practice.
           | 
           | 1. The multitude of string-y types. I end up converting
           | between String, Text, Lazy Text, ByteString, Lazy ByteString,
           | and I forget what else. Each library wants me to pass in a
           | specific string type, and each other library returns a
           | different string type. LLMs are good at this, also for a
           | while I had a ton of helper functions to convert between any
           | two string types. Still, perhaps there's a better way?
           | 
           | 2. The error messages. I come from Elm, so I'm spoiled. Yes,
           | I understand a language with HKTs will never have as nice
           | error messages. Yes, LLMs are pretty good at explaining GHC
           | error messages.
           | 
           | 3. The stdlib. Haskell gets a lot of credit for safety, but a
           | `head` blows up instead of returning a `Maybe`. I know there
           | are other - safer - preludes, but I don't know how to choose
           | between them. I don't know how using a different prelude
           | would impact my projects.
           | 
           | I feel like my next step is either towards Idris, with its
           | polished standard library (and dependent types baked into the
           | language!), or towards something simpler and more Elm-like
           | (Gleam perhaps, or Roc). But if you can sell me on Haskell,
           | I'm all ears!
        
             | cosmic_quanta wrote:
             | I'm not going to sell you on anything. All of the things
             | you've mentioned are true. Loosely, the multitude of string
             | types and the state of the standard library come from the
             | same place: the language is 30+ years old! There are many
             | warts to be found.
             | 
             | However, if you decide to start learning, the path is hard,
             | especially if you come from a non-computer-science
             | background like me. I attempted to learn Haskell twice; I
             | bounced off the first time, quite hard, and didn't try
             | again for years.
             | 
             | What worked for me is a combination of two things:
             | 
             | * Having a goal in mind, that has nothing with the choice
             | of language. For me, it was building a personal website
             | 
             | * The book Haskell Programming from First Principles [0]
             | 
             | and if you have more questions, reach out.
             | 
             | [0]: https://haskellbook.com/
        
       | ilrwbwrkhv wrote:
       | Rust has a bunch of these while being maintainable.
        
         | whateveracct wrote:
         | What about Haskell concurrency isn't maintainable?
         | 
         | The concurrency stuff in the stdlib + the mainstays in the
         | ecosystem are pretty stable and noncontroversial..there's stuff
         | in Haskell that churn but this is not one of them.
        
         | mrkeen wrote:
         | STM is Haskell's feature for safe shared&mutable state.
         | 
         | What is Rust's feature for safe shared&mutable state?
        
           | speed_spread wrote:
           | That would be Arc<Mutex<T>>, which works but is no STM.
        
             | Philpax wrote:
             | Additionally, the actor model is pretty easy to implement:
             | https://ryhl.io/blog/actors-with-tokio/
        
           | zozbot234 wrote:
           | Relevant: https://github.com/Marthog/rust-stm which has usage
           | instructions. It's memory safe as defined in Safe Rust, but
           | unlike the Haskell implementation it's not "safe" in a
           | correctness sense, because Rust does not afford the same
           | control about mutability and purity. (At least, not yet -
           | future additions to the language may improve this somewhat.)
        
         | ykonstant wrote:
         | Isn't concurrency in Rust a notorious pain point? Or am I
         | confusing it with async which is different? [I am stuck in an
         | era before parallelism, so I don't really understand these
         | things]
        
           | mrkeen wrote:
           | This why I haven't fully embraced Rust yet. Whenever I ask
           | about safely mutating shared state (see sibling comment), I'm
           | met with silence, or some comment like: Rust guarantees that
           | you aren't mutating shared state.
        
             | sriram_malhar wrote:
             | I'm surprised you say this. The core type system guarantees
             | is that there is no aliasing while mutating, and no
             | mutation while it is aliased. You get single writer
             | multiple reader for free without overhead.
             | 
             | If you want multiple writers, you can always use the Arc
             | container and use the built-in lock.
        
               | mrkeen wrote:
               | >> or some comment like: Rust guarantees that you aren't
               | mutating shared state.
               | 
               | > The core type system guarantees that there is no
               | [sharing] while mutating, and no mutation while [sharing]
        
           | speed_spread wrote:
           | Correctness in concurrency is actually one of Rust's strong
           | suit. Any pain felt is because Rust is low level and does not
           | prescribe a single concurrency mechanism, leaving each coder
           | to figure out the benefits and constraints of each library.
        
       | FuckButtons wrote:
       | I thought it was a bit odd that the author claims there's no
       | mutexes in sight, the TVar is effectively a mutex guard unless
       | I'm misunderstanding this? (I've written exactly 0 lines of
       | Haskel). Or is the claim that the lack of ceremony and accidental
       | complexity around threading is the real win for concurrency here?
        
         | chongli wrote:
         | No, a TVar is not a mutex guard. A TVar is a software
         | transactional memory (STM) variable. STM works just like a
         | database: you batch together a sequence of operations into a
         | transaction and then execute them. During execution of a
         | transaction, all changes made to the contents of the TVar are
         | stored in a transaction log. If some other transaction occurs
         | during the execution then the whole thing is aborted and re-
         | run.
         | 
         | This can take any ordinary Haskell data structure and give you
         | a lock-free concurrent data structure with easy-to-use
         | transactional semantics. How it performs is another matter!
         | That depends on the amount of contention and the cost of re-
         | playing transactions.
        
           | whateveracct wrote:
           | https://hackage.haskell.org/package/stm-containers
           | 
           | This library is full of STM-oriented data structures. They
           | perform better than a simple `TVar (Map k v)`.
           | 
           | It's kind of a fun trick actually. The stock Map is just a
           | tree. The STM Map is also a tree [1] but with TVars at each
           | node. So this helps a lot with contention - you only contend
           | along a "spine" instead of across the whole tree, which is
           | O(log n).
           | 
           | [1] Technically a HAMT a la unordered-containers - trie,
           | tree, you get the idea :)
        
           | quibono wrote:
           | > How it performs is another matter!
           | 
           | I know you say it depends on how much contention one sees but
           | I'm interested in the performance hit. Also, is STM the
           | "standard" (or accepted) way to do async in Haskell?
        
         | dsign wrote:
         | You are correct, Haskell has quite a few mutex-like types. MVar
         | is one of them.
         | 
         | However, if memory serves me right, TVar is a building block
         | for the transactional memory subsystem. The guard on TVar with,
         | say, modifyTVar is not really stopping execution at entrance
         | but simply indicating that the block modifies the variable. In
         | my mental model, some magic happens in an STM block that checks
         | if two concurrent STM blocks acted upon the same data at the
         | same time, and if so, it reverts the computations of one of the
         | blocks and repeats them with new data.
         | 
         | To my knowledge, Haskell is the only programming language
         | (+runtime) that has a working transactional memory subsystem.
         | It has been in the language for about 20 years, and in that
         | time many have tried (and failed) to also implement STM.
        
           | dionian wrote:
           | https://zio.dev/reference/stm/
        
             | mrkeen wrote:
             | > Implication of Using STM Running I/O Inside STM-- There
             | is a strict boundary between the STM world and the ZIO
             | world. This boundary propagates even deeper because we are
             | not allowed to execute arbitrary effects in the STM
             | universe. Performing side effects and I/O operations inside
             | a transaction is problematic. In the STM the only effect
             | that exists is the STM itself. We cannot print something or
             | launch a missile inside a transaction as it will
             | nondeterministically get printed on every reties that
             | transaction does that.
             | 
             | Does Zio actually offer any protection here, or is it just
             | telling the reader that they're on their own and should be
             | wary of footguns?
        
               | dionian wrote:
               | I am not super familiar with it, great question - my
               | guess would be its the latter! Does Haskell's provide
               | protections?
        
               | hackingonempty wrote:
               | STM happens inside the STM monad while regular effects
               | happen in the ZIO monad. If you try to do ZIO effects
               | inside an STM transaction you'll get a type error.
               | 
               | Scala doesn't enforce purity like Haskell though so it
               | wont stop you if you call some normal Scala or Java code
               | with side effects. In practice its not a problem because
               | you're wrapping any effectful outside APIs before
               | introducing them into your code.
        
           | whateveracct wrote:
           | I think Clojure has some kind of STM too?
           | 
           | Haskell's STM is pretty world-class though. That's fair to
           | say :)
        
             | dwohnitmok wrote:
             | Clojure's STM never really took off because, for various
             | reasons, it's not as easy to compose as Haskell's (where
             | you can build up a big library of STM blocks and piece them
             | together at the very edges of your program). As such
             | Clojure's STM implementation doesn't actually have a great
             | reputation within the Clojure ecosystem where it isn't
             | usually used in most production codebases (whereas in
             | Haskell STM is often one of the first tools used in any
             | production codebase with concurrency).
             | 
             | Basically it's the difference between focusing only on
             | transactional variables without having a good way of
             | marking what is and isn't part of a larger transaction and
             | having a higher-order abstraction of an `STM` action that
             | clearly delineates what things are transactions and what
             | aren't.
        
               | pjmlp wrote:
               | You missed a very important detail, the language runtime.
               | 
               | While Haskell's runtime is designed for Haskell needs,
               | Clojure has to be happy with whatever JVM designers
               | considered relevant for Java the language, the same on
               | the other platforms targeted by Clojure.
               | 
               | This is yet another example of a platform being designed
               | for a language, and being a guest language on a platform.
        
               | dwohnitmok wrote:
               | I don't think this is a limitation of the JVM. When I've
               | used Clojure's STM implementation it's been perfectly
               | serviceable (barring the composability issues I
               | mentioned). Likewise when I've used the various STM
               | libraries in Scala. Eta (basically a Haskell
               | implementation on the JVM that unfortunately stalled in
               | development) also had a fine STM implementation.
               | 
               | It's more of a combination of API and language decisions
               | rather than the underlying JVM.
        
               | eduction wrote:
               | My impression at least watching chatter over the last
               | several years isn't that it has a bad reputation but
               | rather that people haven't found a need for it, atoms are
               | good enough for vast bulk of shared mutable state. Heck
               | even Datomic, an actual bona fide database, doesn't need
               | STM it's apparently all just an atom.
               | 
               | But I've never heard someone say it messed up in any way,
               | that it was buggy or hard to use or failed to deliver on
               | its promises.
        
               | robto wrote:
               | Clojure atoms use STM, though. I've been writing Clojure
               | for almost a decade now, it's not that STM isn't great,
               | it's just that immutable data will carry you a very long
               | way - you just don't need coordinated mutation except in
               | very narrow circumstances. In those circumstances STM is
               | great! I have no complaints. But it just doesn't come up
               | very often.
        
               | dwohnitmok wrote:
               | I would say to the contrary it would come up all the time
               | if the right idioms were in place.
               | 
               | For example, when it comes to concurrent access to a map
               | the Clojure community generally forces a dichotomy,
               | either stick a standard Clojure map in an atom and get
               | fully atomic semantics at the expense of serial write
               | performance or use a Java ConcurrentMap at the expense of
               | inter-key atomicity (or do a more gnarly atom around a
               | map itself containing atoms which gets quite messy quite
               | fast).
               | 
               | Such a stark tradeoff doesn't need to exist! In theory
               | STM gives you exactly the granularity you need where you
               | can access the keys that you need atomicity for and only
               | those keys together while allowing concurrent writes to
               | anything else that doesn't touch those keys (this is
               | exactly how e.g. the stm-containers library for Haskell
               | works that's linked elsewhere).
        
         | dwohnitmok wrote:
         | No a TVar isn't a mutex guard. As a sibling comment points out
         | it gives you transactional semantics similar to most relational
         | databases.
         | 
         | Here's an example in perhaps more familiar pseudocode.
         | var x = "y is greater than 0"       var y = 1
         | forkAndRun {() =>         y = y - 1         if (y <= 0) {
         | x = "y is less than or equal to 0"         }       }
         | forkAndRun {() =>         y = y + 1         if (y > 0) {
         | x = "y is greater than 0"         }       }
         | 
         | In the above example, it's perfectly possible, depending on how
         | the forked code blocks interact with each other, to end up with
         | x = "y is less than or equal to 0"       y = 1
         | 
         | because we have no guarantee of atomicity/transactionality in
         | what runs within the `forkAndRun` blocks.
         | 
         | The equivalent of what that Haskell code is doing is replacing
         | `var` with a new keyword `transactional_var` and introducing
         | another keyword `atomically` such that we can do
         | transactional_var x = "y is greater than 0"
         | transactional_var y = 1              forkAndRun {
         | atomically {() =>           y = y - 1           if (y <= 0) {
         | x = "y is less than or equal to 0"           }         }
         | }              forkAndRun {         atomically {() =>
         | y = y + 1           if (y > 0) {             x = "y is greater
         | than 0"           }         }       }
         | 
         | and never end up with a scenario where `x` and `y` disagree
         | with each other, because all their actions are done atomically
         | together and `x` and `y` are specifically marked so that in an
         | atomic block all changes to the variables either happen
         | together or are all rolled back together (and tried again),
         | just like in a database.
         | 
         | `transactional_var` is the equivalent of a `TVar` and
         | `atomically` is just `atommically`.
        
         | mrkeen wrote:
         | Mutexes lock _code_ , TVars lock _data_.
         | 
         | If you lock a section of code (to protect data), there's no
         | guarantee against mutations of that data from other sections of
         | code.
         | 
         | If you lock the data itself, you can freely pass it around and
         | anyone can operate on it concurrently (and reason about it as
         | if it were single-threaded).
         | 
         | It's the same approach as a transactional database, where you
         | share one gigantic bucket of mutable state with many callers,
         | yet no-one has to put acquire/release/synchronise into their
         | SQL statements.
        
         | ghusbands wrote:
         | As siblings note, TVar is a transactional variable. However,
         | it's not just protective against concurrent writes but also
         | against concurrent reads of altered variables, so it offers
         | true atomicity across any accessed state in a transaction.
         | 
         | So if you have a thread altering `foo` and checking that
         | `foo+bar` isn't greater than 5 and a thread altering `bar` and
         | checking the same, then it's guaranteed that `foo+bar` does not
         | exceed 5. Whereas if only write conflicts were detected (as is
         | default with most databases) then `foo+bar` could end up
         | greater than 5 through parallel changes.
        
       | jes5199 wrote:
       | I love Haskell because I can write provably correct code that
       | still doesn't work
        
         | declan_roberts wrote:
         | I love it because I can spend all my time noodling over types
         | and never ship a product that would have been great shipped in
         | a late night wine-fueled session of 1999 PHP.
        
           | anonzzzies wrote:
           | Now you would vibe it and ship it during the ol' drink.
        
         | kqr wrote:
         | If that is what you want to do, you can do that in any
         | language. It's just that when you do it in e.g. Java, you have
         | to spend a lot longer proving correctness before discovering
         | that it doesn't work.
        
         | jiggawatts wrote:
         | A Haskell quote I like is: "I've only proven this correct, I
         | haven't tried it."
        
           | neilwilson wrote:
           | Isn't that one of Dijkstra's supposed comments?
        
             | OskarS wrote:
             | It's Knuth! [1]
             | 
             | "Beware of bugs in the above code; I have only proved it
             | correct, not tried it."
             | 
             | [1]: https://www-cs-faculty.stanford.edu/~knuth/faq.html
        
               | jiggawatts wrote:
               | It's incredible that given how fuzzy and inaccurate human
               | memory is, we treat any LLM that can't perfectly recite
               | volumes of information as somehow _beneath_ us.
        
       | michalsustr wrote:
       | I'm not familiar with Haskell concurrency. The combination of
       | green threads and large memory allocations due to immutable data
       | structures sounds like it would be hard to implement a web server
       | handling 10k+ concurrent requests on commodity hardware?
       | 
       | Btw. too bad author talks about microsecond guarantees usage but
       | does not provide a link, that would be interesting reading.
        
         | stevan wrote:
         | > Warp is a high-performance HTTP server library written in
         | Haskell, a purely functional programming language. Both Yesod,
         | a web application framework, and mighty, an HTTP server, are
         | implemented over Warp. According to our throughput benchmark,
         | mighty provides performance on a par with nginx.
         | 
         | Source: https://aosabook.org/en/posa/warp.html
        
         | nesarkvechnep wrote:
         | You obviously haven't ran anything on the BEAM (Erlang's VM).
        
           | michalsustr wrote:
           | Correct. Erlang also uses green threads?
        
             | jlouis wrote:
             | Yes. And immutable data structures.
             | 
             | When data is immutable, it can be freely shared. Changes to
             | the data essentially uses copy-on-write. And it only writes
             | the delta change, since you don't need a deep copy due to
             | immutability. Add that the garbage collectors of Haskell
             | and Erlang are designed to work with a high allocation rate
             | and have 0 cost for dead data, and this is much faster than
             | what people think.
             | 
             | The way you implement a webserver in either Haskell or
             | Erlang is rather trivial. Whenever there's an incoming
             | request, you make a thread to handle it. So you don't have
             | 1 webserver serving 10k requests. You have 10k webservers
             | serving 1 request each. And since they are started from the
             | same core data, they'll share that due to immutability. See
             | also old-style Apache or PHP and fork().
        
               | eru wrote:
               | Web servers handling lots of small requests are actually
               | pretty easy to garbage collect to: you just delete all
               | the data at the end of the request.
               | 
               | Either you have a specialised GC that works like this, or
               | probably a good general generational GC can pick up on
               | this pattern on its own.
        
               | jlouis wrote:
               | Or you do as Erlang's BEAM VM: each thread has it's own
               | memory area which is GC'ed individually. This means upon
               | request termination, you just terminate the thread and
               | the memory is reclaimed with no need for a GC.
        
         | _jackdk_ wrote:
         | The interaction of laziness and purity means that the memory
         | costs are not always what you think. Purity means that it's a
         | lot safer to share structure between old and new versions of a
         | data structure where an imperative language would have to do
         | defensive copying, and laziness means that you can
         | incrementally amortise the cost of expensive rebalancing
         | operations (Okasaki is the standard reference for this).
        
         | cosmic_quanta wrote:
         | > sounds like it would be hard to implement a web server
         | handling 10k+ concurrent requests on commodity hardware?
         | 
         | In practice, it is not. The canonical Haskell compiler, GHC, is
         | excellent at transforming operations on immutable data, as
         | Haskell programs are written, into efficient mutations, at the
         | runtime level. Also, since web development is quite popular in
         | the Haskell community, lots of people have spent many hours
         | optimizing this precise use-case.
         | 
         | In my experience, the real downside is that compilation times
         | are a bit long -- the compiler is doing a LOT of work after
         | all.
        
           | eru wrote:
           | > The canonical Haskell compiler, GHC, is excellent at
           | transforming operations on immutable data, as Haskell
           | programs are written, into efficient mutations, at the
           | runtime level.
           | 
           | Yes, at the level of native machine code and memory cells,
           | there's not that much of a difference between immutability +
           | garbage collection, and higher level source code that
           | mutates. Thanks to GC you are going to overwrite the same
           | memory locations over and over again, too.
        
       | internet_points wrote:
       | https://www.oreilly.com/library/view/parallel-and-concurrent...
       | is a great resource for those who want to go deeper into this
        
         | alatriste wrote:
         | I read that book many years ago, but I haven't looked into
         | Haskell for a long time. Is it still relevant today? I imagine
         | many things have changed in 12 years!
        
           | valcron1000 wrote:
           | The fundamentals are the same, and `async` is as relevant as
           | it was back then. The ecosystem is extremely stable in that
           | regard.
        
         | cosmic_quanta wrote:
         | The author is thinking of updating the book to a second edition
         | as well. Looking forward to it
        
           | ahel wrote:
           | noice
        
       | thih9 wrote:
       | After reading the title I was expecting a "pick two". From my
       | anecdotal experience haskell is usually far from simple, but
       | other configurations are possible too.
        
       | wodenokoto wrote:
       | I don't know how async is in other languages but I find Pythons
       | async incredibly difficult to use, and I kinda feel validated
       | about how poor chatGPT is at it as well.
       | 
       | Is it because it is just a very hard thing, or is it because its
       | a synchronous language, with async bolted on? (I'm talking about
       | a purely language point of view, not from a python VM / GIL point
       | of view)
        
         | aeonik wrote:
         | The easiest language I've used for async is Clojure--mostly
         | because the language is immutable by default and ~99% of the
         | code is referentially transparent. That doesn't magically solve
         | async, but it removes an entire class of headaches by nudging
         | you away from shared state and side effects. You don't need
         | locks if there's nothing to lock.
         | 
         | Async is hard, no doubt--but some languages are designed to
         | reduce the surface area of what can go wrong. I've heard great
         | things about Erlang, Elixir, and BEAM-based languages in
         | general. They treat async not as an add-on, but as a core
         | architectural principle.
        
         | Starlevel004 wrote:
         | It's because ``asyncio`` is a dogwater library that's barely
         | functional and full of footguns. The ecosystem is about the
         | same quality too.
        
           | gpderetta wrote:
           | Indeed, as much as I dislike async in general, asyncio is its
           | own special hell.
        
       | _jackdk_ wrote:
       | From the footnotes:
       | 
       | > It gets weirder: in Haskell, exceptions can be thrown to other
       | threads!
       | 
       | What's really interesting is that because of purity, you have to
       | have asynchronous exceptions otherwise you give up a lot of
       | modularity. At least that's what Simons Marlow and Peyton Jones
       | argue in Asynchronous Exceptions in Haskell (2006):
       | https://www.microsoft.com/en-us/research/wp-content/uploads/...
       | 
       | > While the semi-asynchronous approach avoids breaking
       | synchronization abstractions, it is non-modular in that the
       | target code must be written to use the signalling mechanism.
       | Worse still (for us), the semi-asynchronous approach is simply
       | incompatible with a purely-functional language, such as
       | Concurrent Haskell. The problem is that polling a global flag is
       | not a functional operation, yet in a Concurrent Haskell program,
       | most of the time is spent in purely-functional code. On the other
       | hand, since there is absolutely no problem with abandoning a
       | purely-functional computation at any point, asynchronous
       | exceptions are safe in a functional setting. In short, in a
       | functional setting, fully-asynchronous exceptions are both
       | necessary and safe -- whereas in an imperative context fully-
       | asynchronous exceptions are not the only solution and are unsafe.
       | 
       | If you can read PLTese, it's really quite a nice paper.
        
       | kamalhm wrote:
       | Oh so this could be the inspiration on Virtual Threads and
       | Structured Concurrency in Java
        
       | cosmic_quanta wrote:
       | My favourite thing about Haskell concurrency is that there are no
       | colored functions [0]. Writing code in IO, or Async, or the next
       | big thing (asychronous higher-order effect system of the
       | future??), doesn't require language support like Python or Rust.
       | 
       | The one construct that unlocks this lack of colored functions,
       | STM, did require runtime support (as opposed to language
       | support), which at least is transparent to downstream developers
       | 
       | [0]: https://journal.stuffwithstuff.com/2015/02/01/what-color-
       | is-...
        
         | grandempire wrote:
         | This is also an advantage of blocking code. It's just regular
         | code. The async stuff is handled by the operating system.
        
         | mrkeen wrote:
         | Coloured functions are a feature - not a bug, Haskell is full
         | of them, and they are exactly what make STM safe in Haskell,
         | but abandonware in other languages which have tried.
         | 2. The way you call a function depends on its color.
         | 
         | `<-` or `>>=` vs `=`                 3. You can only call a red
         | function from within another red function.
         | 
         | This should sound pretty familiar! You can only call an IO
         | function from within another IO function. STM in this case
         | makes a third colour:                 IO can call IO functions.
         | IO can call STM functions. (*)       IO can call pure
         | functions.            STM can call STM functions.       STM can
         | call pure functions.            pure functions can call pure
         | functions.
         | 
         | (*) calling into an STM block from IO is what makes it 'happen
         | for real': it's the `atomically` which has type STM a -> IO a.
         | 
         | Having these coloured functions is what made STM achievable
         | back in the mid-late 2000s, since the mechanism to prevent STM
         | or pure functions from calling IO was already in-place.
         | 
         | Other languages either tried to figure out how to contain the
         | side-effects and gave up, or just released STM and put the onus
         | on the user not to use side effects.
        
       | kookamamie wrote:
       | > 1. Compose the program into several threads of execution,
       | traditionally scheduled and ran by the operating system
       | 
       | The step 0 is missing:
       | 
       | Compose the program into several lanes of execution,
       | traditionally executed via SIMD.
       | 
       | This is a massive piece of performance left on the table on
       | modern computer architectures, by assuming threading is the first
       | manifestation of concurrency.
        
         | jayd16 wrote:
         | SIMD has been somewhat of a massive failure in this regard.
         | Unlike threads, most languages seem to ignore its existence and
         | abdicate its usage to the sufficiently complex compiler.
         | 
         | I wish there was better author time feedback to the developer
         | on where they're getting such a perf boost. As far as I'm aware
         | there's no popular linting or blue squiggle to guide you in the
         | right direction.
         | 
         | In games it seems like the popular pattern is to rewrite
         | everything entirely in an entity component system framework.
        
           | kookamamie wrote:
           | Agreed completely. Most auto-vectorization approaches are
           | hit-miss and you still cannot have big-binaries, where
           | instruction set is decided dynamically, trivially.
           | 
           | ISPC comes close, but does come with a learning curve.
        
       | thuanao wrote:
       | Correct? Anyone who has worked with concurrency in Haskell is
       | probably laughing... :)
       | 
       | Haskell's IO type system doesn't model concurrency at all. `IO a`
       | could be a fork and join, an infinite event loop, really
       | anything, it's a black box in terms of "correctness". Better than
       | using JavaScript maybe, but hardly "correct" in any sort of
       | formal, tractable sense.
        
       | throwaway17_17 wrote:
       | From the footnotes:
       | 
       | > In bare-metal embedded systems or inside the operating system,
       | it's not unusual to manually break computation into state
       | machines, driven by interrupts.
       | 
       | Although not the topic of TFA, in fact, the footnotes that this
       | is "a whole different ball game." Does anyone have any good
       | source for this aspect of 'low-level'/OS development? I'm more
       | than capable of chasing down sources from a more high level
       | introduction or overview, so anything would be helpful. This
       | concept seems like it may just be a more pragmatic description of
       | embedded/OS development than what I've read previously.
        
       | ackfoobar wrote:
       | "Fast" in title. But I don't see any benchmarks, or discussion on
       | how to reason about the performance of STM code.
        
       ___________________________________________________________________
       (page generated 2025-04-17 23:01 UTC)