wild idea time!
so I was thinking about how multi-backpassing complicates the parser a lot and is also rarely used and not very discoverable but is also useful in some cases, and it reminded me of an old idea from years ago that I figured I should put out there and get peoples' thoughts about it
the basic idea is something like this: what if "all functions take 1 argument, and multi-argument functions are actually 1-argument functions that take tuples as their 1 argument?"
so, concrete implications of this idea would be:
List.map = \list, fn ->
# implementation goes here
List.single = \elem ->
# implementation goes here
List.map = (list, fn) ->
# implementation goes here
List.single = (elem) ->
# implementation goes here
fn a # call `fn` passing `a` (so, same as today)
fn a b # now syntax sugar for calling `fn` passing `(a, b)`
fn a b c # now syntax sugar for calling `fn` passing `(a, b, c)`
a <- fn blah # same as today
(a, b) <- fn blah # now equivalent to today's `a, b <- fn blah`
so in this idea, most Roc code today would look exactly the same except lambdas would go from \a -> blah to (a) -> blah, and \a, b -> blah would become (a, b) -> blah
multi-backpassing would look different though, in that it would go from a, b <- fn blah to (a, b) <- fn blah - this would resolve the parser and discoverability issues (I don't think anyone who looks at that would be surprised/confused about what it did in this world) while maintaining the functionality
error messages could be the same but would need some special-casing, obviously there would have to be type system changes (I'm not saying this is necessarily a good idea or worth it, just talking through the idea!)
Would the same be true for tags? Tag a b becomes sugar for Tag (a, b)? (I don't remember if Tag is actually just a constructor function or not)
some other miscellaneous pros/cons besides being easier to parse and making multi-backpassing more discoverable:
(a, b -> c) -> ((a, b) -> c) because functions would all be "pre-tupled"\ for lambdas...not sure if parsing would be more or less complicated if we aren't parsing \ for lambdas anymore# one idea: keep types the same as today
map2 : List a, List b, (a, b -> c) -> List c
map2 = (list1, list2, fn) ->
# implementation goes here
# another idea: for symmetry with values, always put parens around args.
#
# could also allow this syntax but say today's syntax desugars to this,
# like how today's calling syntax would desugar to calling with parens.
map2 : (List a, List b, ((a, b) -> c)) -> List c
map2 = (list1, list2, fn) ->
# implementation goes here
Tommy Graves said:
Would the same be true for tags?
Tag a bbecomes sugar forTag (a, b)? (I don't remember ifTagis actually just a constructor function or not)
good question! I honestly hadn't thought about that
Tag can be used as a constructor function, e.g. List.map [1, 2, 3] Ok == [Ok 1, Ok 2, Ok 3]
Tommy Graves said:
Would the same be true for tags?
Tag a bbecomes sugar forTag (a, b)? (I don't remember ifTagis actually just a constructor function or not)
let's suppose the answer is yes...what are the pros/cons there? :thinking:
Having 2 as syntax sugar feels really strange to me. I think it would be weird to sometimes see f a b and sometimes f (a, b).
I would hope that essentially only f a b is ever used in practice, which I why I dislike seeing the option to use both.
Also, this definitely opens some question around calling convention. Is the thought that none of the tuples would manifest and the underlying calling convention would stay the same as today?
a <- fn blah
This is inconsistent. Not clear if that is equivalent to (a) <- fn blah or binding an entire tuple and being able to write a.2.
I think we likely always need the parens there.
Brendan Hansknecht said:
a <- fn blah
This is inconsistent. Not clear if that is equivalent to
(a) <- fn blahor binding an entire tuple and being able to writea.2.
interesting, I hadn't thought of that :thinking:
Also, a parse edge case to be careful of: f (a, b) c. First arg of the function is a tuple. Then there is a second arg after.
Slightly off topic: In math it’s normal to use tuples for functions of more than one argument. It kind of blew my mind when I realized that if you want, in ML family languages, you can use tuples for multiple arguments and then use f(x, y) as the normal way to call a function. Apparently the SML standard library uses tuples for multiple arguments except for functions like map where currying is particularly useful. I also once found an college course online using OCaml that taught things this way and didn’t teach currying until like halfway through the course.
Another issue:
fn a # call
fnpassinga(so, same as today)
x = (a, b)
f x
So this should pass a tuple that contains x to f. Essentially, it is an extra level of tuple that may not be wanted.
I think the current syntax may not desugar cleanly in cases like this. So you are forced to destructure and rebuild. Also means it may not be pipeline friendly if you pipeline in the tuple of args ( I guess this would mean that pipelining becomes for the first field into the tuple instead of the tuple as a whole).
It's worth nothing that there is also a non-trivial difference in the operational semantics of passing multiple arguments (f x y) vs one argument (f (x, y)). In the former case, the compiler is free to treat the arguments as scalars, and e.g. pass x by reference if it is larger and y by value, or pass x on the stack and y in a register, and so on. In the latter case, this is not possible without first running an optimization pass (SRA) to replace the aggregate tuples with scalars - which can be non-trivial, so it can incur a compile-time cost and not be guaranteed.
Yeah, that is why I asked about calling convention. I assume that we would really just have a tuple syntax sugar but we would always desugar. So in reality, this would all be syntax and we would not change how functions are generated today.
I like the argument about (a, b) -> a + b being much closer to the javascript / C# syntax for closures.
Another way to resolve the multi-backpassing issue might be to require this there but nowhere else.
Something like:
(a, b) <- foo
And, on the off chance you actually actually need to capture a tuple as a single argument, you'd do double parens:
((a, b)) <- foo
Theoretically, we could also just require \ when backpassing. Though that may read too weird:
\a, b <- foo
I'm a fan of this idea. I remember being confused as to why the \ disappeared when defining a function using backpassing when I was learning about it. Including it could make it clearer that it's just another way of defining an anonymous function. I do have a bit of a soft spot for the backslash lambda syntax though :sweat_smile:
I agree using function syntax for back passing (whether that's \ or ()) helps show that you're defining a function
Last updated: Jun 16 2026 at 16:19 UTC