Hi! I'm experimenting with building a simple elm-like library for rendering & updating TUI apps, e.g. vim-like text editors, etc.
I'm trying to model Signal's something like the following (haskell), but unsure how to express that in Roc:
data Signal a where
KeyPressed :: char -> Signal char
Map :: Signal b -> (b -> a) -> Signal a
So, a couple questions:
No, it's not possible in Roc. If you require the type b
to be well known I'm not sure you can model this directly without type erasure. Can you pull out map
so that it's a function over Signal rather than a constructor? Guessing not, but curious what the code is like.
I don't think I need it, strictly speaking. I was mostly trying to avoid exposing myself to the unbridled power that is Task.
In fact yeah; I should just be using Task here. I'm making things unnecessarily difficult for myself :eyeroll:
It would be nice to e.g. guarantee that random code that's trying to render editor state can't also be printing to the console except thru sanctioned means (returning a new Node)
I don't suppose GADTs have been discussed? On the table for the future? Too complicated to be worth it?
I don't think they've been discussed.
re: limiting task
we didn't end up doing it in roc-ray, but an approach I/we thought about was stubbing in fake error types to make, eg, draw calls not compile during init
Joshua Warner said:
I don't suppose GADTs have been discussed? On the table for the future? Too complicated to be worth it?
they haven't been discussed because I think they require #ideas > nominal types - which is in the stage of "vague idea" :big_smile:
but I think the Signal b -> (b -> a) -> Signal a
part of that would require type-erased closures, right @Ayaz Hafiz?
that's one option. it requires existentials or that
can existentials be compiled to first-order functions? :thinking:
I thought they had the same problems in common with rank-N types
they can be yes
hm, so why would this work with our compilation models when rank-N types wouldn't?
rank N types can work
hm I must be missing something haha
I thought we concluded they couldn't work
if I understand correctly, https://www.microsoft.com/en-us/research/wp-content/uploads/2009/09/implication_constraints.pdf?from=https://research.microsoft.com/en-us/um/people/simonpj/papers/gadt/implication_constraints.pdf&type=exact says GADTs can be implemented in way that preserves principal decidable type inference
so that wouldn't be a blocker
in theory it’s doable
the question probably should be is it worth doing
it occurred to me that I believe GADTs would make it possible from a types perspective to do Elm Architecture
without needing multiple type parameters like you would today
but Elm's Html.lazy
still relies on persistent data structures to be fast, which remains the thing that would scare me away from using a Roc rendering system based on the Elm Architecture on a large UI
Why does lazy need a persistent data structure and not just a dictionary?
what Html.lazy
does in Elm is:
lazy
has been called with that function and that argument, it calls the function passing the argument, and returns the return value.with persistent data structures, it can do a "shallow equality check" of the cloned argument - that is, assuming it's a persistent data structure (where you can have a different outermost node, but inside it are all the same addresses as the one that was stored, so you don't have to recursively go traverse the entire data structure to see if it's equal), then you can have a very fast equality check as long as you're okay with false negatives (where it says they are unequal but actually if you traversed the entire data structure, you'd discover they were actually equal).
with shallow equality, it doesn't really matter if the argument is big nested data structure because it's not traversing the whole thing
e.g. in a long linked list (which is what Elm uses for List
) you only compare the first elem
and if they point to the same next elem, you're done; they're definitely equal
if not, then they might not be equal, and rather than traversing the entire thing to find out, you just assume they're unequal and re-evaluate the function
but "shallow equality" isn't a thing if you have opportunistic mutation instead of persistent data structures
so our only options would be to deeply clone the entire arg, and then deeply compare for equality every time
which could potentially wipe out the performance improvement that lazy
brings (especially because the happy path is potentially much more expensive), which is a scary prospect to me because lazy
was by far the most effective tool I ever had for addressing performance problems in the Elm Architecture
without that, I would be afraid that I'd get to a certain point where performance problems were just unsolvable
maybe that wouldn't happen in practice, but the only way to really be sure is to build a big complicated application and find out the hard way, which is not something I'd personally be interested in :sweat_smile:
Oh, I understand. Yeah, that does make it harder by default
Would need to be more carefully applieds
May want to make persistent data structures with tags if this kinda structure is needed.
I think it just means we need to find a UI architecture that's a better fit for Roc
after all, the reason Evan named it "The Elm Architecture" was based on his view that it's the architecture that naturally emerges if you have Elm's design constraints, but Roc has some relevantly different design decisions :big_smile:
Fair enough, but lazy is a much more general concept than TEA. Similar things are all over the place, like @cache
in python
Also, TRA isn't as awesome of an acronym....
on the topic of TEA, I recently came across https://sutil.dev/#documentation-about-sutil
which is essentially svelte but F#, and has a optional module that "recreates" TEA
I didn't dive deep but I think it's a better approach than the Elm one since we can't really do pointer equality
Last updated: Jul 06 2025 at 12:14 UTC