Stream: ideas

Topic: Platform mocking in a PI world


view this post on Zulip Anthony Bullard (Dec 10 2024 at 03:29):

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.

view this post on Zulip Anthony Bullard (Dec 10 2024 at 03:30):

Since all true side effects come from the platform

view this post on Zulip Anthony Bullard (Dec 10 2024 at 03:31):

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).

view this post on Zulip Sam Mohr (Dec 10 2024 at 03:32):

Currently, the thought is that we have two tools that compose to build a mock platform: stateful + effectful top-level expects, and module params

view this post on Zulip Sam Mohr (Dec 10 2024 at 03:33):

Meaning you can write something like this:

view this post on Zulip Sam Mohr (Dec 10 2024 at 03:36):

# 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]

view this post on Zulip Sam Mohr (Dec 10 2024 at 03:36):

I'm not sure how well that would work at scale, but all the tools should be there

view this post on Zulip Sam Mohr (Dec 10 2024 at 03:37):

It seems preferable to me to use an "inline" solution that doesn't require importing a different platform for testing

view this post on Zulip Anthony Bullard (Dec 10 2024 at 03:38):

I didn't know about those expect \{} -> declarations...

view this post on Zulip Anthony Bullard (Dec 10 2024 at 03:38):

Is that in latest???

view this post on Zulip Sam Mohr (Dec 10 2024 at 03:38):

That's not a thing yet

view this post on Zulip Sam Mohr (Dec 10 2024 at 03:38):

Not even a GitHub issue

view this post on Zulip Sam Mohr (Dec 10 2024 at 03:38):

Just the latest non-thrown-out popular suggestion

view this post on Zulip Anthony Bullard (Dec 10 2024 at 03:38):

Oh ok. Here I was hoping we could get labelled expects and test reporting, but this is awesome too

view this post on Zulip Sam Mohr (Dec 10 2024 at 03:39):

Do you like or dislike it?

view this post on Zulip Anthony Bullard (Dec 10 2024 at 03:39):

To me it seems _really_ similar

view this post on Zulip Anthony Bullard (Dec 10 2024 at 03:39):

To my idea

view this post on Zulip Sam Mohr (Dec 10 2024 at 03:39):

Since we haven't even planned working on it, it'd be interesting to hear if you think we can improve

view this post on Zulip Sam Mohr (Dec 10 2024 at 03:40):

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

view this post on Zulip Sam Mohr (Dec 10 2024 at 03:40):

In your example, the platform is there, and we crash at runtime if we try to run an "actual" effect

view this post on Zulip Anthony Bullard (Dec 10 2024 at 03:40):

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

view this post on Zulip Sam Mohr (Dec 10 2024 at 03:40):

That could work

view this post on Zulip Anthony Bullard (Dec 10 2024 at 03:40):

And expects stay the same

view this post on Zulip Anthony Bullard (Dec 10 2024 at 03:40):

The hard thing though is Pure Roc doesn't let you maintain any sort of state

view this post on Zulip Anthony Bullard (Dec 10 2024 at 03:41):

Unless those modules were a special type that allowed for top level var

view this post on Zulip Sam Mohr (Dec 10 2024 at 03:41):

I think any solution that:

Is a good sell

view this post on Zulip Sam Mohr (Dec 10 2024 at 03:41):

You're right that Pure Roc is stateless by design

view this post on Zulip Anthony Bullard (Dec 10 2024 at 03:42):

I don't know if top-level var for just these mock platforms would be acceptable

view this post on Zulip Sam Mohr (Dec 10 2024 at 03:42):

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

view this post on Zulip Anthony Bullard (Dec 10 2024 at 03:42):

You'd need a different type, and then make importing that into anything in the main.roc's dependency graph illegal

view this post on Zulip Sam Mohr (Dec 10 2024 at 03:43):

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

view this post on Zulip Anthony Bullard (Dec 10 2024 at 03:43):

What are the signatures for get! and set!

view this post on Zulip Anthony Bullard (Dec 10 2024 at 03:43):

That's fine to me

view this post on Zulip Sam Mohr (Dec 10 2024 at 03:44):

get! : {} => a
set! : a => {}

view this post on Zulip Anthony Bullard (Dec 10 2024 at 03:44):

Yeah, forget top level var

view this post on Zulip Anthony Bullard (Dec 10 2024 at 03:45):

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...

view this post on Zulip Anthony Bullard (Dec 10 2024 at 03:45):

The usage further down should get you the information you need

view this post on Zulip Sam Mohr (Dec 10 2024 at 03:45):

Roc's type inference will handle it

view this post on Zulip Anthony Bullard (Dec 10 2024 at 03:45):

I think we could meld these two

view this post on Zulip Anthony Bullard (Dec 10 2024 at 03:46):

Use my concept and then have get! and set! be functions in a Mock package in the builtins

view this post on Zulip Anthony Bullard (Dec 10 2024 at 03:46):

Or be in the prelude for this type of file

view this post on Zulip Anthony Bullard (Dec 10 2024 at 03:46):

Lots of small little design ideas to play around with

view this post on Zulip Anthony Bullard (Dec 10 2024 at 03:47):

I'd say you _may_ want:

get! : Str => a
set! : Str, a => {}

Or similar and have the backing datastructure be a hashmap

view this post on Zulip Anthony Bullard (Dec 10 2024 at 03:47):

But we should talk more about this soon

view this post on Zulip Anthony Bullard (Dec 10 2024 at 03:48):

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

view this post on Zulip Anthony Bullard (Dec 10 2024 at 03:49):

That doesn't work for Roc

view this post on Zulip Sam Mohr (Dec 10 2024 at 03:49):

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

view this post on Zulip Sam Mohr (Dec 10 2024 at 03:50):

Okay, interesting to think about!

view this post on Zulip Brendan Hansknecht (Dec 10 2024 at 04:02):

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.

view this post on Zulip Brendan Hansknecht (Dec 10 2024 at 04:03):

Cause file read probably wants a different get and set data from UTC now from etc.

view this post on Zulip Anthony Bullard (Dec 10 2024 at 04:05):

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

view this post on Zulip Sam Mohr (Dec 10 2024 at 04:06):

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 {}

view this post on Zulip Sam Mohr (Dec 10 2024 at 04:06):

Which is a hassle, but we (or a third party lib) could make convenience functions

view this post on Zulip Brendan Hansknecht (Dec 10 2024 at 04:07):

But then everything has to be the same type to fit into a dict and that is a hassle.

view this post on Zulip Sam Mohr (Dec 10 2024 at 04:07):

It doesn't have to be a Dict

view this post on Zulip Sam Mohr (Dec 10 2024 at 04:07):

It just is in this case

view this post on Zulip Sam Mohr (Dec 10 2024 at 04:08):

It could be that get! stores a { users : Dict Str User, emails : List Str }

view this post on Zulip Sam Mohr (Dec 10 2024 at 04:08):

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

view this post on Zulip Brendan Hansknecht (Dec 10 2024 at 04:09):

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.

view this post on Zulip Brendan Hansknecht (Dec 10 2024 at 04:09):

Then in each effect you can call key and store exactly the data you want for that one effect

view this post on Zulip Brendan Hansknecht (Dec 10 2024 at 04:10):

You also can share a key between effects via closure captures if that is wanted

view this post on Zulip Brendan Hansknecht (Dec 10 2024 at 04:10):

Feels the right level of power without collision due to string keys

view this post on Zulip Sam Mohr (Dec 10 2024 at 04:10):

Well, then get! can't have just one return type

view this post on Zulip Sam Mohr (Dec 10 2024 at 04:11):

Not sure how that'd work

view this post on Zulip Sam Mohr (Dec 10 2024 at 04:12):

I'm also not convinced that just get! and set! aren't enough if we provide convenience wrappers.

view this post on Zulip Sam Mohr (Dec 10 2024 at 04:13):

We could also have expect generate a different getter and setter for each value in the record

view this post on Zulip Brendan Hansknecht (Dec 10 2024 at 04:14):

Actually maybe key should return the get and set function for that key, would make the most sense.

view this post on Zulip Sam Mohr (Dec 10 2024 at 04:14):

expect { users, emails } ->
    currentUsers = users.get! {}
    users.set! (Dict.update currentUsers "jodie" 123)

    emails : { get! : {} => List Email, set! : List Email => {} }

view this post on Zulip Sam Mohr (Dec 10 2024 at 04:15):

This requires a default value for get! and set! to be inferred, UNLESS

view this post on Zulip Brendan Hansknecht (Dec 10 2024 at 04:15):

Yes, that is way better and kinda what I was trying to work towards

view this post on Zulip Sam Mohr (Dec 10 2024 at 04:15):

Nice!

view this post on Zulip Sam Mohr (Dec 10 2024 at 04:16):

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

view this post on Zulip Sam Mohr (Dec 10 2024 at 04:16):

Not specifically this

view this post on Zulip Sam Mohr (Dec 10 2024 at 04:16):

But something like this with a pure and an effectful version could work

view this post on Zulip Brendan Hansknecht (Dec 10 2024 at 04:17):

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.

view this post on Zulip Richard Feldman (Dec 10 2024 at 04:17):

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

view this post on Zulip Sam Mohr (Dec 10 2024 at 04:17):

That's why I bring up the Action-State thing here. I agree that nested state is the tricky thing

view this post on Zulip Sam Mohr (Dec 10 2024 at 04:18):

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

view this post on Zulip Sam Mohr (Dec 10 2024 at 04:18):

But yes, it's basically Brendan's idea

view this post on Zulip Richard Feldman (Dec 10 2024 at 04:18):

it could also include this, for software transactional memory:

Store.transact! : Store a, (a -> (a, ret)) => ret

view this post on Zulip Richard Feldman (Dec 10 2024 at 04:19):

there's a bit of a rabbit hole when it comes to concurrency, deadlocks, and atomicity with something like this

view this post on Zulip Richard Feldman (Dec 10 2024 at 04:20):

but anyway, one use for it could be in tests, because it would be a builtin

view this post on Zulip Sam Mohr (Dec 10 2024 at 04:20):

Makes me want to re-read the Pony tutorial to see if there is something lockless we could manage

view this post on Zulip Richard Feldman (Dec 10 2024 at 04:20):

so you wouldn't need a test-specific thing

view this post on Zulip Sam Mohr (Dec 10 2024 at 04:21):

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

view this post on Zulip Richard Feldman (Dec 10 2024 at 04:21):

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

view this post on Zulip Sam Mohr (Dec 10 2024 at 04:22):

But once you enable state outside of tests, I think there'd be a whole part of the Roc ecosystem that composed stateful mechanisms

view this post on Zulip Sam Mohr (Dec 10 2024 at 04:22):

Not sure if that's terrible, but it would be a bifurcation in the making

view this post on Zulip Sam Mohr (Dec 10 2024 at 04:23):

The nice thing about Store in Roc is that the ! required suffix makes this a much more sane version of refs in Ocaml

view this post on Zulip Richard Feldman (Dec 10 2024 at 04:23):

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 }
)

view this post on Zulip Richard Feldman (Dec 10 2024 at 04:24):

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

view this post on Zulip Richard Feldman (Dec 10 2024 at 04:24):

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.

view this post on Zulip Richard Feldman (Dec 10 2024 at 04:25):

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

view this post on Zulip Richard Feldman (Dec 10 2024 at 04:25):

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

view this post on Zulip Richard Feldman (Dec 10 2024 at 04:26):

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

view this post on Zulip Sam Mohr (Dec 10 2024 at 04:26):

What if the platform has its own lock?

view this post on Zulip Richard Feldman (Dec 10 2024 at 04:26):

anyway, so it's a combination of an interesting concurrency primitive and also a potentially nice way to address tests

view this post on Zulip Richard Feldman (Dec 10 2024 at 04:27):

the idea is that the Store API does not offer a direct lock! primitive operation

view this post on Zulip Richard Feldman (Dec 10 2024 at 04:27):

so read! and write! use locks behind the scenes, and so does transact!

view this post on Zulip Richard Feldman (Dec 10 2024 at 04:27):

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

view this post on Zulip Sam Mohr (Dec 10 2024 at 04:28):

Yep, makes sense

view this post on Zulip Richard Feldman (Dec 10 2024 at 04:28):

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

view this post on Zulip Sam Mohr (Dec 10 2024 at 04:28):

There's not a way that the platform could wait on something in the transact! because it's pure

view this post on Zulip Richard Feldman (Dec 10 2024 at 04:28):

oh yeah that's also important

view this post on Zulip Richard Feldman (Dec 10 2024 at 04:29):

since read! and write! and transact! are all effectful, you can't run any of them from within the pure function that transact! offers

view this post on Zulip Sam Mohr (Dec 10 2024 at 04:30):

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!

view this post on Zulip Sam Mohr (Dec 10 2024 at 04:31):

Bit of a shame that this doesn't quite handle Action-State, unless that were to use Store as well...

view this post on Zulip Richard Feldman (Dec 10 2024 at 04:33):

I haven't really thought about it in that context

view this post on Zulip Sam Mohr (Dec 10 2024 at 04:33):

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

view this post on Zulip Sam Mohr (Dec 10 2024 at 04:34):

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

view this post on Zulip Richard Feldman (Dec 10 2024 at 04:35):

I have thoughts on the topic of UI state haha

view this post on Zulip Sam Mohr (Dec 10 2024 at 04:35):

In your own time, then!

view this post on Zulip Sam Mohr (Dec 10 2024 at 04:36):

It's my current white whale apparently

view this post on Zulip Sam Mohr (Dec 10 2024 at 04:36):

It feels like there's something obvious I'm missing, because in theory it's so simple

view this post on Zulip Richard Feldman (Dec 10 2024 at 04:39):

I guess the short version is that you can pick between:

view this post on Zulip Richard Feldman (Dec 10 2024 at 04:40):

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

view this post on Zulip Richard Feldman (Dec 10 2024 at 04:41):

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

view this post on Zulip Sam Mohr (Dec 10 2024 at 04:42):

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

view this post on Zulip Sam Mohr (Dec 10 2024 at 04:43):

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

view this post on Zulip Sam Mohr (Dec 10 2024 at 04:44):

I'm trying to figure out a way to build that heterogeneously-typed stack in an ergonomic way

view this post on Zulip Sam Mohr (Dec 10 2024 at 04:44):

One way is with backpassing

view this post on Zulip Sam Mohr (Dec 10 2024 at 04:45):

Another is with State.store!

view this post on Zulip Sam Mohr (Dec 10 2024 at 04:45):

A third is with

{ stateful!
    users,
    emails,
}

-- which goes to

{
    users : { get, set },
    emails : { get, set },
}

view this post on Zulip Sam Mohr (Dec 10 2024 at 04:47):

I am quite convinced that this is what the solution will look like, I just haven't yet found an in-between that is:

view this post on Zulip Sam Mohr (Dec 10 2024 at 04:48):

A syntax like 'users that creates a lens fixes the first two, but sucks for teaching

view this post on Zulip Sam Mohr (Dec 10 2024 at 04:49):

If we bit the effect bullet and tracked two kinds of effectfulness, statefulness and purity, then we could do all three

view this post on Zulip Sam Mohr (Dec 10 2024 at 04:49):

But that's a barrel of worms

view this post on Zulip Richard Feldman (Dec 10 2024 at 04:52):

hm, I'm not sure if we're talking about the same thing

view this post on Zulip Richard Feldman (Dec 10 2024 at 04:52):

I'm more talking about UI state than application state

view this post on Zulip Sam Mohr (Dec 10 2024 at 04:52):

Something like

-- exclam means impure
writeFile! : Str, List U8 => {}

-- at means stateful
@saveUser : Str -> {}

view this post on Zulip Richard Feldman (Dec 10 2024 at 04:52):

oh I see

view this post on Zulip Richard Feldman (Dec 10 2024 at 04:52):

so you're talking about like in an event handler saying "hey go change this piece of application state"

view this post on Zulip Sam Mohr (Dec 10 2024 at 04:53):

I'm trying to find a solution for UI state that wouldn't make Roc code way too powerful

view this post on Zulip Sam Mohr (Dec 10 2024 at 04:53):

Kind of?

view this post on Zulip Richard Feldman (Dec 10 2024 at 04:53):

as opposed to it just saying like "here is the new state as a return value" and then having that get propagated

view this post on Zulip Sam Mohr (Dec 10 2024 at 04:53):

In svelte, you'd do

view this post on Zulip Sam Mohr (Dec 10 2024 at 04:54):

Scratch that, in Solid you'd do

view this post on Zulip Sam Mohr (Dec 10 2024 at 04:56):

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>
    );
}

view this post on Zulip Sam Mohr (Dec 10 2024 at 04:57):

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

view this post on Zulip Sam Mohr (Dec 10 2024 at 04:58):

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 }

view this post on Zulip Sam Mohr (Dec 10 2024 at 04:59):

You could achieve something close with Store.set!

view this post on Zulip Richard Feldman (Dec 10 2024 at 05:00):

I think a platform could do that if desired

view this post on Zulip Richard Feldman (Dec 10 2024 at 05:00):

make create_signal! use hosted stuff

view this post on Zulip Sam Mohr (Dec 10 2024 at 05:01):

Absolutely!

view this post on Zulip Sam Mohr (Dec 10 2024 at 05:01):

But, now all components are effectful

view this post on Zulip Richard Feldman (Dec 10 2024 at 05:02):

but this seems like it's essentially the same as the React Component tradeoffs

view this post on Zulip Sam Mohr (Dec 10 2024 at 05:02):

Exactly

view this post on Zulip Sam Mohr (Dec 10 2024 at 05:02):

There is definitely a pure way to track this, just not with Roc's current syntax

view this post on Zulip Sam Mohr (Dec 10 2024 at 05:03):

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

view this post on Zulip Richard Feldman (Dec 10 2024 at 05:04):

to me though, the React way of doing state management is a pit of failure rather than a pit of success

view this post on Zulip Sam Mohr (Dec 10 2024 at 05:05):

Would you consider it a pit of success if the same syntax let you get out typed state?

view this post on Zulip Sam Mohr (Dec 10 2024 at 05:05):

e.g.

clock : Color -> Elem { second, minute, hour }

view this post on Zulip Sam Mohr (Dec 10 2024 at 05:06):

I feel like React is success if we know what state is where

view this post on Zulip Richard Feldman (Dec 10 2024 at 05:07):

I don't think the problem with React's state management has anything to do with syntax

view this post on Zulip Sam Mohr (Dec 10 2024 at 05:08):

Agreed

view this post on Zulip Sam Mohr (Dec 10 2024 at 05:08):

(i think)

view this post on Zulip Sam Mohr (Dec 10 2024 at 05:08):

I dislike passing all bits of state around all of the time within a component

view this post on Zulip Sam Mohr (Dec 10 2024 at 05:08):

I want to know the state of the component otherwise

view this post on Zulip Sam Mohr (Dec 10 2024 at 05:09):

In the way that var means I don't have to do seed and seed2 and seed3

view this post on Zulip Sam Mohr (Dec 10 2024 at 05:09):

Can I get something that helps me manage aggregated state within a single function?

view this post on Zulip Sam Mohr (Dec 10 2024 at 05:10):

That doesn't resort to effectfulness

view this post on Zulip Sam Mohr (Dec 10 2024 at 05:11):

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)?

view this post on Zulip Sam Mohr (Dec 10 2024 at 05:16):

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"

view this post on Zulip Richard Feldman (Dec 10 2024 at 05:16):

maybe a better way of condensing what I'm saying is that there are two ways of thinking about state in a UI application:

  1. There is one big atomic type that represents the entire state of the application. All render functions get their data from somewhere in this one giant value (although almost always they get passed some subset of it in practice). Like I can look at one type annotation and be like "yeah, that's it, that's the entire state of the application, it's all in that one type annotation."
  2. State exists in any places other than that.

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.

view this post on Zulip Richard Feldman (Dec 10 2024 at 05:17):

it's just a binary yes or no, is there one type that has "the entire state of the application at this exact moment"

view this post on Zulip Richard Feldman (Dec 10 2024 at 05:17):

I think the best UI state management systems answer "yes" to that question

view this post on Zulip Brendan Hansknecht (Dec 10 2024 at 05:19):

I feel like that makes some sense for UI state, but less sense for disparate effect state (in tests).

view this post on Zulip Richard Feldman (Dec 10 2024 at 05:19):

oh yeah this is just about UIs, not about tests

view this post on Zulip Brendan Hansknecht (Dec 10 2024 at 05:20):

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.

view this post on Zulip Brendan Hansknecht (Dec 10 2024 at 05:20):

Action state like solutions have always felt really messy to me

view this post on Zulip Brendan Hansknecht (Dec 10 2024 at 05:20):

But maybe I need to play with them more in practice

view this post on Zulip Richard Feldman (Dec 10 2024 at 05:21):

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:

view this post on Zulip Sam Mohr (Dec 10 2024 at 05:21):

I think action state just has a boilerplate problem, but it's way better than the alternative

view this post on Zulip Richard Feldman (Dec 10 2024 at 05:21):

"it feels messy" is an annoyance; "my program has all these state management bugs I can't debug effectively" is a serious problem

view this post on Zulip Brendan Hansknecht (Dec 10 2024 at 05:22):

Yeah, it's hard.

view this post on Zulip Brendan Hansknecht (Dec 10 2024 at 05:22):

I live in a world of footguns, so keeping a few doesn't feel too bad to me.

view this post on Zulip Brendan Hansknecht (Dec 10 2024 at 05:22):

But I agree that we should strive to avoid them

view this post on Zulip Sam Mohr (Dec 10 2024 at 05:22):

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

view this post on Zulip Richard Feldman (Dec 10 2024 at 05:22):

yeah basically I'm making the case for "choose to have the boilerplate problem instead of the more serious problem" :big_smile:

view this post on Zulip Sam Mohr (Dec 10 2024 at 05:23):

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

view this post on Zulip Sam Mohr (Dec 10 2024 at 05:23):

Myself included in that group

view this post on Zulip Richard Feldman (Dec 10 2024 at 05:24):

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:

view this post on Zulip Brendan Hansknecht (Dec 10 2024 at 05:25):

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.

view this post on Zulip Richard Feldman (Dec 10 2024 at 05:28):

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"

view this post on Zulip Richard Feldman (Dec 10 2024 at 05:30):

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"

view this post on Zulip Richard Feldman (Dec 10 2024 at 05:31):

so then library authors could use that

view this post on Zulip Richard Feldman (Dec 10 2024 at 05:32):

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"

view this post on Zulip Sam Mohr (Dec 10 2024 at 05:47):

I'll listen tonight!

view this post on Zulip Anthony Bullard (Dec 10 2024 at 11:43):

Holy cow. You guys really went down some paths from our initial conversation!

view this post on Zulip Anthony Bullard (Dec 10 2024 at 12:22):

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

view this post on Zulip Anthony Bullard (Dec 10 2024 at 12:30):

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

view this post on Zulip Tobias Steckenborn (Jan 02 2025 at 06:49):

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