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
there's already precedent for this in inferring "statements" to have a value of {}
so then we don't have zero-arg function syntax (or a semantic concept), but rather the type is always {} => ...
but you can still call it with foo() instead of having to write foo({})
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()
If so, I'm on board!
yeah, I like it
Yes for plain applys, and not in methods
I guess this implies that we don't need to overload what || means anymore
can just do thunk = |{}| ... or thunk = |()| ... depending on #ideas > () for unit type?
|()| is definitely a TIE fighter
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
we can’t we have zero arg functions right now? because there’s no way to annotate them?
Yep
But I don't think there's a problem otherwise
some nice symmetry with the type if we don't use ||
do_nothing : () -> Logger
do_nothing = |()|
Logger.{ write_raw!: |_, _| () }
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
we talked about it; there were problems
higher-order functions in particular either needed parens or looked strange
i see
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?
Yeah... don't really wanna write TIE fighters everywhere
Would be nice to desugar || to |()| for args as well
yeah we could certainly always say foo() is syntax sugar for foo(()) and || is syntax sugar for |()|
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.
If foo = || whatever is fine for defining 0-arity functions, then we only need sugar for the type. (foo : -> a is definitely silly.)
Sky Rose said:
What if we went the other way? Instead of
||andfoo()being sugar for 1-arity functions, the language really has 0-arity functions, and thenfoo : () -> ais the sugar forfoo: -> 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?
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.
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.
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)
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:
I think foo : => Str is the best option and it's better to use that than to do all this unit value desugaring
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.
I'm curious what others think about that, e.g. I remember @Luke Boswell pushing back against that syntax in the other thread
Yeah it looks really strange to me.
Not a hill I'd die on or anything...
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
maybe it makes sense to go with it for now, but keep the door open to considering other options
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 => ()) => ()
So unless we are totally changing type signatures (like with #ideas > Inline type annotations), I don't think these are worth considering.
I'll still gladly take the sugar to avoid () = foo!(()) and instead just have foo!()
I also think it is reasonable to change the default unit type to (). Seems that is more common.
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
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?
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.
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.
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 functionfoo : 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.
I think we should try the sugar idea and see how it goes. So that means:
{} to ()foo() desugar to foo(())Last updated: Jun 16 2026 at 16:19 UTC