What's a good way to represent trees in Roc? I'd like every node (whether they're leaves or not) to have identical fields, such as label
, but non-leaf nodes also have a children
field.
Is the following idiomatic?
Tree : [Leaf { label : Str }, Node { label : Str, children : List Tree}]
I don't think we have enough Roc code out there to say whether your way is idiomatic, but it's an obvious and clean implementation using our type system effectively
One thing though, do you want to allow for empty trees?
This doesn't currently allow for that
You'd want something like
Tree : [Empty, Node { label : Str, children : List Tree }]
Thanks Sam. It's for an exercism exercise which does not have any test cases for empty trees, but I think it still makes sense to allow them. :+1:
Ah yeah, this is a case where a tag union shouldn't even be required, but recursion check limitation in the roc compiler make it required.
I was wondering about that: the compiler could see the List
and understand that an empty list can stop the recursion. In fact, this is true of any container that can be empty.
Yeah, which in roc is just tags and lists. Cause all other containers that can be empty would be built on those primitives
Oh, and I guess optional boxes of the recursive type. (or a box of an option of the recursive type)
I'm working on the pov
(point of view) exercise which involves manipulating trees. I used the definition suggested by @Sam Mohr :
Tree : [Empty, Node { label : Str, children : List Tree }]
However, I just realized that I should have used Set Tree
instead of List Tree
for the children, because the exercise explicitly requires us to ignore the order of the children.
However, I can't create a Set
of trees, because trees are not hashable, it seems, probably because they contain a Set
. I'm not sure how to do that without making Tree
an opaque type. In fact, even if I make it an opaque type and use implements [Hash { hash: hashTree }]
, the compiler crashes when I try to add a tree to a Set
. I'm a bit stuck, so any help would be much appreciated! :thank_you:
Set
is also ordered. Just for reference
It also should be hashable
Maybe I missed that though
Yeah, set had a hash implementation....maybe a compiler bug....not sure at this moment
Can you share the source? I'll take a look later
I haven’t read the instructions so not sure about the context, but would it work to keep using a list but make the tests ignore order? That might be easier for the students
oh, they are unordered for comparison only...ah...yeah, set is better
It has an internal order that a user can get access to, but it compares unordered
Oh, this looks like a recursion bug if I had to guess. I think we may have an issue with ability specialization for recursive types.
repro:
app [main] { pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.15.0/SlwdbJ-3GR7uBWQo6zlmYWNYOxnvo8r6YABXD-45UOw.tar.br" }
import pf.Stdout
Tree : [Empty, Node { label : Str, children : Set Tree }]
main =
x : Tree
x = Node { label: "a", children: Set.fromList [] }
Inspect.toStr x
|> Stdout.line!
Error:
thread 'main' panicked at crates/compiler/load_internal/src/file.rs:685:26:
way too many specialization passes!
cc @Ayaz Hafiz just in case you have any immediate comments that might help whoever debug this.
well that's wild
the only case where that happens is if you have infinite specialization
i.e. polymorphic recursion
Let's see:
Request hash for Tree
-> Set Tree
-> Request hash impl for Dict Tree {}
-> Hash.hashUnordered
on the List (Tree, {})
-> Request hash for (Tree, {})
-> Back to the top? This is where it should cutoff and call to the generated Tree
hash function, but it apparently is looping forever.
yeah not sure what's happening
Seems to be specific to the container being part of the recursion:
This can be hashed Tree : [Empty, Node { label : Str, children : Tree }]
But these two will fail to hash:
Tree : [Empty, Node { label : Str, children : Set Tree }]
Tree : [Empty, Node { label : Str, children : List Tree }]
Thanks for looking into this. Perhaps issue #7098 is also linked to this infinite recursion bug?
Isaac Van Doren said:
I haven’t read the instructions so not sure about the context, but would it work to keep using a list but make the tests ignore order? That might be easier for the students
That's actually what I did, but if sets can be hashed and compared for equality while ignoring the order, then they pretty much fit the exercise perfectly, and I think it the code will be simpler for the users. Plus we don't have any exercise that uses sets yet, so it's properly a good chance to use them. And also this exercise is the hardest I'm seen yet, so users will be expecting (and probably hoping for) some complexity.
#7098 is different. You just have infinite recursion leading to a stack overflow (really not sure why so many roc stack overflows lead to segfaults)
help node1.children node2.children
instead of help [tree1] [tree2]
And yeah, Set
is definitely the right tool here once we fix the hashing bug here.
Oh yes, I missed the infinite recursion, thanks @Brendan Hansknecht . The code continues to crash, but it looks like it's because of the List.dropAt
issue (#7065), which you recently fixed. With these two fixes, the code works fine.
I closed #7098, since it was a bug in my code (+ the List.dropAt
issue #7065).
I also opened #7108 regarding the infinite loop issue with recursive types involving containers.
And if you're interested in the Exercism exercise that I'm working on, I submitted a draft PR (the code isn't fully tested yet due to #7108).
Thanks for logging things!
I just noticed a bug related to sets and comparison so thought I would mention it here. When sets are compared within tags, the internal order matters:
# Passes
expect Set.fromList [1, 2] == Set.fromList [1, 2]
# Passes
expect Set.fromList [1, 2] == Set.fromList [2, 1]
# Passes
expect Ok (Set.fromList [1, 2]) == Ok (Set.fromList [1, 2])
# Fails
expect Ok (Set.fromList [1, 2]) == Ok (Set.fromList [2, 1])
Wild guess. We aren't correctly resolving abilities and using the opaque set equality operator when it is wrapped in a tag. Instead, it is doing a raw comparison of the internal impl as if the opaque type wasn't there.
Last updated: Jul 05 2025 at 12:14 UTC