7 ms·
Concurrency in Haskell: Fast, Simple, Correct
- haskell17373 2y agoIt'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 2y agoA 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 2y agoQuibble: Both Clojure and Scala have a Software Transactional Memory implementation, and the original Clojure Ant demo showed this.
- kreetx 2y agoWhile the async library is great, then everything that came before (forkIO, MVar's, etc) was already simple enough - it's only the exception handling that was missing.
- mikojan 2y agoIn another life I will be a Haskell programmer
- Avicebron 2y agoYeah 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 2y agoWhy not python ?
- linguae 2y agoI’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.
- rizky05 2y ago[dead]
- kqr 2y agoYeah, 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!
- xboxnolifes 2y agoThey really aren't anything alike at all.
- tasuki 2y agoNo 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 2y agoThe 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.
- whateveracct 2y agoIt's a lot of fun!
- runeks 2y agoI fear that in another life I will be a JavaScript programmer
- kreetx 2y agoChoose this life instead!
- cosmic_quanta 2y agoThere's no time like the present. Feel free to reach out if I can help you along your journey
- tasuki 2y agoNot 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 2y agoI'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/ https://haskellbook.com/
- ilrwbwrkhv 2y agoRust has a bunch of these while being maintainable.
- whateveracct 2y agoWhat 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 2y agoSTM is Haskell's feature for safe shared&mutable state. What is Rust's feature for safe shared&mutable state?
- speed_spread 2y agoThat would be Arc<Mutex<T>>, which works but is no STM.
- Philpax 2y agoAdditionally, the actor model is pretty easy to implement: https://ryhl.io/blog/actors-with-tokio/ https://ryhl.io/blog/actors-with-tokio/
- zozbot234 2y agoRelevant: https://github.com/Marthog/rust-stm 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 2y agoIsn'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]
- FuckButtons 2y agoI 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 2y agoNo, 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 2y agohttps://hackage.haskell.org/package/stm-containers 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 2y ago> 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?
- pkjens04 2y agoA mutex locks the thread, which doesn't happen with TVar. For mutexes TMVar would be used.
- jes5199 2y agoI love Haskell because I can write provably correct code that still doesn’t work
- declan_roberts 2y agoI 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 2y agoNow you would vibe it and ship it during the ol' drink.
- TacticalCoder 2y ago[dead]
- kqr 2y agoIf 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 2y agoA Haskell quote I like is: “I’ve only proven this correct, I haven’t tried it.”
- neilwilson 2y agoIsn’t that one of Dijkstra’s supposed comments?
- OskarS 2y agoIt'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 https://www-cs-faculty.stanford.edu/~knuth/faq.html
- michalsustr 2y agoI’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.
- nesarkvechnep 2y agoYou obviously haven’t ran anything on the BEAM (Erlang’s VM).
- michalsustr 2y agoCorrect. Erlang also uses green threads?
- jlouis 2y agoYes. 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 2y agoWeb 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.
- internet_points 2y agohttps://www.oreilly.com/library/view/parallel-and-concurrent/9781449335939/ https://www.oreilly.com/library/view/parallel-and-concurrent... is a great resource for those who want to go deeper into this
- alatriste 2y agoI 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 2y agoThe fundamentals are the same, and `async` is as relevant as it was back then. The ecosystem is extremely stable in that regard.
- cosmic_quanta 2y agoThe author is thinking of updating the book to a second edition as well. Looking forward to it
- ahel 2y agonoice
- thih9 2y agoAfter 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 2y agoI 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 2y agoThe 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 2y agoIt's because ``asyncio`` is a dogwater library that's barely functional and full of footguns. The ecosystem is about the same quality too.
- gpderetta 2y agoIndeed, as much as I dislike async in general, asyncio is its own special hell.
- _jackdk_ 2y agoFrom 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/2016/07/asynch-exns.pdf 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 2y agoOh so this could be the inspiration on Virtual Threads and Structured Concurrency in Java
- cosmic_quanta 2y agoMy 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-your-function/ https://journal.stuffwithstuff.com/2015/02/01/what-color-is-...
- grandempire 2y agoThis is also an advantage of blocking code. It’s just regular code. The async stuff is handled by the operating system.
- mrkeen 2y agoColoured 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.
- UlisesAC4 2y agoIt is a shame that the people you are answering is being downvoted, I also understand the importance of coloring functions, but look at the examples that person put, python and rust. In those, executing a colored function (at least the async related ones) propagates up to the top of the program, that is a cost that we have to interiorize, but I would be lying if I told you I wouldn't he happy with such behavior. I do a lot of js/ts and I would love to just be able to "inline" await without making my current scope recursively to the top of the program like it can be done with F# with the Async.StartAsTask operation.
- kookamamie 2y ago> 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 2y agoSIMD 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 2y agoAgreed 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.
- SleepyMyroslav 2y agoI would say that Highway [1] comes close. Can't say anything about ISPC because in gamedev work it never even came into consideration for multiple platforms. 1. https://google.github.io/highway/en/master/ https://google.github.io/highway/en/master/
- thuanao 2y agoCorrect? 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 2y agoFrom 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 2y ago"Fast" in title. But I don't see any benchmarks, or discussion on how to reason about the performance of STM code.