This is a placeholder for a larger thought that I'm having, but don't have time tonight to write out.
High level idea: Should there be a mechanism in Roc to allow you to provide a Pure Roc implementation of the platform you rely on for testing purposes? So that you can test without the platform or access to the filesystem / network etc.
Since all true side effects come from the platform
Basically all platform functions with a => type signature would crash the test command (maybe run?) when the callsite for such a function is encountered in the path if the mock is not provided (no idea right now how that would be done).
Currently, the thought is that we have two tools that compose to build a mock platform: stateful + effectful top-level expects, and module params
Meaning you can write something like this:
# in File.roc
module [readFile!]
readFile! : Str => Result (List U8) ReadErr
# in MockFile.roc
module [readFile!] { get!, set! }
readFile! : Str => Result (List U8) ReadErr
readFile! = \path ->
use get! and set! to pretend to read from a file
# in App.roc
app [main!] { ... }
expect \{ get!, set! } ->
import MockFile { get!, set! } as File
File.readFile! "test.txt" == Ok [1, 2, 3]
I'm not sure how well that would work at scale, but all the tools should be there
It seems preferable to me to use an "inline" solution that doesn't require importing a different platform for testing
I didn't know about those expect \{} -> declarations...
Is that in latest???
That's not a thing yet
Not even a GitHub issue
Just the latest non-thrown-out popular suggestion
Oh ok. Here I was hoping we could get labelled expects and test reporting, but this is awesome too
Do you like or dislike it?
To me it seems _really_ similar
To my idea
Since we haven't even planned working on it, it'd be interesting to hear if you think we can improve
I think the difference here is that we're providing a means to do what you're doing without needing a platform to run tests
In your example, the platform is there, and we crash at runtime if we try to run an "actual" effect
I was going to do something terrible like just have it be a module and then call roc test --mock pf=MockPlatform.roc or something similar
That could work
And expects stay the same
The hard thing though is Pure Roc doesn't let you maintain any sort of state
Unless those modules were a special type that allowed for top level var
I think any solution that:
Is a good sell
You're right that Pure Roc is stateless by design
I don't know if top-level var for just these mock platforms would be acceptable
Which is why this proposal introduces get! and set!, and they are the only place outside of a platform that you can maintain state, and they also only work in tests
You'd need a different type, and then make importing that into anything in the main.roc's dependency graph illegal
Anthony Bullard said:
I don't know if top-level var for just these mock platforms would be acceptable
I also think you may have a different idea of var than what we actually plan on writing. var in Roc just allows for re-assignment of a variable with the same name and type within a function, but everything is still immutable. No values are mutating
What are the signatures for get! and set!
That's fine to me
get! : {} => a
set! : a => {}
Yeah, forget top level var
And I'm assuming that means you'd have to always type annotate the bound value from get! for specialization? Well I guess maybe not...
The usage further down should get you the information you need
Roc's type inference will handle it
I think we could meld these two
Use my concept and then have get! and set! be functions in a Mock package in the builtins
Or be in the prelude for this type of file
Lots of small little design ideas to play around with
I'd say you _may_ want:
get! : Str => a
set! : Str, a => {}
Or similar and have the backing datastructure be a hashmap
But we should talk more about this soon
It's been on my mind. In my language Chakra events happened in exactly one way and were meant to be easily mockable - and inspectable. Basically using Elm's Msg concept
That doesn't work for Roc
I think it would be nice to make an ergonomic mechanism for this. The nice thing about get! and set! being just functions is that an external Roc library could provide stuff like buildMockFile! that takes get! and set!, and each platform wouldn't need to design it
Okay, interesting to think about!
I think get and set also have to take a key, right? Otherwise you couldn't get and set independently in separate places. You don't want all state period in every get and set. Probably can make a special function to generate a key that does it by source location so it is always unique or something like that.
Cause file read probably wants a different get and set data from UTC now from etc.
Brendan Hansknecht said:
I think get and set also have to take a key, right? Otherwise you couldn't get and set independently in separate places. You don't want all state period in every get and set. Probably can make a special function to generate a key that does it by source location so it is always unique or something like that.
That’s why this:
I'd say you _may_ want:
get! : Str => a
set! : Str, a => {}
Or similar and have the backing datastructure be a hashmap
I was assuming you'd need to do something like
expect \{ get!, set! } ->
getByName! = \name ->
get! {}
|> Dict.get name
|> Result.withDefault []
addByName! = \name, newVal ->
oldVals = get! {}
forName =
getByName! name
|> List.append newVal
set! (Dict.update oldVals name forName)
import UserStore { getByName!, addByName! } as US
US.validateUsers! {} == Ok {}
Which is a hassle, but we (or a third party lib) could make convenience functions
But then everything has to be the same type to fit into a dict and that is a hassle.
It doesn't have to be a Dict
It just is in this case
It could be that get! stores a { users : Dict Str User, emails : List Str }
I have a feeling that whatever we come up with to make Action-State more manageable could also be a perfect solution for this problem
I would push for
key! : {} => Key
get! : Key => a
set! : Key, a => {}
Where a call to key generates the key based on the location in the source code. Always returning the same key on repeated calls.
Then in each effect you can call key and store exactly the data you want for that one effect
You also can share a key between effects via closure captures if that is wanted
Feels the right level of power without collision due to string keys
Well, then get! can't have just one return type
Not sure how that'd work
I'm also not convinced that just get! and set! aren't enough if we provide convenience wrappers.
We could also have expect generate a different getter and setter for each value in the record
Actually maybe key should return the get and set function for that key, would make the most sense.
expect { users, emails } ->
currentUsers = users.get! {}
users.set! (Dict.update currentUsers "jodie" 123)
emails : { get! : {} => List Email, set! : List Email => {} }
This requires a default value for get! and set! to be inferred, UNLESS
Yes, that is way better and kinda what I was trying to work towards
Nice!
Sam Mohr said:
Something along the lines of:
State : { color : Color, count : U64, } StateAccess : { color : { get : State -> Color, set : State, Color -> State, }, count : { get : State -> U64, set : State, U64 -> State, } } (state, stateAccess) = { stateful! color: Red, count: 123, } colorComponent = Elem.translate state stateAccess.color
This was my concept for Action-State
Not specifically this
But something like this with a pure and an effectful version could work
My only question is if that will play nicely with nested get and set. Like if you want to offload a chunk of state to a library that has a handful of letters and setters itself.
a related idea I've been kicking around is offering a builtin like:
Store.init! a => Store a
Store.read! : Store a => a
Store.write! : Store a, a => a
That's why I bring up the Action-State thing here. I agree that nested state is the tricky thing
Richard Feldman said:
a related idea I've been kicking around is offering a builtin like:
Store.init! a => Store a Store.read! : Store a => a Store.write! : Store a, a => a
This makes a lot of sense inside tests! Not sure if we'd want to support outside of tests
But yes, it's basically Brendan's idea
it could also include this, for software transactional memory:
Store.transact! : Store a, (a -> (a, ret)) => ret
there's a bit of a rabbit hole when it comes to concurrency, deadlocks, and atomicity with something like this
but anyway, one use for it could be in tests, because it would be a builtin
Makes me want to re-read the Pony tutorial to see if there is something lockless we could manage
so you wouldn't need a test-specific thing
It would be easy to check if it was used outside of tests, so even if it's an always imported builtin for convenience it wouldn't be that bad if it's localized to builtins
so there basically have to be locks behind the scenes for read! and write! but deadlocks only start being a problem when you start wanting to do multiple reads and/or writes atomically
But once you enable state outside of tests, I think there'd be a whole part of the Roc ecosystem that composed stateful mechanisms
Not sure if that's terrible, but it would be a bifurcation in the making
The nice thing about Store in Roc is that the ! required suffix makes this a much more sane version of refs in Ocaml
the software transactional memory approach is interesting because what you can do is have that transact! above, and then also offer some sort of record-builder-like syntax for doing a bigger transaction, e.g. (I'm making up syntax here as an example)
{
foo: store1,
bar: store2,
baz: store3
}.transact!(\{ foo, bar, baz } ->
{ foo: foo + 1, bar : bar + foo, baz: !baz }
)
the really interesting thing about the software transactional memory approach (Clojure and Haskell offer this) is that you can rule out deadlocks, but you have to have the rule that the transaction callback function has to be pure
and basically what they do is they run the function, get the answer, go grab all the locks, and see if anything changed since they started the transaction.
if anything changed, or if any of the locks can't be obtained because something else is using the lock, then they just give up, release all the locks, and try again automatically
and because it's a pure function, it's safe to re-run it as many times as necessary no matter how many times it fails
and if this transaction is the only way to have locks on multiple things at once, then there's no possibility of deadlocks occurring anywhere, because the transaction never waits on an individual lock; if it can't have the lock, it immediately gives up and releases all the other locks it was holding
What if the platform has its own lock?
anyway, so it's a combination of an interesting concurrency primitive and also a potentially nice way to address tests
the idea is that the Store API does not offer a direct lock! primitive operation
so read! and write! use locks behind the scenes, and so does transact!
but given the way all of those use locks, it's not possible to end up in a state where two threads are waiting on locks for each other
Yep, makes sense
because read! and write! can only wait on a lock when they're in a state where they have no locks, and transact! never waits on locks while holding any others
There's not a way that the platform could wait on something in the transact! because it's pure
oh yeah that's also important
since read! and write! and transact! are all effectful, you can't run any of them from within the pure function that transact! offers
This would help with the nested state problem, since a lot of state management wouldn't have to stem from the same parent get! and set!
Bit of a shame that this doesn't quite handle Action-State, unless that were to use Store as well...
I haven't really thought about it in that context
We'd probably want a specific effect type tracking in the type system if we went with that API, lest we allow for insanity in each component
Managing nested state is always a hard problem in FP, which is why half-functional frontend frameworks always make that the biggest cheating point on functional practices
I have thoughts on the topic of UI state haha
In your own time, then!
It's my current white whale apparently
It feels like there's something obvious I'm missing, because in theory it's so simple
I guess the short version is that you can pick between:
in theory it would be nice to be able to pick a hypothetical option of "we know what's going on and have no wiring annoyances" but I'm not aware of a way to get both in the same state management system; it seems like you have to pick one of the two options above
and I prefer the first option, because wiring is easy to write, and figuring out what's going on with a complicated UI state is far from easy and I want all the help I can get with that
It seems like the stack approach that Solid.js kinda goes for is a really good in-between: within a function/code block, you track what state was introduces by having a stack in the background for that function. Each stateful value gets a ref that gets written to
If you can find a way to automatically convert that stack (via some syntax or compiler black magic) into a tuple or struct, then you know what state is being managed, but you didn't need to "box it up" yourself
I'm trying to figure out a way to build that heterogeneously-typed stack in an ergonomic way
One way is with backpassing
Another is with State.store!
A third is with
{ stateful!
users,
emails,
}
-- which goes to
{
users : { get, set },
emails : { get, set },
}
I am quite convinced that this is what the solution will look like, I just haven't yet found an in-between that is:
A syntax like 'users that creates a lens fixes the first two, but sucks for teaching
If we bit the effect bullet and tracked two kinds of effectfulness, statefulness and purity, then we could do all three
But that's a barrel of worms
hm, I'm not sure if we're talking about the same thing
I'm more talking about UI state than application state
Something like
-- exclam means impure
writeFile! : Str, List U8 => {}
-- at means stateful
@saveUser : Str -> {}
oh I see
so you're talking about like in an event handler saying "hey go change this piece of application state"
I'm trying to find a solution for UI state that wouldn't make Roc code way too powerful
Kind of?
as opposed to it just saying like "here is the new state as a return value" and then having that get propagated
In svelte, you'd do
Scratch that, in Solid you'd do
function Clock(color: Color) {
const [second, setSecond] = createSignal()
const [minute, setMinute] = createSignal()
const [hour, setHour] = createSignal()
return (
<circle>
<Hand {second} />
<Hand {minute} />
<Hand {hour} nudge={setHour} />
</circle>
);
}
This creates a list of three cells in a global stack, and then uses those to figure out how to manage second, minute, and hour based on the order of their creation during the function definition
If we had such a stack we could push into, then we could do the above and get back either (U64, U64, Str) or { second : U64, minute : U64, hour : Str }
You could achieve something close with Store.set!
I think a platform could do that if desired
make create_signal! use hosted stuff
Absolutely!
But, now all components are effectful
but this seems like it's essentially the same as the React Component tradeoffs
Exactly
There is definitely a pure way to track this, just not with Roc's current syntax
It's the reason why I haven't started pushing to remove backpassing now that it has been deprecated for a while, I think that would help here
to me though, the React way of doing state management is a pit of failure rather than a pit of success
Would you consider it a pit of success if the same syntax let you get out typed state?
e.g.
clock : Color -> Elem { second, minute, hour }
I feel like React is success if we know what state is where
I don't think the problem with React's state management has anything to do with syntax
Agreed
(i think)
I dislike passing all bits of state around all of the time within a component
I want to know the state of the component otherwise
In the way that var means I don't have to do seed and seed2 and seed3
Can I get something that helps me manage aggregated state within a single function?
That doesn't resort to effectfulness
I feel like that's what I'm getting at here, but maybe you feel that passing around state counts as syntax (or something close to it)?
I will try my hand at putting together a proposal for something if I can this weekend, that would help this discussion move from "I want something" to "would this thing in particular work for Roc"
maybe a better way of condensing what I'm saying is that there are two ways of thinking about state in a UI application:
I'm saying I think #1 is the way to go because #2 feels nicer in the small but is astronomically more error-prone in the large.
it's just a binary yes or no, is there one type that has "the entire state of the application at this exact moment"
I think the best UI state management systems answer "yes" to that question
I feel like that makes some sense for UI state, but less sense for disparate effect state (in tests).
oh yeah this is just about UIs, not about tests
Also, I get the passing state down the stack in smaller an smaller pieces. I still have never seen a great way to do updates of those smaller and smaller pieces.
Action state like solutions have always felt really messy to me
But maybe I need to play with them more in practice
Brendan Hansknecht said:
Action state like solutions have always felt really messy to me
I completely agree, which is why I think the alternative is a footgun :big_smile:
I think action state just has a boilerplate problem, but it's way better than the alternative
"it feels messy" is an annoyance; "my program has all these state management bugs I can't debug effectively" is a serious problem
Yeah, it's hard.
I live in a world of footguns, so keeping a few doesn't feel too bad to me.
But I agree that we should strive to avoid them
Richard Feldman said:
I'm saying I think #1 is the way to go because #2 feels nicer in the small but is astronomically more error-prone in the large.
I'm not even considering 2 haha
yeah basically I'm making the case for "choose to have the boilerplate problem instead of the more serious problem" :big_smile:
I'm asking how to fix the boilerplate problem, because it's the only option, so we have to entice the people that would choose the danger option because it's easier to write
Myself included in that group
it would be cool to find a solution to it, but I have yet to see one that doesn't feel like it introduces worse problems...but maybe it's out there, I dunno :shrug:
I find this gets the worst when you have truely unrelated code, but it all still has to boil into a single state. Like if you are testing and happen to use a library, but it's state is really unrelated to the rest of your app and would best be managed separately, but still has to be pipe through your app due to dependency ordering.
I will say I have seen an interesting perspective on that in https://youtu.be/4n5fFMLVOBo?si=H96I7Mc8X9O5VGEe - basically the idea is that you have the single state atom, but then you have a separate API for defining new "UI primitives"
so kinda like how you have some pieces of UI state that are essentially decoupled from your particular application (e.g. where exactly is the blinking caret in a text input), you have an API that lets you say "okay this state is going to be managed by a completely separate system that's more complicated - e.g. has to specify things like initial state, what happens when it gets rendered for the first time vs. re-rendered, etc. - but makes reuse simpler for the person using it"
so then library authors could use that
it seems kinda like saying "you have one big state atom, but then also you can opt into having inaccessible islands of state on top of that, except it's significantly more work to reach for the latter, so hopefully they will only get used sparingly and not cause problems"
I'll listen tonight!
Holy cow. You guys really went down some paths from our initial conversation!
My idea was more along these lines (now informed by the above discusssions)
# SomeModule.roc
module [Data, my_effect!]
import pl.File
import pl.Http
import pl.Decode
import pl.Json
Data := { ... }
my_effect! : Str => Data
my_effect! = \url ->
hashed = hash url
if File.isFile! hash |> Result.withDefault false then
File.readUtf8! hashed |> Result.map \bytes -> Decode.bytes bytes Json.Utf8 |> validateData
else
Http.get! url Json.Utf8 |> Result.map \json -> validateData Json
# SomeModuleTest.roc
module []
import SomeModule
testUrl1 = "https://some-api.comi/api/v1/data"
testUrl2 = "https://some-api.comi/api/v1/otherData"
mockFiles =
Dict.empty {}
|> Dict.insert (hash testUrl) (Ok {a: 123})
mockResponses =
Dict.empty {}
|> Dict.insert testUrl2 (Ok {a: 456})
|> Dict.insert testUrl1 (Err NotFoundErr) # Sorry, I haven't used Http yet, and the docs aren't great....work with me
expectWith! {pl: PlatformMock { files: mockFiles, responses: mockResponses }}
(SomeModule.my_effect! testUrl!) == (Ok { a: 123 }) && (pl.num_http_calls! {}) == 0
# PlatformMock.roc
platform mock {files, responses} -> [Http.get!, File.isFile!, File.readUtf8!, num_http_calls!]
store = create_store! { http_requests: 0, file_reads: 0 }
Http.get! = \url, format ->
store.transact! \s -> {s & http_requests: s.http_requests + 1}
when Dict.get responses url is
Ok data -> Ok data
Err _ -> Err NotFoundErr
File.isFile! = \str ->
store.transact! \s -> {s & http_requests: s.file_reads + 1}
when Dict.get files str is
Ok data -> Ok data
Err _ -> Err FileReadErr
File.readUtf8! = \str -> ...
num_http_calls! = \{} -> store.get! |> .http_requests
Lots of errors in there I'm sure, but you get it I hope
You'd probably also have to have some function to clear the store exported from the platform mock so it could be reset between expects
I don't know if it fits here, but e.g. something like https://mswjs.io/ with regards to network calls would be really nice. In testing then simply disable the connection to the outside and instead call the mock.
Last updated: Jun 16 2026 at 16:19 UTC