8 ms·
Now what happens when you add STM to these languages? You've just created an enormous footgun by creating another color of function (https://journal.stuffwithst
by AaronFriel 6y ago
Now what happens when you add STM to these languages? You've just created an enormous footgun by creating another color of function (https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/ https://journal.stuffwithstuff.com/2015/02/01/what-color-is-..., see also: https://news.ycombinator.com/item?id=8984648 https://news.ycombinator.com/item?id=8984648).
What this proposal misses is that reasoning about software transactional memory in functions all but demands being able to abort and retry code to make progress, and that therefore means having a high level separation between transactional code and IO code.
Suppose you had a banking application, you would like to use STM (as in the "ractor" example in this article) to move balances from one to another. In Haskell, you'd perform STM actions on those TVars (transactional vars) and use the default retry policy of STM to automatically construct an order of operations such that each operation appears to occur atomically. (STM doesn't impose a clock based ordering but I don't think it would be tremendously difficult to do so.)
If you don't use Haskell's "check" or "retry", you find that your code will abort and fail with near certainty. You can't perform lots of transactions on tvars and not, occasionally, read from a var another transaction is writing.
So you add "check" calls to verify your preconditions and postconditions such as balances cannot go negative, and you add "retry" calls to automatically retry transactions that fail because of concurrency.
Now imagine doing that in a modern Ruby codebase, or a modern JavaScript codebase. There's so much mutation that happens in these environments, can you guarantee within a Ruby function that it hasn't mutated another variable with ease? That it hasn't written to disk, made an API call, or changed some global state?
Haskell pioneered the use of STM. It's easier to reason about in Haskell because the world of the STM can be isolated from the world of effects due to the IO monad. It's easy to declare "this function is blue" and "this function is red" and the never the twain shall meet.
In any other language, all bets are off.
- thaumasiotes 6y ago> That’s why C# has that little caveat. You can avoid the pain of async in C# by using threads. This is the first time I've seen threads referred to as a way to avoid pain.
- taffer 6y agoThreads are fine as long as they do not share mutable data structures. For example, multithreaded application servers are a rather painless experience.
- mumblemumble 6y agoThat observation doesn't really contribute to a comparison of dedicated threads and async/await, though, because the same is true of async routines.
- eyelidlessness 6y agoSince all of these concurrency approaches’ pain points are around shared mutable state, maybe it’s shared mutable state that’s the pain point.
- bglusman 6y agoYup, this is the conclusion of Joe Armstrong and why Erlang/Elixir/BEAM just doesn't allow mutable state, other than process dictionary which is inherently non-shared.
- deleted 6y ago[deleted]
- ancarda 6y agoI honestly prefer threads and mutex locks to async/async. Feel free to burn me at the stake.
- formerly_proven 6y agoWhether async or threads are preferable depends a lot on context and often the answer will be "both". E.g. GUI code -> async, because you must do everything on the UI thread. Heavy lifting in a GUI app -> threads. Traditionally you'd do this with a huge mess of callbacks, but these days it's reasonably easy to just write the GUI portion using async and have background computations run in threads signal their results through futures into the async code, which makes everything so much easier.
- jpcooper 6y agoThere’s no guarantee that you’re not calling unsafePerformIO in Haskell, which a lot of outwardly pure libraries do. In fact, wouldn’t STM be doing this under the hood? Even the FFI in Haskell allows you to import a function without the return type being in IO as long as you pinky promise that it is pure. The idea in the end is to provide a language for working on memory concurrently. You either choose speak it or not.
- deleted 6y ago[deleted]
- kpmah 6y agoAs with Rust's 'unsafe', the fact that you can explicitly circumvent checks doesn't make those checks useless. If something goes wrong you know exactly where to look.
- jpcooper 6y agoThat may be the case for Rust. I am not sure what the implications of unsafe are for the type system. In any case, the unsafeness of unsafePerformIO and importing an FFI function as a pure function are not reflected in the type in Haskell. These are things that can and do happen. My point was that the ability to go outside the bounds of a language does not necessarily make the language useless.
- a1369209993 6y ago> I am not sure what the implications of unsafe are for the type system. There are no implications, that's the point. > the unsafeness of unsafePerformIO and importing an FFI function as a pure function are not reflected in the type in Haskell Yes, that is correct behaviour; if it was reflected in the type it'd be call safePerformIO.
- nextaccountic 6y ago> In any case, the unsafeness of unsafePerformIO and importing an FFI function as a pure function are not reflected in the type in Haskell. It's reflected in safe Haskell annotations. Unfortunately nobody cares about those. Haskell has a culture problem regarding safety, just like most other languages, both high level and low level (with the exception of Java, that don't make data races UB)
- chrisseaton 6y agoIt’d be good if all writes in a transaction were automatically transactional and we had no TVars, and also if all IO automatically made a transaction irrevocable. But I’m not sure this is tractable in either implementation effort or runtime overhead.
- marcosdumay 6y agoAre you talking about the Haskell one? If so, you can't do IO inside a transaction (it won't compile), and any value not on a TVar is thread-local.
- chrisseaton 6y ago> Are you talking about the Haskell one? No? I'm responding to the idea of the 'colour' of functions and suggesting how I think it should be in Ruby - all side effects transactional, but IO irrevocable. Drop the idea of TVar and make all mutable locations transactional.
- cat199 6y ago> In any other language, all bets are off. No, in any other language, one has to have an understanding of the codebase and what is mutating and what is not instead of relying on the language to do this work for them, which, granted, can be a lot more work. dismissing things because 'i dont want to deal with it because it is hard' is not the same as dismissing it because 'this is not possible', this argument is the former. see also: ocaml / scheme style FP, which can be 99.9% as pure-functional and type-checked as haskell if one wants to build their system that way.
- marcosdumay 6y agoWell, if you don't like the wording, translate "all bets are off" into "the programmer(s) must never make a mistake on those 10k to 10M lines on the code". That's not a reasonable thing to expect.
- rurban 6y agoNot only bets are off in non-functional language, it's the wrong solution. STM puts the cost of locking into everything, ie slower default operations, whilst still being concurrency unsafe. Deadlocks can occur. There are proven good solutions for fast and safe concurrency with ownership tracking and a good compiler/scheduler. No, not Rust. And not STM. We fought with this for years.
- rrss 6y ago> There are proven good solutions for fast and safe concurrency with ownership tracking and a good compiler/scheduler. Do you have any pointers to these solutions for the uninitiated?
- rurban 6y agoAll concurrency-safe languages. Eg pony, concurrent pascal, the parrot vm, ...
- amelius 6y agoEh, STM is often used as a solution to avoid locking. https://en.wikipedia.org/wiki/Software_transactional_memory https://en.wikipedia.org/wiki/Software_transactional_memory
- brandmeyer 6y agoIf you read into the implementation papers a little more deeply, you'll find that they are typically implemented with locks under the covers.
- orthoxerox 6y agoHaskell should just have transactional IO, then. RDBMSs have had it since god knows how long ago.
- AaronFriel 6y agoHaskell is a general purpose programming language, so I'm not sure what you mean. You can't in the general case have "transactional IO".
- a1369209993 6y agoHonestly the main feature of Haskell in this regard isn't STM-colored functions (as you note, that's a bad thing), but rather that it almost supports color-generic functions in the form of `Monad f => A -> f B -> f C`. Unfortunately, it doesn't correctly handle the identity monad `type Id a = a`, instead requiring `newtype Id a = Id a`, so it's impossible to write a function that matches both (eg) `A -> B` and `Monad f => A -> f B`.
- kccqzy 6y agoI don't understand your argument. It is correct that the standard library provides you with a newtype-based identity monad. But as you have said, you can also write own `type Id a = a`. What's wrong with that?
- a1369209993 6y ago> What's wrong with [`type Id = a`]? The fact that it doesn't work: > - The type synonym 'Id' should have 1 argument, but has been given none > - In the instance declaration for 'Monad Id' > | instance Monad Id where
- kccqzy 6y agoOf course that won't work because that type synonym is far from a monad. I think you meant to write `type Id a = a` or `type Id f a = f a` instead. But I get your argument now. What you are saying is that we want some kind of a way to define functions that are either pure or return a monadic value. Currently that's not doable because these two have distinct types. It may be difficult to unify them via the type system. This is even more difficult for higher-order functions that take other functions.
- a1369209993 6y ago> I think you meant to write `type Id a = a` Indeed I did, sorry, unfortunately too late to edit. The following reproduces the error: {-# LANGUAGE TypeSynonymInstances #-} type Id a = a instance Monad Id where x >>= f = f x return = id (The last two lines can be ommitted, since GHC aborts before checking the instance body. Edit: which is why I didn't notice I had the arguments to >>= backward; fixed?) > It may be difficult to unify them via the type system. This is even more difficult for higher-order functions that take other functions. IMO, the main difficulty in unifying `a ~ Monad m => m a` is inferring `m ~ \a1-> a1` in a way that allows `\a1-> a1` to inherit `Id`'s instances. It's a understandable deficiency, but it is a deficiency.
- funcDropShadow 6y agoAnother reason why it is easier to integrate and use STM in Haskell than in most other languages, is the prevalence of immutable types. When only a tiny fraction of all memory structures needs to be tracked by STM as TVars, it becomes orders of magnitudes cheaper than in languages with predominantly mutable data structures.