this is a sketch of an idea, not proposal-level quality at this point, but I wanted to put it out there now because it's become relevant to multiple different threads
the basic idea is to add a builtin with an API like this:
Store.init! a => Store a
Store.read! : Store a => a
Store.write! : Store a, a => a
Store.transact! : Store a, (a -> (a, ret)) => ret
and then add a transact! method to structural records that works kinda like record builders:
{ a, b, c } = {
a: store1,
b: store2,
c: store3,
}.transact!(|{ a, b, c }| {
a: a + 1,
b: a + b,
c: !c,
})
this API has the property that all of these operations can be thread-safe, and none of them can deadlock (!)
(however, you can use them to create your own locks that can deadlock if you do the looping explicitly yourself)
(brb, kid stuff, then will explain more later :sweat_smile:)
a critical reason for it to have this property is that transact! only accepts pure functions, and all of these functions are effectful
in the case of the transact! that works on records, and involves multiple stores, the way it avoids deadlocks is that it runs the pure function on all the current values of those stores, but doesn't lock them - and then at the end checks if any of them have changed since it started running. if any did change, then its answer is based on stale data and the pure function gets rerun on the new data
since it's a pure function, it's harmless to rerun as many times as necessary
as usual, if you have lots of contention it'll be slow due to rerunning a bunch, but lots of contention always slows down concurrent programs
Clojure and Haskell both have STM primitives like this
Can it be pure with captures?
I assume so
yeah they'd all have refcounts over 1 for the lifetime of the store
one of the things we could use this for is that it would mean we wouldn't need the get! and set! primitives in tests
you could just use this directly for simulated implementations of effectful functions
some things I haven't thought through all the way:
transact! to detect if any of the values changed since it started? Doing a full equality check on them would probably be fine for small primitives like integers, but could be super expensive for larger values. For those, it would be cheaper to have some sort of flag or counter which gets changed when they change, but different variations of that design have other tradeoffs.One idea for change detection is a simple monotonic counter
yeah, although if it ever wraps, that's a huge problem :sweat_smile:
Wait, I've had someone I work with solve this problem
It's something about a byte array
But I'm struggling to remember
Oh, i'm wrong, I think what he did was track the function pointer of the last function to run on the state
Why is change detection needed?
I don't see any change related event api
I'm assuming to prevent race conditions
transact! in a multi-threaded scenario without deadlocks would need to check that the input is still valid
before it commits its transaction
I definitely don't follow. Wouldn't this be protected by an RWLock? Write and transact would both do grab the write lock. Read would grab the read lock
So you are saying instead of just checking for a change and then re-running the transaction, you just wait for the write lock?
Isn't that exactly a recipe for deadlocks?
No, totally deadlock free with a fair RWLock
Also, it depends on the cost of the update and how often there is contentions which is the best solution
Doing a looping transaction has no guarantee to ever resolve. A fair RWLock is guaranteed to resolve.
But what if the transaction function itself never terminates?
For example a fast set of write transaction may completely block a slow looping transaction. So looping with various size transactions is generally not safe.
Anthony Bullard said:
But what if the transaction function itself never terminates?
Then you have a bug and it deadlocks 100% of the time.
This is like asking what if a user program gets stuck in an infinite loop. It has nothing to do with the transaction and is a general problem of software.
I think with Clojure's STM primitives, one thread can block itself, but can't block the store itself
Sure, you could do that with a looping primitive, but the looping primitive is trivial to break fairness
And broken fairness may mean that certain classes of transactions will never resolve
I'm wrong, a deadlocked writer can't block readers, but it looks like it can block would-be writers to the same store
Screenshot 2024-12-21 at 7.15.28 PM.png
Oh, clojure is a lot more complex. And I don't think it's model will work on roc
It is really design to assume immutable data structures that are appended to. Roc does not have those.
They also look to break apart stores such that writers can interact with different parts of the store without blocking eachother or causing retries.
Also, I see how it is a bit different from a simple RWLock. The fundamental difference is that a reader can always read stale data and that is ok.
So a writer doesn't actually have to block readers at all.
This could be done in roc by using an atomic pointer swap after the write creates a new version of the data structure
This would have absolutely terrible performance with roc lists and strs. They would be duplicated in every single write transaction
Also, yeah, based on my reading of clojure, a stream of fast transactions could permanently block a single slow transaction. So it doesn't have fairness. Though maybe they have a trick to fix that that isn't obviously described
Oh right, I forgot we don't use persistent data structures
That said, stm seems to intentionally be quite optimistic in general preferring redoing work over contention
Sounds like some stm systems have priority to deal with slow transactions that would get blocked by a stream of fast transactions
Not exactly sure how it works though
Oh, and it definitely requires atomic refcounting in the data in the stm share, but maybe that was already a given.
that's pretty interesting - I hadn't heard of fair RWLocks before!
I'm not sure whether those would permit removing the restriction that the transaction function has to be sure :thinking:
or if that would reintroduce the possibility of deadlocks even when you're not looping
It's big problem is that it means way more contention. All reads have to be paused for a write to go through, but the write gets a unique view of the data and can mutate in place.
It is one writer or n readers active at a time
This probably is best for roc with large data in a transaction (due to avoiding copies), but very pessimistic for small data (copy is cheap) and is not great for high contention scenarios. Retries are probably cheaper in many cases.
I wonder if there is a form of transactional memory that works better without persistent data structures.
Actors :laughing:
But all access with actors would either be async or blocking. But this all depends on your concurrency model
Oh, so messages and each actor owns a unique copy of it's own data
Yeah, that would play nice with the roc data model
But that means that Actors are your concurrency model
Yeah, essentially coroutines and messaging queue with coroutines owning data. Essentially the go model.
But shared data has a ref count of 1 when the actor works with it
Obviously could do more explicit actors as well.
But a copy for each time the data is shared
I like both
Don’t communicate by sharing data, Share data by communicating
But my lang was to be a purely functional Actor language, so I have my biases
Anthony Bullard said:
But a copy for each time the data is shared
Only a copy if someone tries to mutate
Can just increase the refcount by default when sharing
Ooh, that’s right! Advantage to recounting which BEAM doesn’t use(from my memory)
That makes this model make even more sense!
Well,,,
Then the owner doesn’t have that 1 refcount anymore
This concept featured pretty heavily in Rich Hickey's earlier presentations about Clojure's multithreading story. In practice the community wound up not really using it and instead just use atoms and usually one big atom.
I'm sure they exist but I certainly never worked on a production system that used anything besides atoms and I can't recall anybody presenting about it at the handful of conj I went to or the NYC meetups over a couple years
so I guess the actor API would be something like:
Actor.init! : state, (state, msg => state) => Actor state msg
Actor.send! : Actor state msg, msg => {}
Is there any way to get information out of an actor?
not in that API :big_smile:
I guess there probably should be though? :sweat_smile:
I currently have a very shallow understanding of the actor model
it seems like the general idea is to sort of build your whole program out of actors, and then give them ways to talk to each other
in this design, I think that could be accomplished by the update function being given its own "address" so that it can send itself to other actors
something like this:
Actor.init! :
state,
(Actor state msg, state, msg => state)
=> Actor state msg
Actor.send! : Actor state msg, msg => {}
so then if one actor wants to ask another actor about some piece of information, it says "hey here is my address (my Actor value, which internally is some address integer) and I want to know this information...when you have the answer, send it to me"
and yeah, definitely a cool thing about that design is that state would always have a refcount of 1 in normal operation (unless you did something silly like put your state in an outgoing message)
so it would just get mutated in-place every update
I think you could build this using the Go-style channel primitive we talked about on another thread :thinking:
also, this doesn't seem like it would be much use for tests :sweat_smile:
Richard Feldman said:
I think you could build this using the Go-style channel primitive we talked about on another thread :thinking:
Yeah, it is basically coroutines and channels but with a tiny bit more organization and base structure.
Karl said:
This concept featured pretty heavily in Rich Hickey's earlier presentations about Clojure's multithreading story. In practice the community wound up not really using it and instead just use atoms and usually one big atom.
oh interesting - looking at the docs for Clojure atoms, they actually match the original API I posted at the top - except without the .transact! that works on records
the Store.transact! I posted at the top appears to be a generalized version of Clojure's swap! (generalized because instead of always returning the value that was stored in there previously, it gives you the option of returning something different if you like)
swap sounds the same as transact
But I guess it is a slightly different model
Still a spin loop over a persistent data structure and then only writing back if nothing has changed. This is done via a spin loop and an atomic swap
I think it will still have the same problems with our mutable or copy data structures.
But for our primitive types would be reasonable
I'm somewhat on the fence about this. It seems like if we want to enable a means to have something like mutable state in Roc, this is a very good way to do it. But whether that is a good thing to make easy is maybe not a good idea. I don't have much Ocaml experience, but when reading through Ayaz' cor experiment for lambda sets, the use of unannotated refs meant that I had to build a mental map of when state might "shift under my feet" instead of being able to read it for myself.
In Roc, we have ! with purity inference, so it'll be obvious that something effectful is happening, but it's not tracked what in particular is happening, in particular we don't know that Roc-local state is being mutated. I would hope that this PI-based "danger coloring" of Store usage would incentivize people towards immutability, but maybe like my recent threads on encouraging readable code, this isn't really a problem in practice.
In another language, I'd push harder that we really consider before introducing mutable state to a functional language, but Roc has removed ill-used/ill-received features in the past. So long as we are willing to do that for this as well, then I think experimenting isn't a bad thing.
To be clear, I think this change very likely good for Roc, but I think we should consider how and how much it might be exploited by people that other Roc devs need to engage with the code of.
If anyone has real experience with codebases that use something like Ocaml's ref, I'd love to hear about your opinion on how much that helps/harms the average codebase. My experience has been much more on the reading side than the writing side (basically no writing).
Sam Mohr said:
To be clear, I think this change very likely good for Roc, but I think we should consider how and how much it might be exploited by people that other Roc devs need to engage with the code of.
Think through the mis-use cases
Do I understand correctly, that with this feature, you could do something like this in basic-webserver?
app [Model, init!, respond!] { pf: platform "https://github.com/roc-lang/basic-webserver/releases/..." }
Model : Actor U64 {}
init! = \_ ->
Actor.init! 0 (\state, _ => state + 1) |> Ok
respond! = \req, model ->
Actor.send! model {}
count = Actor.read! model
Ok {status: 200, headers: [], body: Str.toUtf8 "Count: $(Num.toStr count)<br>"}
Update: I removed the try
That's probably a better wording, yes. I contorted my sentence that much because I'm trying to be particular here: this feature is powerful for individual devs, but would it help or harm someone trying to read their code?
@Oskar Hahn that looks like a viable usage to me, except you don't need to use try since the example API Richard gave was infallible.
What I don't understand is, how does this scale?
Currently, you can start multiple basic-webservers on different machines. The current usecase of Model in basic-webserver is to hold stuff like database connections. The current API does not allow you to change the Model. So basic-webserver can be scaled over multiple servers.
With the Actor, this is no longer possible. Roc would need some way to share the state between all instances.
Maybe the original version of the idea was more suited for scalability, since Store.transact uses a pure function that a platform could call on all instances.
A RWMutex is much simpler, but it only works on one machine.
There is the nice talk from @Richard Feldman Distributed Pure Functions
The idea, that as an application author, you don't have to care, if your code is scaled over multiple CPUs, multiple machines or multiple data centers is appealing. Is this still possible with a Store/Actor?
Sam Mohr said:
If anyone has real experience with codebases that use something like Ocaml's
ref, I'd love to hear about your opinion on how much that helps/harms the average codebase. My experience has been much more on the reading side than the writing side (basically no writing).
I do!
I love them. Seems like an excellent compromise between pragmatically allowing mutation where it's needed but also making it super obvious when it's happening and about the right amount of annoying.
refs have to be assigned with := and dereferenced with ! so they are a pain to use as normal variables... so they don't get used too often, which is good.
I have found them rare in ocaml codebases and I tend to use them rarely myself.
Mostly they are used internally when building data structures and such so users aren't readily exposed to them.
I don't know OCaml's refs, but I do have some experience with these kinds of 'mutable variables using IO' APIs in Haskell. There's a select set of circumstances where these can be useful, and I don't have the sense they're overused outside of that.
With regards to the 'how will this scale across multiple machines' question, I wonder if this is a trade-off that the platform can make. Some platforms might optimize ease of prototyping and offer an API like this and compromise on scalability. Another platform might optimize for scalability and not offer this API, compromising on convenience.
I'm glad to hear that both of you have had a good experience with ref, it sounds like existing languages have provided a good experience with it. I think a worry I hadn't been overtly considering here is that Roc has the potential to reach a lot of people, including lots of beginners. It definitely feels like macroeconomics trying to predict what will be a good decision in a few years. If people have good experience here, I'll take a bit of faith in our team's expertise that we aren't making a footgun for schmucks.
Oskar Hahn said:
The idea, that as an application author, you don't have to care, if your code is scaled over multiple CPUs, multiple machines or multiple data centers is appealing. Is this still possible with a Store/Actor?
That definitely is possible, but I don't think that is the goal here. Erlang is an actor system that seamlessly scales to many machines. I believe there is a rust library that can do the same as well.
My understanding was that this actor suggestion was more about suggesting a go style tooling for more concurrency and state based message passing power. It would be equally limited as go routines and channels are today. So single machine
Also, basic webserver model is not mutable cause we don't have atomic refcounting and we have not built a good API for mutating it. It really should be mutable.
General question:
Is any of this something that really should be built into the language? Most of this feels like it probably should be platform primitives.
Actors and message passing, a mutable long-lasting state store, these don't feel like they belong in roc. They feel like they belong in the platform.
Not as sure about ref, but my gut feeling is the same for that as well.
Oh, looking more at ref it is really unrelated to the rest of this conversation.
I have a lot of thoughts about this conversation, but I want to finish this bugfix before I weigh in. My response will probably be blog-post length :rofl:
It doesn't deal with synchronization or sharing accross threads safely which is the main focus of the rest of this conversation.
I think it probably should be moved to another thread
Brendan Hansknecht said:
Is any of this something that really should be built into the language? Most of this feels like it probably should be platform primitives.
Actors and message passing, a mutable long-lasting state store, these don't feel like they belong in roc. They feel like they belong in the platform
But I think I agree largely with this take. One of my goals in contributing to Roc is to work on a Actor platform
the original motivation for this thread is basically to have a builtin that can be used in tests for simulating effects, and which might also be useful in other situations such as the ones where ref in OCaml (or similar Haskell primitives that are thread safe) are used.
if we're going to have a primitive like that, it must not introduce data races into the language, which is why concurrency is in scope for discussing how it would work.
I agree that actors and channels shouldn't be builtins
If it's just for tests, cool. My gripes all come from its use in normal, production code
I would think if this is just for the test usecase, the simpler API (without transact!) might be better and then we don't have to worry about deadlocking at all
Tests can only ever be single threaded, so that does make the api requirements a lot simpler
I understood the original idea, that Store was something, that could be used in normal applications, not only in tests. I like the idea and hope, that there is a solution.
If the platform should be responsible to decide, how to handle the datarace, would it be possible to solve it like the memory-management, that a platform can define functions, that get called by roc? Like roc_rwmutexor something, that is more abstract? Then the platform can decide, if is just wants to support this on a single machine, or via network in a datacenter.
if it's just for tests then it wouldn't be a builtin. we have a separate design for that - basically just allow tests to be a lambda which receives functions to get and set some state:
expect |{ get!, set! }|
# test goes here
if we had a builtin that could do this, then we wouldn't need a separate thing for tests
I think it's an interesting idea to explore the use case of "I want this Roc program to be able to be distributed seamlessly across machines," but that's not what this particular thread is about, so I think it's worth starting a separate thread if we want to discuss that idea :big_smile:
I realized two problems with this idea:
expect |{ get!, set! }| idea does not have this problem, because the state-getting and state-setting functions only exist within each test, and don't exist outside the tests.so my conclusion about this idea is that it seems like the wrong direction. :big_smile:
I think we'll be totally fine with something like expect |get!, set!|. It wouldn't be that hard to make a community-driven package called something like test-constructs that you can build complex records with get! and set!. I can try putting something like that together in the near future to ensure it'd work, but with that, people could just import a File-like, or a Utc-like, etc.
We should also definitely explore @Anthony Bullard 's idea (I think it was his) for implementing a hosted module's function defs using get! and set!. If there was a way to ensure your test functions match get! and set!, then you could very easily run tests without making your effectful API a layer further mocked/abstracted
#ideas > Platform mocking in a PI world
To address 1., could it be an optional part of the platform? Like make a common STM interface that's managed by the platform, has the same interface for all platforms, and platforms can either provide their own (e.g. for hot code reloading) or use the default?
Some platforms may not want it, but i agree the interface should be shared amongst those that do
Last updated: Jun 16 2026 at 16:19 UTC