Stream: ideas

Topic: () for unit type?


view this post on Zulip Richard Feldman (Jan 02 2025 at 15:57):

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:

view this post on Zulip Richard Feldman (Jan 02 2025 at 15:58):

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

view this post on Zulip Richard Feldman (Jan 02 2025 at 15:59):

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:

view this post on Zulip Richard Feldman (Jan 02 2025 at 15:59):

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

view this post on Zulip Richard Feldman (Jan 02 2025 at 16:00):

most functional languages use () as the unit type, and most mainstream languages annotate zero-argument functions as something like () => ...

view this post on Zulip Richard Feldman (Jan 02 2025 at 16:00):

and there's a nice visual connection between a function with the type foo : () => ... being called like foo()

view this post on Zulip Richard Feldman (Jan 02 2025 at 16:02):

so if we made that change, it would mean:

view this post on Zulip Richard Feldman (Jan 02 2025 at 16:02):

any thoughts on this welcome!

view this post on Zulip Brendan Hansknecht (Jan 02 2025 at 16:03):

To be fair, I don't think open tuples should exist. I think they only exist cause open records exist.

view this post on Zulip Brendan Hansknecht (Jan 02 2025 at 16:03):

It's really weird to only be able to specify the prefix of a tuple

view this post on Zulip Sam Mohr (Jan 02 2025 at 16:04):

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.

view this post on Zulip Richard Feldman (Jan 02 2025 at 16:05):

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)

view this post on Zulip Richard Feldman (Jan 02 2025 at 16:06):

so open tuples need to exist for .0 .1 .2 etc. to exist, which in turn means () has to exist

view this post on Zulip Richard Feldman (Jan 02 2025 at 16:06):

otherwise we can't have .0, .1, .2, etc.

view this post on Zulip Brendan Hansknecht (Jan 02 2025 at 16:08):

yeah, realized that after typing my message...what a pain.

view this post on Zulip Richard Feldman (Jan 02 2025 at 16:10):

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?

view this post on Zulip Brendan Hansknecht (Jan 02 2025 at 16:10):

|tuple| tuple.2

a -> b where a.2 : b :lol:

view this post on Zulip Sam Mohr (Jan 02 2025 at 16:12):

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.

view this post on Zulip Richard Feldman (Jan 02 2025 at 16:12):

foo : void => Str would look strange to me :sweat_smile:

view this post on Zulip Sam Mohr (Jan 02 2025 at 16:14):

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."

view this post on Zulip Sam Mohr (Jan 02 2025 at 16:14):

We're just used to either {} or () because they're empty values that already fall out of our type system

view this post on Zulip Sam Mohr (Jan 02 2025 at 16:15):

But something like void communicates "this thing is ignored". Kinda like wildcards...

view this post on Zulip Brendan Hansknecht (Jan 02 2025 at 16:15):

As a note, I don't think that we can type |tuple| tuple.2 today.

view this post on Zulip Sam Mohr (Jan 02 2025 at 16:15):

foo : * => Str haha

view this post on Zulip Brendan Hansknecht (Jan 02 2025 at 16:16):

I guess it should be (a, b)c -> b

view this post on Zulip Brendan Hansknecht (Jan 02 2025 at 16:16):

Apparently tuple.2 is a (*)b which is very wrong.

view this post on Zulip Sam Mohr (Jan 02 2025 at 16:17):

~ 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

view this post on Zulip Sam Mohr (Jan 02 2025 at 16:17):

Whoops

view this post on Zulip Sam Mohr (Jan 02 2025 at 16:17):

It should work, though

view this post on Zulip Brendan Hansknecht (Jan 02 2025 at 16:17):

It does kinda work with roc check, but not really

view this post on Zulip Brendan Hansknecht (Jan 02 2025 at 16:17):

And \a -> a.2 works in normal apps I think.

view this post on Zulip Sam Mohr (Jan 02 2025 at 16:19):

I think the REPL has a hard time compiling generic code that's not made concrete in usage

view this post on Zulip Brendan Hansknecht (Jan 02 2025 at 16:19):

Anyway, () -> Str and {} -> Str both seem fine. More or less equally odd when calling without args.

view this post on Zulip Brendan Hansknecht (Jan 02 2025 at 16:20):

Fundamentally () == {} cause tuples are records under the hood

view this post on Zulip Anthony Bullard (Jan 02 2025 at 16:54):

NOT void NEVER void

view this post on Zulip Anthony Bullard (Jan 02 2025 at 16:55):

Sorry I am traumatized by discussions in TC-39 around partial application and pipelines and pattern matching

view this post on Zulip Sam Mohr (Jan 02 2025 at 16:56):

Hey man, I'm okay with {} or ()

view this post on Zulip Anthony Bullard (Jan 02 2025 at 16:58):

https://github.com/tc39/proposal-discard-binding if you want to see

view this post on Zulip Anthony Bullard (Jan 02 2025 at 16:58):

I'm just trying to bring some humor.

view this post on Zulip Anthony Bullard (Jan 02 2025 at 16:58):

But seriously, not void :pray:

view this post on Zulip Sam Mohr (Jan 02 2025 at 16:58):

I'm very much here for the humor

view this post on Zulip Sam Mohr (Jan 02 2025 at 16:59):

I enjoyed your recent "whomp whomp"

view this post on Zulip Karl (Jan 02 2025 at 17:17):

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.

view this post on Zulip Sam Mohr (Jan 02 2025 at 17:21):

@Karl Why do you think () feels like unit to you but {} doesn't?

view this post on Zulip Karl (Jan 02 2025 at 17:26):

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.

view this post on Zulip Richard Feldman (Jan 02 2025 at 17:27):

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:

view this post on Zulip Brendan Hansknecht (Jan 02 2025 at 17:27):

yeah, same in python

view this post on Zulip Richard Feldman (Jan 02 2025 at 17:30):

that actually seems like a significantly stronger argument for making this change :sweat_smile:

view this post on Zulip Agus Zubiaga (Jan 02 2025 at 17:59):

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