Stream: ideas

Topic: zero-arg function syntax


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

I just realized a fairly obvious design for "zero-arg functions" - just say that foo() is syntax sugar for foo({}) and everything else works the same way as today

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

there's already precedent for this in inferring "statements" to have a value of {}

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

so then we don't have zero-arg function syntax (or a semantic concept), but rather the type is always {} => ...

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

but you can still call it with foo() instead of having to write foo({})

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

We'd want this for functions, but not method calls on values, presumably. That would allow this:

split_lines : Str -> List Str

s = "abc\ndef"
lines = s.split()

to coexist with this

Foo := { data: U64 }

new : {} -> Foo

my_foo = new()

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

If so, I'm on board!

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

yeah, I like it

view this post on Zulip Anthony Bullard (Jan 02 2025 at 17:01):

Yes for plain applys, and not in methods

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

I guess this implies that we don't need to overload what || means anymore

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

can just do thunk = |{}| ... or thunk = |()| ... depending on #ideas > () for unit type?

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

|()| is definitely a TIE fighter

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

For now yes. Though I don't think it was really an ambiguous problem, and I think the || syntax will still be good if we ever do decide to do actual zero-arg functions

view this post on Zulip Ayaz Hafiz (Jan 02 2025 at 18:04):

we can’t we have zero arg functions right now? because there’s no way to annotate them?

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

Yep

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

But I don't think there's a problem otherwise

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

some nice symmetry with the type if we don't use ||

do_nothing : () -> Logger
do_nothing = |()|
    Logger.{ write_raw!: |_, _| () }

view this post on Zulip Ayaz Hafiz (Jan 02 2025 at 18:05):

what if we wrapped the type annotation arguments with || to make it more consistent and make zero arg fns a thing? again apologies if this has already been discussed

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

we talked about it; there were problems

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

higher-order functions in particular either needed parens or looked strange

view this post on Zulip Ayaz Hafiz (Jan 02 2025 at 18:10):

i see

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

Richard Feldman said:

I guess this implies that we don't need to overload what || means anymore

Would it be .thunk!(|()| Stdout.line!("foo!")) as well?

view this post on Zulip Sam Mohr (Jan 02 2025 at 18:32):

Yeah... don't really wanna write TIE fighters everywhere

view this post on Zulip Sam Mohr (Jan 02 2025 at 18:32):

Would be nice to desugar || to |()| for args as well

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

yeah we could certainly always say foo() is syntax sugar for foo(()) and || is syntax sugar for |()|

view this post on Zulip Sky Rose (Jan 03 2025 at 02:20):

What if we went the other way? Instead of || and foo() being sugar for 1-arity functions, the language really has 0-arity functions, and then foo : () -> a is the sugar for foo: -> a?

Using unit as an argument was always a weird workaround to not being able to call 0-arity functions with spaces, but now that we have parens for calling, we don't need that workaround anymore, we can have real 0-arity functions.

view this post on Zulip Sky Rose (Jan 03 2025 at 02:23):

If foo = || whatever is fine for defining 0-arity functions, then we only need sugar for the type. (foo : -> a is definitely silly.)

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

Sky Rose said:

What if we went the other way? Instead of || and foo() being sugar for 1-arity functions, the language really has 0-arity functions, and then foo : () -> a is the sugar for foo: -> a?

conceptually I do like the idea of having actual 0-arg functions, but then what's the inferred type if you put a function |()| into the repl?

view this post on Zulip Sky Rose (Jan 03 2025 at 02:30):

Probably a 1-arity function that really takes an argument of type (). Which could still exist, but is not the recommended way to write a function that doesn't care about its inputs. I don't think it's a problem if that's possible, just like it's possible to write f = \{}, {} -> today.

view this post on Zulip Sky Rose (Jan 03 2025 at 02:31):

The problem is if we use () -> a as sugar for -> a, then it'd be syntactically impossible to write the type of a function defined with |()|, which would be weird, and maybe a reason to use some different sugar for defining the type.

view this post on Zulip Sky Rose (Jan 03 2025 at 02:37):

Another weirder suggestion: If we really want () -> a as the sugar because it looks like a function call, and we don't want to make it impossible to put a real empty tuple there, we could change the tuple syntax so that tuples look different. #ideas > Change tuple syntax from () to {}? (That topic never reached a conclusion)

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

I personally don't have a problem with foo : => Str but the motivation for this thread was trying to find an alternative to that :big_smile:

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

I think foo : => Str is the best option and it's better to use that than to do all this unit value desugaring

view this post on Zulip Sky Rose (Jan 03 2025 at 02:59):

Same. It does look silly, but I think the consistency is more important than the aesthetics, so my top choice would be to have 0-arg functions with no sugar (|| and : ->) and then only add optional sugar later if we want.

view this post on Zulip Richard Feldman (Jan 03 2025 at 03:50):

I'm curious what others think about that, e.g. I remember @Luke Boswell pushing back against that syntax in the other thread

view this post on Zulip Luke Boswell (Jan 03 2025 at 03:51):

Yeah it looks really strange to me.

view this post on Zulip Luke Boswell (Jan 03 2025 at 03:51):

Not a hill I'd die on or anything...

view this post on Zulip Richard Feldman (Jan 03 2025 at 04:13):

it's not my favorite syntax, but if I do a stack ranking of all the designs we've talked about so far, I like it better than the alternatives

view this post on Zulip Richard Feldman (Jan 03 2025 at 04:13):

maybe it makes sense to go with it for now, but keep the door open to considering other options

view this post on Zulip Brendan Hansknecht (Jan 03 2025 at 04:45):

I see no reason to have real zero arg functions and a type like foo : => Str.

It is "more correct", but it has no value in my opinion. I would much rather have () => Str. It is more readable and does not look like a bug. It is also a sugar that is essentially optional to explain.

Not to mention => Str looks even worse in higher order functions:

exec_transaction! : (=> Result ok err) => Result ok (TransactionErr err)

Also, this is ignoring the other half of the spectrum. Zero result functions:

List.forEach! : List a, (a =>) =>

I'll take () or {} anyday over either of those.

exec_transaction! : (() => Result ok err) => Result ok (TransactionErr err)

List.forEach! : List a, (a => ()) => ()

view this post on Zulip Brendan Hansknecht (Jan 03 2025 at 04:47):

So unless we are totally changing type signatures (like with #ideas > Inline type annotations), I don't think these are worth considering.

view this post on Zulip Brendan Hansknecht (Jan 03 2025 at 04:48):

I'll still gladly take the sugar to avoid () = foo!(()) and instead just have foo!()

view this post on Zulip Brendan Hansknecht (Jan 03 2025 at 04:49):

I also think it is reasonable to change the default unit type to (). Seems that is more common.

view this post on Zulip Richard Feldman (Jan 03 2025 at 05:35):

Brendan Hansknecht said:

It is "more correct", but it has no value in my opinion. I would much rather have () => Str. It is more readable and does not look like a bug. It is also a sugar that is essentially optional to explain.

fair points, and also () is the mainstream syntax for zero-arg, so even if it technically means something slightly (but inconsequentially) different in Roc, it probably mostly just makes the language feel more familiar

view this post on Zulip Kilian Vounckx (Jan 03 2025 at 07:46):

I like the sugar, but at what level does is go?
I don't know how sugar usually gets implemented, but I guess it's not a simple find and replace before everything. That would give terrible error messages. So when does it happen. After type checking?
What if I have a generic function foo : a -> Bool. And what if I now somehow call it with a unit? foo(()). What happens?

view this post on Zulip Sky Rose (Jan 03 2025 at 14:17):

0-Result functions and (a =>) aren't an issue cuz functions always return one value, those would still return unit.
But I guess if we're using unit for return values then it's less bad to also have unit as a 1-arg placeholder in the args. And it's more bad to have my earlier proposed sugar : () => () for : => (), where one () is real and one is fake.

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

Sky Rose said:

0-Result functions and (a =>) aren't an issue cuz functions always return one value, those would still return unit.

This makes no sense to me. Returning a unit is exactly as fake as taking a unit as an arg. Both compile into doing nothing.

view this post on Zulip Brendan Hansknecht (Jan 03 2025 at 15:22):

Kilian Vounckx said:

I like the sugar, but at what level does is go?
I don't know how sugar usually gets implemented, but I guess it's not a simple find and replace before everything. That would give terrible error messages. So when does it happen. After type checking?
What if I have a generic function foo : a -> Bool. And what if I now somehow call it with a unit? foo(()). What happens?

I think either would ultimately be fine.

If you just make it a dumb replacement. foo : a -> Bool would just work if it was called as foo() and it would give the same result of being called as foo(()). Given taking a single a input is so rare and can only be meaningful with extra restrictions on the input type, I don't really think this would be a problem in practice

If you want to be safer, you can make it only work for functions that have a concrete input type of (). At which point, foo() would fail saying that it expected one argument a but instead got zero args.

view this post on Zulip Richard Feldman (Jan 06 2025 at 23:12):

I think we should try the sugar idea and see how it goes. So that means:


Last updated: Jun 16 2026 at 16:19 UTC