Stream: ideas

Topic: tuples for function arguments


view this post on Zulip Richard Feldman (Jul 06 2023 at 14:47):

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

view this post on Zulip Richard Feldman (Jul 06 2023 at 14:47):

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:

view this post on Zulip Richard Feldman (Jul 06 2023 at 14:49):

  1. lambda syntax looks more like CoffeeScript or JS, e.g.

today

List.map = \list, fn ->
    # implementation goes here

List.single = \elem ->
    # implementation goes here

idea

List.map = (list, fn) ->
    # implementation goes here

List.single = (elem) ->
    # implementation goes here

view this post on Zulip Richard Feldman (Jul 06 2023 at 14:50):

  1. calling functions becomes syntax sugar, e.g.
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)`

view this post on Zulip Richard Feldman (Jul 06 2023 at 14:51):

  1. multi-backpassing becomes tuple destructuring
a <- fn blah # same as today
(a, b) <- fn blah # now equivalent to today's `a, b <- fn blah`

view this post on Zulip Richard Feldman (Jul 06 2023 at 14:51):

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

view this post on Zulip Richard Feldman (Jul 06 2023 at 14:52):

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

view this post on Zulip Richard Feldman (Jul 06 2023 at 14:52):

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!)

view this post on Zulip Tommy Graves (Jul 06 2023 at 14:58):

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)

view this post on Zulip Richard Feldman (Jul 06 2023 at 14:58):

some other miscellaneous pros/cons besides being easier to parse and making multi-backpassing more discoverable:

# 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

view this post on Zulip Richard Feldman (Jul 06 2023 at 14:58):

Tommy Graves said:

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)

good question! I honestly hadn't thought about that

view this post on Zulip Richard Feldman (Jul 06 2023 at 14:59):

Tag can be used as a constructor function, e.g. List.map [1, 2, 3] Ok == [Ok 1, Ok 2, Ok 3]

view this post on Zulip Richard Feldman (Jul 06 2023 at 15:05):

Tommy Graves said:

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)

let's suppose the answer is yes...what are the pros/cons there? :thinking:

view this post on Zulip Brendan Hansknecht (Jul 06 2023 at 15:08):

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.

view this post on Zulip Brendan Hansknecht (Jul 06 2023 at 15:09):

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?

view this post on Zulip Brendan Hansknecht (Jul 06 2023 at 15:10):

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.

view this post on Zulip Brendan Hansknecht (Jul 06 2023 at 15:11):

I think we likely always need the parens there.

view this post on Zulip Richard Feldman (Jul 06 2023 at 15:27):

Brendan Hansknecht said:

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.

interesting, I hadn't thought of that :thinking:

view this post on Zulip Brendan Hansknecht (Jul 06 2023 at 15:29):

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.

view this post on Zulip Ajai Nelson (Jul 06 2023 at 15:39):

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.

view this post on Zulip Brendan Hansknecht (Jul 06 2023 at 15:41):

Another issue:

fn a # call fn passing a (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).

view this post on Zulip Ayaz Hafiz (Jul 06 2023 at 17:17):

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.

view this post on Zulip Brendan Hansknecht (Jul 06 2023 at 17:33):

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.

view this post on Zulip Joshua Warner (Jul 07 2023 at 01:43):

I like the argument about (a, b) -> a + b being much closer to the javascript / C# syntax for closures.

view this post on Zulip Joshua Warner (Jul 07 2023 at 01:46):

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

view this post on Zulip Brendan Hansknecht (Jul 07 2023 at 02:31):

Theoretically, we could also just require \ when backpassing. Though that may read too weird:

\a, b <- foo

view this post on Zulip Sven van Caem (Jul 08 2023 at 08:53):

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:

view this post on Zulip Sky Rose (Jul 12 2023 at 12:54):

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