Brendan Hansknecht said:
Yeah, it is convention. Could technically using
NothingorUnitas a tag with a single variant, also no data in those types.
A shameless re-gifting of Brendan's idea: why don't we use Nothing as the conventional unit type? Empty record, empty tuple, etc have a certain amount of historical zero-size/one-possible cleverness about them, but also require a certain amount of explanation.
Nothing is self-describing, and could be introduced in a tutorial as an Aside after covering tags. All that you'd need to know as a reader/learner is that there are no special tags whatsoever (they're all just tags), and optionally that tags are stored efficiently based on the number and nature of the variants. From there, "why Nothing?" is just a "shrug, why not?" answer.
The main counter I could see is a potential concern about Nothing showing up in tag unions, though since it is usually used as input for thunks or as the throwaway result for tasks like Stdout.line, it doesn't seem like it'd propagate too far via open tag unions.
For me, I like {} cause it is super short and looks kinda like a function call or struct initialization in context. Dict.empty {} vs Dict.empty Nothing
I guess I would potentially prefer empty tuple better, but idk Dict.empty ()
Does empty tuple work? In the repl on the main site page, (1, 2) works but () does not. The tutorial also doesn't mention them at this time. Tuples are syntactically, because (1) can't be a tuple without introducing ambiguity or magic.
Python has (1,) for single-element tuples, which is also awkward.
Roc could disallow single element tuples, but then should also disallow zero-element tuples for consistency.
Side note: I wonder why Dict.empty is a function rather than just a value. As an immutable language, there shouldn't theoretically be any need to make a thunk-like wrapping just to initialize an empty data structure.
Yeah, empty tuple doesn't work, but I think it could be nice to add.
Also, Dict.empty being required to be a function is related to specialization and monomorphization. I would need to dig to find the doc.
@Ayaz Hafiz do you have an easy link to your "let specialization, let's not" doc?
The base is that it makes the compiler type checking a lot more complicated because you now have a Dict.empty value that is trying to be used as many different concrete types.
It is a Dict Str I32 and Dict Something Str and etc.
Seems like a possible place for special casing, i.e. make some built-in thunky things look like values, but underneath rewrite them into functions if that's the path of least resistance for the implementation. The benefit is an unnecessary implementation detail is kept out of the core modules.
Brendan Hansknecht said:
Yeah, empty tuple doesn't work, but I think it could be nice to add.
so that's how it's done in Rust, and also in Elm, but in Elm it's always bothered me a bit that both {} and () exist and there's a convention to always use () so I wanted to have only one in the language so there could be more of an obvious one way to do it
it was also obvious at the time to go with {} because back then we had records but not tuples
Seems like a possible place for special casing, i.e. make some built-in thunky things look like values, but underneath rewrite them into functions if that's the path of least resistance for the implementation.
Yeah, that might be a way to go, but it only essentially affects creating empty data structures. So very minor gains. Also, if done accidentally, it could lead to allowing something to be two different types when it really should be a type mismatch. I think this is a case where the cost is so minor that we mostly aren't concerned, but some special complexity could be added if there ends up being enough demand.
My thought is that the implementation may get sophisticated enough later to handle gradual type inference from a * state, or it might shift to a fundamentally different internal paradigm with different tradeoffs.
At some point we'll probably want to lock down the language to start ensuring compatibility. At that point, we'll have locked-in oddities that are solely there due to perhaps arbitrary implementation constraints rather than due to language design reasons.
also the creating data structures case is a temporary state of affairs
Dict.empty shouldn't need to be a function in the future
Oh, we have a solution for that?
I thought ayaz's doc was about wanting to keep that as a a function
so there are some cases where we can be like "ok this is a variation on a builtin that we know" (e.g. a wrapper around List) so we can allow it
Ah
Cool
Would non-builtin data types (like opaque types that internally use the builtins) need to still jump through thunk hoops to provide an empty/zero value once the builtins are de-thunked, or will the de-thunking transitively benefit custom types as well?
Ayaz knows more than I do, but I think it's only builtins. Otherwise we end up with the "Let's Not" problem again.
https://rwx.notion.site/Let-generalization-Let-s-not-742a3ab23ff742619129dcc848a271cf
The problem is more than just implementation complexities, though restricting polymorphism does ease a lot of things. It's also that making
value : Result (Num *) Err
value = someComplicatedPolymorphicDecode "1" # I'm any number!
is a bad idea in the absence of at least constant evaluation because now you actually have N copies of these values, and they all must be re-evaluated each time they're called
Supporting Dict.empty = @Dict {} (or any other function defined in such a way that it consists only of literals) to be eligible for polymorphism would be pretty simple to add today - it's a purely syntactic check. It's just that right now, the syntactic check for whether something can be polymorphic only admits values that look like a number, or a lambda (\... -> ...)
That makes sense. Polymorphic decodes, even with explicit hinting (like Rust's into stuff) are a bit magical. I could see a language like Idris doing that, but it does seem like that kind of thing is ruled out as a non-goal for Roc.
Though anything like [] |> List.append 5 or Dict.empty |> Dict.insert 1 2, or MyTree.empty |> MyTree.insert "a" "b" are all something I'd eventually hope would work without thunks for the empty initializers.
It's gradual type inference/specialization, but something that theoretically should be deterministic at compile time.
Ayaz Hafiz said:
Supporting
Dict.empty = @Dict {}(or any other function defined in such a way that it consists only of literals) to be eligible for polymorphism would be pretty simple to add today - it's a purely syntactic check.
interesting! could this be a good first issue for someone new to the compiler, given a write-up of how to do it?
Yeah
I would be a big fan of using () over {} and either over Nothing. Anything that significantly increases line length is worth avoiding IMO, more length means more breaks, more breaks means less code on screen.
Also I think () is better just because every other language I'm aware of uses it as the "unit" type and I think for something where it doesn't matter it's worth following convention.
What would be the one-tuple syntax though?
There doesn't have to be one. () isn't being used as a 0-length tuple, it's being used as an arbitrary value, and we don't have any need for 0- or 1-length tuples.
I suppose the concern I've got is the question of internal consistency.
(1, 2) is the syntax for introducing tuples, thus () would most likely be interpreted as a zero-element tuple.() is alignment with other languages, there are many things in Roc that intentionally deviate. It could've been syntax-identical to Elm, for example (as NodeJS is to browser JS). Or we could have curly braces to delimit scope, and parens to wrap function params and calls: that would be the most approachable to the largest group of programmers. To them, both () and {} will be equally obscure. My point is that in terms of approachability, the difference between () and {} is probably negligible.() is _not_ a tuple, but instead just a new token, value, and type (perhaps called unit). That would remove the awkwardness of having a 0- and 2-tuples but not 1-tuples. However, we'd still have {} either way, and would've introduced something new to the language that was already fulfilled by something else.As such, I believe {} is the better choice (it introduces fewer special cases to the language).
As an exception, if tuples just desugared to records (e.g. if (A, B) were equivalent to {f1: A, f2: B}), then {} and () would be identical, the choice would distill down to just a stylistic convention. It also wouldn't matter if 1-tuples could be constructed, because they'd just be records anyway.
Tuples do desugar to records
Just with special number fields
That is why tuple.0 works
I missed that discussion. Does List.map .0 work to get the first element of each tuple, or does .0 parse as a Frac literal?
And can number fielded-records be constructed using record syntax, and have a mix of number and non-number fields?
Will get the first element of a tuple
They cannot be constructed with record syntax. They are special to tuples.
Thanks
The "desugaring to records" means I don't really have much of an opinion on () vs {}
oh actually we ended up going with a separate (but still extensible) type for tuples, which is what is currently implemented
the plan for awhile was to do the "records but with numbers for fields" design but it ended up changing before the implementation
@Richard Feldman is this the thread corresponding to what ended up being selected for the language?
Oh, I guess I missed that change...oops
yep!
Last updated: Jun 16 2026 at 16:19 UTC