Stream: ideas

Topic: gui system


view this post on Zulip Richard Feldman (Feb 14 2022 at 04:21):

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

view this post on Zulip Anton (Feb 14 2022 at 13:30):

Looking solid :+1:
Another naming option for Elem.translate could be Elem.transfer, although I think translate is pretty good.

view this post on Zulip Johannes Maas (Feb 14 2022 at 13:43):

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.

view this post on Zulip Anton (Feb 14 2022 at 14:03):

We can have a rule for that in our own counterpart of elm-analyse that is enabled by default in the editor.

view this post on Zulip Pit Capitain (Feb 14 2022 at 15:51):

@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:

    increase = button {
        label: text "+",
        onPress: \prev, _ -> Action.update (prev + 1),
    }

view this post on Zulip Pit Capitain (Feb 14 2022 at 15:54):

Forget my first remark, your examples are consistent with the type signature...

view this post on Zulip Pit Capitain (Feb 14 2022 at 16:01):

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.

view this post on Zulip Martin Stewart (Feb 14 2022 at 18:38):

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.

view this post on Zulip Folkert de Vries (Feb 14 2022 at 19:16):

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

view this post on Zulip Folkert de Vries (Feb 14 2022 at 19:16):

on the other hand, that makes scaling an app easier

view this post on Zulip Lucas Rosa (Feb 14 2022 at 22:32):

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

view this post on Zulip Richard Feldman (Feb 14 2022 at 23:01):

yeah I think this is an unavoidable tradeoff unfortunately

view this post on Zulip Philippe (Feb 14 2022 at 23:03):

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?

view this post on Zulip Lucas Rosa (Feb 14 2022 at 23:04):

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

view this post on Zulip Lucas Rosa (Feb 14 2022 at 23:04):

I like this doc though, cool stuff

view this post on Zulip Richard Feldman (Feb 14 2022 at 23:07):

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!

view this post on Zulip Richard Feldman (Feb 14 2022 at 23:07):

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:

view this post on Zulip Richard Feldman (Feb 14 2022 at 23:08):

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

view this post on Zulip Richard Feldman (Feb 14 2022 at 23:10):

we can do so much better!

view this post on Zulip Lucas Rosa (Feb 14 2022 at 23:18):

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

view this post on Zulip Brendan Hansknecht (Feb 14 2022 at 23:18):

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.

view this post on Zulip Brendan Hansknecht (Feb 14 2022 at 23:18):

Otherwise, this looks like a nice proposal to bring TEA to Roc.

view this post on Zulip Lucas Rosa (Feb 14 2022 at 23:19):

one key difference from TEA is how the updates work

view this post on Zulip Lucas Rosa (Feb 14 2022 at 23:19):

having just a model and a render function feels more like react

view this post on Zulip Brendan Hansknecht (Feb 14 2022 at 23:19):

That's true

view this post on Zulip Brendan Hansknecht (Feb 14 2022 at 23:19):

but that honestly doesn't make much of a difference from my perspective

view this post on Zulip Lucas Rosa (Feb 14 2022 at 23:21):

I do agree with the concern for the line you quoted

view this post on Zulip Lucas Rosa (Feb 14 2022 at 23:22):

&left sugar feels a bit odd. maybe because I'm just used to thinking that's a reference

view this post on Zulip Brendan Hansknecht (Feb 14 2022 at 23:22):

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

view this post on Zulip Brendan Hansknecht (Feb 14 2022 at 23:23):

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

view this post on Zulip Brendan Hansknecht (Feb 14 2022 at 23:25):

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.

view this post on Zulip Brendan Hansknecht (Feb 14 2022 at 23:46):

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.

view this post on Zulip jan kili (Feb 15 2022 at 00:04):

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 }

view this post on Zulip jan kili (Feb 15 2022 at 01:15):

Actually, I'm just assuming platforms are expected to expose an Action module just like they expose a Task module - is that accurate?

view this post on Zulip jan kili (Feb 15 2022 at 01:17):

I see now that the doc doesn't define where the Action record comes from.

view this post on Zulip jan kili (Feb 15 2022 at 01:18):

I don't see how the first example is valid, I'd expect the compiler to complain about Action or Action.none being undefined.

view this post on Zulip Brendan Hansknecht (Feb 15 2022 at 01:30):

The platform definitely should be exposing Action. If it wasn't defined by the platform, the platform wouldn't be able to handle actions.

view this post on Zulip jan kili (Feb 15 2022 at 02:08):

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.

view this post on Zulip jan kili (Feb 15 2022 at 02:14):

That would be simpler for app developers and more expressive for platform developers: [ Update, Delegate, None ] instead of [ Action ]

view this post on Zulip jan kili (Feb 15 2022 at 02:17):

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

view this post on Zulip Richard Feldman (Feb 15 2022 at 02:46):

I have a partially-written doc about how the reuse across platform works, but I can give some quick notes

view this post on Zulip Richard Feldman (Feb 15 2022 at 02:50):

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.

view this post on Zulip Richard Feldman (Feb 15 2022 at 02:51):

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

view this post on Zulip Richard Feldman (Feb 15 2022 at 02:51):

so the next question is how to deal with the fact that platforms support different effects

view this post on Zulip Richard Feldman (Feb 15 2022 at 02:52):

let's use an autocomplete dropdown as an example

view this post on Zulip Richard Feldman (Feb 15 2022 at 02:52):

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

view this post on Zulip Richard Feldman (Feb 15 2022 at 02:52):

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

view this post on Zulip Richard Feldman (Feb 15 2022 at 02:53):

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?

view this post on Zulip Richard Feldman (Feb 15 2022 at 02:55):

basically I make my API look something like this:

State : { entries : List Str, onInput : (Str -> Action (List Str)) }

Autocomplete.render  : State -> Elem State

view this post on Zulip Richard Feldman (Feb 15 2022 at 02:56):

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

view this post on Zulip Richard Feldman (Feb 15 2022 at 02:56):

so now, let's say I'm on a platform that has HTTP tasks

view this post on Zulip Richard Feldman (Feb 15 2022 at 02:58):

I call this passing something like:

{
    entries,
    onInput: \str ->
        Http.get "/autocomplete?query={str}"
            |> Task.toAction \result -> httpResponseToAction result
}

view this post on Zulip Richard Feldman (Feb 15 2022 at 02:58):

and that's it!

view this post on Zulip Richard Feldman (Feb 15 2022 at 02:59):

and actually, I don't have to do a HTTP request here

view this post on Zulip Richard Feldman (Feb 15 2022 at 02:59):

I can do any kind of effect I want to load the autocomplete results, e.g. read them from a file or a databsae

view this post on Zulip Richard Feldman (Feb 15 2022 at 02:59):

*database

view this post on Zulip Richard Feldman (Feb 15 2022 at 02:59):

or just return Action.update to have them come from a hardcoded list in memory, that's fine too

view this post on Zulip Richard Feldman (Feb 15 2022 at 03:01):

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

view this post on Zulip Richard Feldman (Feb 15 2022 at 03:01):

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

view this post on Zulip Richard Feldman (Feb 15 2022 at 03:01):

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

view this post on Zulip Richard Feldman (Feb 15 2022 at 03:01):

so...that's the idea!

view this post on Zulip Richard Feldman (Feb 15 2022 at 03:04):

(oh, and :point_up: is one reason it's important to actually have a separate Action type!)

view this post on Zulip Richard Feldman (Feb 15 2022 at 03:09):

also note that in a real-world scenario I'd have written a proper tutorial explanation of how to do all this :laughing:

view this post on Zulip jan kili (Feb 15 2022 at 03:11):

also note that

Haha no judgement or expectations! I think the current system design process works great.

view this post on Zulip jan kili (Feb 15 2022 at 03:21):

What is Action.fromTask Task.toAction actually doing in that last snippet? Why can't that |> chain simply end with something like |> \suggestions -> Update suggestions ?

view this post on Zulip Richard Feldman (Feb 15 2022 at 03:23):

oops, that should have been Task.toAction

view this post on Zulip Richard Feldman (Feb 15 2022 at 03:24):

(edited to fix it!)

view this post on Zulip jan kili (Feb 15 2022 at 03:24):

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?

view this post on Zulip Richard Feldman (Feb 15 2022 at 03:25):

so I guess I need to get into this a bit more

view this post on Zulip Richard Feldman (Feb 15 2022 at 03:26):

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

view this post on Zulip jan kili (Feb 15 2022 at 03:26):

(this may be my Elm ignorance speaking)

view this post on Zulip Richard Feldman (Feb 15 2022 at 03:26):

(nope, nothing to do with Elm in this case!)

view this post on Zulip Richard Feldman (Feb 15 2022 at 03:26):

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

view this post on Zulip Richard Feldman (Feb 15 2022 at 03:27):

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)

view this post on Zulip Richard Feldman (Feb 15 2022 at 03:27):

so this is a new concept for the compiler: packages can use the Effect type without knowing what its capabilities are

view this post on Zulip Richard Feldman (Feb 15 2022 at 03:28):

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"

view this post on Zulip Richard Feldman (Feb 15 2022 at 03:29):

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

view this post on Zulip Richard Feldman (Feb 15 2022 at 03:30):

so it's actually storing functions in there, in addition to non-function data

view this post on Zulip Richard Feldman (Feb 15 2022 at 03:31):

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

view this post on Zulip Richard Feldman (Feb 15 2022 at 03:31):

but that's pretty big and clumsy, so definitely would be nicer to use a type alias!

view this post on Zulip Richard Feldman (Feb 15 2022 at 03:31):

separately though, it's also better to make it opaque so the library can add new operations as a non-breaking change

view this post on Zulip Richard Feldman (Feb 15 2022 at 03:32):

(also there's a security consideration there, but that's also a separate topic haha)

view this post on Zulip Richard Feldman (Feb 15 2022 at 03:33):

but under the hood, that's all it is

view this post on Zulip Richard Feldman (Feb 15 2022 at 03:33):

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"

view this post on Zulip jan kili (Feb 15 2022 at 03:34):

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

view this post on Zulip Richard Feldman (Feb 15 2022 at 03:34):

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)

view this post on Zulip Richard Feldman (Feb 15 2022 at 03:35):

yeah, you've got it!

view this post on Zulip jan kili (Feb 15 2022 at 03:35):

🤯

view this post on Zulip Richard Feldman (Feb 15 2022 at 03:35):

also Roc platforms can define what useful operations Effect can do

view this post on Zulip Richard Feldman (Feb 15 2022 at 03:35):

e.g. a platform can make a new function like httpRequest : Str, OtherStuff -> Effect HttpResponse

view this post on Zulip Richard Feldman (Feb 15 2022 at 03:35):

but a package library can't do that

view this post on Zulip jan kili (Feb 15 2022 at 03:36):

(side note, are platforms not packages?)

view this post on Zulip Richard Feldman (Feb 15 2022 at 03:36):

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"

view this post on Zulip Richard Feldman (Feb 15 2022 at 03:36):

yeah they are - I guess I should have said "library" to disambiguate :big_smile:

view this post on Zulip Richard Feldman (Feb 15 2022 at 03:37):

so I guess useful terminology for us to adopt might be:

view this post on Zulip Richard Feldman (Feb 15 2022 at 03:37):

and of course you can get both libraries and platforms from the package manager

view this post on Zulip jan kili (Feb 15 2022 at 03:37):

(are platforms libraries? Lol)

view this post on Zulip jan kili (Feb 15 2022 at 03:38):

(I doubt platforms would define platform-agnostic importables, so no)

view this post on Zulip Richard Feldman (Feb 15 2022 at 03:38):

let's define them as not libraries haha :sweat_smile:

view this post on Zulip Richard Feldman (Feb 15 2022 at 03:38):

just to have a word to distinguish between platform packages ("platforms") and non-platform packages ("libraries")

view this post on Zulip jan kili (Feb 15 2022 at 03:39):

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?

view this post on Zulip Richard Feldman (Feb 15 2022 at 03:39):

right

view this post on Zulip Richard Feldman (Feb 15 2022 at 03:39):

like a user of the library wants to run an effect in response to an event handler

view this post on Zulip Richard Feldman (Feb 15 2022 at 03:40):

but the library can't possibly know what effects are supported

view this post on Zulip Richard Feldman (Feb 15 2022 at 03:40):

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

view this post on Zulip jan kili (Feb 15 2022 at 03:41):

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?

view this post on Zulip jan kili (Feb 15 2022 at 03:42):

Is it time for me to swallow a large FP pill?

view this post on Zulip Richard Feldman (Feb 15 2022 at 03:42):

so the Elem type has to store the event handler function somehow

view this post on Zulip Richard Feldman (Feb 15 2022 at 03:42):

otherwise it wouldn't know what code to run when the event fired!

view this post on Zulip Richard Feldman (Feb 15 2022 at 03:42):

and certainly the library can't know about Task

view this post on Zulip Richard Feldman (Feb 15 2022 at 03:43):

so it can't say "an Elem stores a Task inside it" - because it can't possibly know what a Task is!

view this post on Zulip jan kili (Feb 15 2022 at 03:43):

Just wondering if we can prevent the function color virus from spreading...

view this post on Zulip Richard Feldman (Feb 15 2022 at 03:44):

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:

view this post on Zulip Richard Feldman (Feb 15 2022 at 03:45):

or, to say it another way: "to have un-colorful functions requires having arbitrary side effects in your functions"

view this post on Zulip Richard Feldman (Feb 15 2022 at 03:45):

so we definitely won't have that!

view this post on Zulip Richard Feldman (Feb 15 2022 at 03:45):

but whether the function is returning Task or Effect or Action, it's equally colorful

view this post on Zulip Richard Feldman (Feb 15 2022 at 03:45):

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

view this post on Zulip Richard Feldman (Feb 15 2022 at 03:47):

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?

view this post on Zulip Richard Feldman (Feb 15 2022 at 03:48):

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?

view this post on Zulip Richard Feldman (Feb 15 2022 at 03:48):

I think the latter is a better design, which is convenient because it's also the only one that's possible :laughing:

view this post on Zulip jan kili (Feb 15 2022 at 03:48):

I agree - if the async color must bleed into the library, then Action is the best design.

view this post on Zulip Richard Feldman (Feb 15 2022 at 03:49):

anyway, I gotta run - but thanks for talking through this!

view this post on Zulip Richard Feldman (Feb 15 2022 at 03:49):

if you have any more thoughts/questions, feel free to leave 'em here and I'll circle back in the morning :smiley:

view this post on Zulip jan kili (Feb 15 2022 at 03:50):

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

view this post on Zulip Lucas Rosa (Feb 15 2022 at 05:44):

nice cool convo


Last updated: Jun 16 2026 at 16:19 UTC