I wrote up a design for a GUI system (and revised it several times, hence the v5 in the title) - would love to hear feedback on it!
https://docs.google.com/document/d/16qY4NGVOHu8mvInVD-ddTajZYSsFvFBvQON_hmyHGfo/edit?usp=sharing
Looking solid :+1:
Another naming option for Elem.translate could be Elem.transfer, although I think translate is pretty good.
I was a bit unsure about having the update "in-place", since I thought it would be pretty easy to mess it up, e. g. by using the state. But after sleeping on it I think that's actually not a big issue, it should be doable to teach people that. If it does happen though, the debugging will be rough.
But the convenience this change enables is pretty appealing. So it's an interesting tradeoff.
We can have a rule for that in our own counterpart of elm-analyse that is enabled by default in the editor.
@Richard Feldman I haven't read the whole document yet, but I like it very much so far, including the &field record setter syntax :+1:
Two remarks:
Action.update is Action.update : state -> Action state (which is what I had expected), but you use it in the examples in a different way: increase = button {
label: text "+",
onPress: \prev, _ -> Action.update (prev + 1),
}
Elem.translate have an additional parameter of type parent? Otherwise I wouldn't know how to create an Elem parent from the given parameters?Forget my first remark, your examples are consistent with the type signature...
I think what I really expected was that Action.update would take a state modifying function as parameter, not the state itself. I have to read the rest of the document first before making more comments, though.
This approach to GUIs looks interesting! I've done a similar approach in Elm. Something along the lines of Html Model instead of Html Msg and providing getters and setters to control what is in scope for a particular piece of UI. I think this approach works well for things like forms that map well to a bunch of fields in a record. I haven't tried this approach with an entire app though.
That said, I'm worried about the part that mentions how tasks can be performed inside event handlers. This seems like it will lead to lots of non-local behavior. For example, you notice that the app is producing an http request that it shouldn't be doing and in order to figure out where it is coming from you have to look in every event handler since any part of the render function could be triggering it.
the editor could fix this but I think this is still a good point: by splitting the update part of the app into many tiny updates you loose the ability to quickly get the general picture of what an application does
on the other hand, that makes scaling an app easier
One issue I've always had with elm was that everything needed to bubble up to a single update. Pretty much along the lines of what Folkert said. Getting a big picture is nice but sometimes I just want to think locally instead especially within a much largely app
yeah I think this is an unavoidable tradeoff unfortunately
I am still learning and wrapping my head around everything. The comment bellow comes from ignorance and curiosity. I hope it won't offend.
Web technologies are widely available. There are libraries, frameworks, tools, guidelines, linters, ... Would there be more value, for Roc, to use off the shelf solutions (doesn't have to be web) for it's V1?
Richard Feldman said:
yeah I think this is an unavoidable tradeoff unfortunately
that's ok it's definitely not a deal breaker and the type checker helps wrangle that complexity as the app grows anyways
I like this doc though, cool stuff
Web technologies are widely available. There are libraries, frameworks, tools, guidelines, linters, ... Would there be more value, for Roc, to use off the shelf solutions (doesn't have to be web) for it's V1?
It's a good question!
to be totally honest, I've been increasingly frustrated with the ergonomics, limitations, and runtime performance of these technologies over the course of the career I've spent with them, and one of the major things that excited me about working on Roc was the opportunity to not use any of them :big_smile:
so that's not to say that nobody ought to, but rather that I want to aim higher than what I've learned to be possible with those technologies
we can do so much better!
I like to imagine a world where websites are just blogs and landing pages. And then all desktop apps are high performance cross-platform GUIs
I really don't like how this line feels: left = Elem.translate renderCounter .left &left
I get the requirement with the current setup, but I feel that there should be a better way to do this.
Otherwise, this looks like a nice proposal to bring TEA to Roc.
one key difference from TEA is how the updates work
having just a model and a render function feels more like react
That's true
but that honestly doesn't make much of a difference from my perspective
I do agree with the concern for the line you quoted
&left sugar feels a bit odd. maybe because I'm just used to thinking that's a reference
My biggest concern is that it isnt' really a solid contract or complete definition.
What if one platform supports x UI elements and other supports another set?
What if my platform has a bunch of specific but convenient actions but other platforms don't include those?
I just feel that it will be very likely that this interface will just be similar but not the same between platforms. This means that you can't just swap platforms, you need to port your app for the (hopefully) minor difference between each platform
Lucas Rosa said:
&left sugar feels a bit odd. maybe because I'm just used to thinking that's a reference
The line is also just really repetitive which makes it look more confusing left = ... .left &left
I think this proposal could work really well, but it would require some sort of platform interface definition. That way each platform is guaranteed to give the same base elements. Then user space libraries could be made to build on top of that and keep things cross platform.
I guess one extra note. If the interfaces are versioned, someone could write a shim crate. Like I want to use this platform with the newer gui interface, but it hasn't been updated yet. Just use the shim and you are good.
Small note: I think Action should be renamed to Reaction, as the concept matches the "reactive"/"reactivity" terminology already used in modern declarative UIs (especially web apps). If the two extra characters seem annoying, I think most apps will do unqualified imports anyways: pf.Reaction.{ task, update }
Actually, I'm just assuming platforms are expected to expose an Action module just like they expose a Task module - is that accurate?
I see now that the doc doesn't define where the Action record comes from.
I don't see how the first example is valid, I'd expect the compiler to complain about Action or Action.none being undefined.
The platform definitely should be exposing Action. If it wasn't defined by the platform, the platform wouldn't be able to handle actions.
On third thought, why is Action (or the platform's Action module) necessary at all? Why can't the return value of an event handler just be a tag like onPress: \prev, _ -> Update (prev + 1) ? That seems equally platform-agnostic.
That would be simpler for app developers and more expressive for platform developers: [ Update, Delegate, None ] instead of [ Action ]
This is probably lower-level feedback than you were looking for @Richard Feldman , but I'm honestly having a harder time wrapping my Elm-ignorant (but other-frontend-frameworks-experienced) brain around this doc than I expected, so I'm trying to trim complexity to help myself to get it
I have a partially-written doc about how the reuse across platform works, but I can give some quick notes
so Action would be defined in the GUI library, not the platform.
However, it would have a way for platforms to define custom Actions (using a single function call - but it takes some background explanation as how to how that works, so let's just assume for the moment that this is possible). This means that platform authors can write a Task.toAction function that converts from that platform's Task to an Action, for example:
toAction : Task ok err, (Result ok err -> (state -> Action state)) -> Action state
(Notably, Tasks can be chained together with await, but Actions can't.) This is flexible enough to work with whatever type the platform author chooses for effects, including if they decide to do something other than Task - they just need to specify how to convert whatever their thing is to an Action.
so now the platform author can specify whatever effects they want to support in terms of Task, and any user of the library can convert them to an Action using Task.toAction, in order to use them in an effect handler
so the next question is how to deal with the fact that platforms support different effects
let's use an autocomplete dropdown as an example
I want to write a generic autocomplete dropdown, and publish it to the package repo, and have anyone be able to use it on any platform
and one of the things I want it to be able to do is to do HTTP requests to load autocomplete search results as the user types
but of course it has to be platform-agnostic, so I can't generically say "run a HTTP request Task" because Task is a platform-specific concept, so what do I do?
basically I make my API look something like this:
State : { entries : List Str, onInput : (Str -> Action (List Str)) }
Autocomplete.render : State -> Elem State
so the specified onInput handler will fire whenever the user types, and it will call the given (Str -> Action (List Str)) function passing the Str that the user has entered, and it's that function's job to return an Action (List Str) which provides the list of autocomplete results to display for that Str
so now, let's say I'm on a platform that has HTTP tasks
I call this passing something like:
{
entries,
onInput: \str ->
Http.get "/autocomplete?query={str}"
|> Task.toAction \result -> httpResponseToAction result
}
and that's it!
and actually, I don't have to do a HTTP request here
I can do any kind of effect I want to load the autocomplete results, e.g. read them from a file or a databsae
*database
or just return Action.update to have them come from a hardcoded list in memory, that's fine too
the point is that by asking the user of the API to specify some arbitrary Action which provides the output it needs, the library doesn't actually have to know the details of how that effect is made
it just specifies the inputs (Str) and the outputs it expects (List Str) in the form of this Str -> Action (List Str) function for the caller to provide
and then it gives the caller total flexibility about how to come up with that answer given those inputs, while at the same time making the whole API platform-agnostic
so...that's the idea!
(oh, and :point_up: is one reason it's important to actually have a separate Action type!)
also note that in a real-world scenario I'd have written a proper tutorial explanation of how to do all this :laughing:
also note that
Haha no judgement or expectations! I think the current system design process works great.
What is Action.fromTaskTask.toAction actually doing in that last snippet? Why can't that |> chain simply end with something like |> \suggestions -> Update suggestions ?
oops, that should have been Task.toAction
(edited to fix it!)
I understand the need for event handlers in a declarative+reactive UI library, but I don't understand why their return types need to be more than just data. Is it because the functions need to be called before their intent should be realized in the UI?
so I guess I need to get into this a bit more
the basic mechanism that makes this work under the hood is that it relies on the fact that all effects in a Roc platform are implemented in terms of some Effect type
(this may be my Elm ignorance speaking)
(nope, nothing to do with Elm in this case!)
so even if you're using Task for everything, it has to be a wrapper around Effect because that's how you specify host calls
so the actual type of Action - again, defined only in the graphics library, not in the platform - is something like this:
Action state : [ None, Always state, Fx (Effect (state -> Action state)) ]
(except an opaque type)
so this is a new concept for the compiler: packages can use the Effect type without knowing what its capabilities are
so package authors can say "the platform is going to define what different operations can produce an Effect that actually runs an effect; I don't get to do that...but I do actually know that some sort of Effect is going to exist, so I can use it in my data structures even though the platform is in charge of what useful things can be done with it"
so the implementation of Task.toAction then looks something like this:
toAction : Task ok err, (Result ok err -> (state -> Action state)) -> Action state
toAction = \effect, resultToCb ->
fx : Effect (state -> Action state)
fx = Effect.map effect resultToCb
Fx fx
so it's actually storing functions in there, in addition to non-function data
so if you wanted to, one possible design would be to not say Action state everywhere, but instead say [ None, Always state, Fx (Effect (state -> Action state)) ] everywhere
but that's pretty big and clumsy, so definitely would be nicer to use a type alias!
separately though, it's also better to make it opaque so the library can add new operations as a non-breaking change
(also there's a security consideration there, but that's also a separate topic haha)
but under the hood, that's all it is
the act of making it opaque is just saying "nobody outside the Action module is allowed to pattern match on this, and they can only make new instances of it using the functions I expose"
Okay, so...
Roc defines Effect
Roc platforms define Tasks that wrap Effect
This Roc library defines Action that wraps Effect (so that platforms can put Tasks inside it)
?
so it controls the interface people can use to interact with the type, even though its internal structure is just an ordinary Roc type (hence the name interface modules - their main purpose is to provide interfaces for working with types that are opaque to the module, via strategic use of exposes)
yeah, you've got it!
🤯
also Roc platforms can define what useful operations Effect can do
e.g. a platform can make a new function like httpRequest : Str, OtherStuff -> Effect HttpResponse
but a package library can't do that
(side note, are platforms not packages?)
it can just be like "well there's this Effect thing that exists, and I don't really know what effects it supports, but I can store one anyway as long as someone else gives it to me"
yeah they are - I guess I should have said "library" to disambiguate :big_smile:
so I guess useful terminology for us to adopt might be:
and of course you can get both libraries and platforms from the package manager
(are platforms libraries? Lol)
(I doubt platforms would define platform-agnostic importables, so no)
let's define them as not libraries haha :sweat_smile:
just to have a word to distinguish between platform packages ("platforms") and non-platform packages ("libraries")
Back to the main topic - it sounds like the only reason that a UI state management library would need Effect or Action or anything more than simple data would be so that it can trigger platform effects, yeah?
right
like a user of the library wants to run an effect in response to an event handler
but the library can't possibly know what effects are supported
so it stores an Effect inside Action without knowing what the effect is capable of, and then gives the platform a way to convert from Task (or however they decide to wrap their Effect type) to Action
It just feels like overkill :/ isn't there some way the app can trigger a platform effect in response to an element state change without the library being on the hook (hehe) to wrap that effect type?
Is it time for me to swallow a large FP pill?
so the Elem type has to store the event handler function somehow
otherwise it wouldn't know what code to run when the event fired!
and certainly the library can't know about Task
so it can't say "an Elem stores a Task inside it" - because it can't possibly know what a Task is!
Just wondering if we can prevent the function color virus from spreading...
the "colorful functions" thing is definitely unavoidable if you want to have a distinction between functions that can and can't do effects, which I think is a feature and not a bug :big_smile:
or, to say it another way: "to have un-colorful functions requires having arbitrary side effects in your functions"
so we definitely won't have that!
but whether the function is returning Task or Effect or Action, it's equally colorful
they're just different types that can be converted between using a function call, in the same way that List Str and List I64 are different types that can be converted between with one function call
another way to think about it: if some platform authors decide to use raw Effect, and others decide to use Task, and others decide to use Stream, which one should the graphics library use to be platform-agnostic? Just pick one and have the others be incompatible?
or should it use its own Action type, which also includes the Action.update operation that wouldn't make sense on Task, and then offer a way to convert between that type and whatever effect type the platform actually uses for all of its primitives?
I think the latter is a better design, which is convenient because it's also the only one that's possible :laughing:
I agree - if the async color must bleed into the library, then Action is the best design.
anyway, I gotta run - but thanks for talking through this!
if you have any more thoughts/questions, feel free to leave 'em here and I'll circle back in the morning :smiley:
Thanks for bearing with me! I'm gonna think about this a little more, as I'm optimistic that purely-synchronous state management is possible somehow... I hope to have an aha moment :)
nice cool convo
Last updated: Jun 16 2026 at 16:19 UTC