building on #ideas > zero-arg function syntax - we've always used {} for our unit type instead of () (which is what most typed functional programming languages, and also Rust, use) mostly because we originally did not have tuples in the language, and it seemed weird to have an "empty tuple" in a language that did not have tuples at all :big_smile:
today we do have tuples, and () is technically needed in the language in order to make an open tuple closed - e.g. if I have a tuple with the type (Str, Str)a and I want to convert that to (Str, Str), I have to be able to make a become (), which in turn implies that the type system has to have a concept of (), which in turn implies that in order to implement a function that closes a tuple, there must also be a () value in the language
however, this is basically a super strange edge case that comes up approximately never, and I'm not even sure if we ever bothered to implement it even though technically it should be there :big_smile:
but if we're using foo() to be syntax sugar for foo({}), and then having the type of foo be {} => ... I wonder if we should reconsider
most functional languages use () as the unit type, and most mainstream languages annotate zero-argument functions as something like () => ...
and there's a nice visual connection between a function with the type foo : () => ... being called like foo()
so if we made that change, it would mean:
() is used the same way in both languagesany thoughts on this welcome!
To be fair, I don't think open tuples should exist. I think they only exist cause open records exist.
It's really weird to only be able to specify the prefix of a tuple
I'm not personally seeing much of a benefit besides the familiarity aspect for Rust/FP devs. Both {} and () still need to be desugared for "zero-arg functions", and they compile to the same thing.
Brendan Hansknecht said:
To be fair, I don't think open tuples should exist. I think they only exist cause open records exist.
they have to exist for the type system to work; for example, if I put this into the repl:
|tuple| tuple.2
...what is the inferred type of that function? (Keep in mind that calling it should work on tuples with lengths greater than 3 as well)
so open tuples need to exist for .0 .1 .2 etc. to exist, which in turn means () has to exist
otherwise we can't have .0, .1, .2, etc.
yeah, realized that after typing my message...what a pain.
Sam Mohr said:
I'm not personally seeing much of a benefit besides the familiarity aspect for Rust/FP devs. Both
{}and()still need to be desugared for "zero-arg functions", and they compile to the same thing.
another way to think of it is: pretend we don't already have {} implemented, which one would we choose knowing what we do about the language today?
|tuple| tuple.2
a -> b where a.2 : b :lol:
If neither of them was used already, I'd reach for void or something more communicative. The only reason I'd pick () over {} is because of my burning passion for Rust, which I try to discount.
foo : void => Str would look strange to me :sweat_smile:
I think foo : {} => Str also looks strange. "Why is it taking an empty record? It can't use any fields, then, must be because it's ignored."
We're just used to either {} or () because they're empty values that already fall out of our type system
But something like void communicates "this thing is ignored". Kinda like wildcards...
As a note, I don't think that we can type |tuple| tuple.2 today.
foo : * => Str haha
I guess it should be (a, b)c -> b
Apparently tuple.2 is a (*)b which is very wrong.
~ 7s
❯ roc repl
The rockin' roc repl
────────────────────────
Enter an expression, or :help, or :q to quit.
» x = \a -> a.2
An internal compiler expectation was broken.
This is definitely a compiler bug.
Please file an issue here: <https://github.com/roc-lang/roc/issues/new/choose>
failed to find fn symbol for "#UserApp_replOutput_17910680071577632132"
Location: crates/compiler/gen_dev/src/object_builder.rs:1063:21
Whoops
It should work, though
It does kinda work with roc check, but not really
And \a -> a.2 works in normal apps I think.
I think the REPL has a hard time compiling generic code that's not made concrete in usage
Anyway, () -> Str and {} -> Str both seem fine. More or less equally odd when calling without args.
Fundamentally () == {} cause tuples are records under the hood
NOT void NEVER void
Sorry I am traumatized by discussions in TC-39 around partial application and pipelines and pattern matching
Hey man, I'm okay with {} or ()
https://github.com/tc39/proposal-discard-binding if you want to see
I'm just trying to bring some humor.
But seriously, not void :pray:
I'm very much here for the humor
I enjoyed your recent "whomp whomp"
I would appreciate this. I've actually never explicitly associated {} with unit, just what you write when there are no arguments. Looking back, my thought process would be roughly "{} is falsy" or a different syntax for nil punning so I blame JS/Clojure but mostly JS.
@Karl Why do you think () feels like unit to you but {} doesn't?
Familarity. I've never gotten into the Haskell family of languages but the ML family uses (). The other part is that an empty object/map/dict is a fairly common thing in dynamic languages.
Karl said:
an empty object/map/dict is a fairly common thing in dynamic languages.
I never considered this, but it's a good point. I guess {} actively means something different in TypeScript, for example. :thinking:
yeah, same in python
that actually seems like a significantly stronger argument for making this change :sweat_smile:
I don't feel strongly about it, but I only see upsides to making this change.
Last updated: Jun 16 2026 at 16:19 UTC