Overloading with tuples

I was asking about this feature on URLO (in a fashion: why this obvious idea is not implemented), but got no useful responses (except for reference to mysterious decisions-made-in-the-past-which-noone-name) thus I want to present it here as an idea.

I tried to look on IRLO and found this thread which ended with the following message: You must have something you want to use it for or you wouldn’t have brought it up, so would you mind posting some concrete examples where you think it would make the overall code clearer?

My idea is significantly limited form of what was proposed there: make it possible to call function which accepts a single tuple argument (or two arguments one of which is self, when it's called as foo.bar(…) and not Foo::foo(bar, …) with appropriate number of separate arguments if their number is not 1 (that is: if you call it like foo.bar() then it becomes foo.bar(()) and foo.bar(1, 2) then it can be interpreted as foo.bar((1, 2)), but foo.bar(42) would not become a foo.bar((42,)).

This may sound like a useless syntax sugar, but can be used to make foreign JavaScript and C++ APIs much easier to use. Because currently used approach provides monstrosities like init_mouse_event_with_can_bubble_arg_and_cancelable_arg_and_view_arg_and_detail_arg_and_screen_x_arg_and_screen_y_arg_and_client_x_arg_and_client_y_arg_and_ctrl_key_arg_and_alt_key_arg_and_shift_key_arg_and_meta_key_arg_and_button_arg_and_related_target_arg.

How would this work? Just like official blog said few years ago: traits provide much of the benefit of overloading: if a method is defined generically over a trait, it can be called with any type implementing that trait.

You can do this in a stable Rust today, using traits:

    let foo = Foo::new(());
    foo.bar(());

    let foo = Foo::new(24);
    foo.bar(1);

    let foo = Foo::new((1, 2));
    foo.bar((0.2, 2));

That is: you can provide different overloads for a function or a method but when you need to pass number of arguments ≠ 1 then you have to wrap your arguments in a tuple.

Note that there are already one place in a language where compiler does that trick: Fn/FnMut/FnOnce traits. In fact on unstable Rust you can implement the syntax people would expect.

Now, you may say that function overloading is evil and one shouldn't use it, but the fact of the matter: it does exist. Many existing libraries and many existing platforms use it. If we can make use of these APIs closer to official documentation with examples in other languages then it would make overral code cleaner.

And if we only implement conversion from tuples to function arguments for a case where function accepts one argument and tuple have number elements other than one, then there should be no ambiguity because existing compiler would reject such code.

WDYT?

2 Likes

So what I usually say is that Rust has principled overloading, as opposed to the ad-hoc overloading that's common elsewhere. And that's important both for how Rust does generics as well as for how it does type inference.

So that could have already been init_mouse_event((more, ..., here)) with a trait -- as you mention -- but notably it wasn't. Have you investigated why they made that choice? What is it about the super-long-name approach that they liked better? The "two more parens" approach is obviously less caller-typing-impact than the long name, so they must have been optimizing for something else.

I'll note that the example on the MDN page is not something I'd consider good in any language:

evt.initMouseEvent("click", true, true, window, 0, 0, 0, 80, 20, false, false, false, false, 0, null);

(I've never seen a function with six boolean parameters that I like.) And they seem to agree, since the method is deprecated in favor of the much simpler MouseEvent constructor.

That one takes only two parameters, the required type one and an options dictionary. Which could translate into rust excellently, with the call potentially looking something like this:

MouseEvent::new(
    event_name,
    s! {
        clientX = 100,
        altKey = true,
    })

(Using the macro from Short enum variant syntax in some cases - #9 by scottmcm until Esteban's struct defaults RFC happens.)

And it looks like, in the mean time, they've made a builder for it MouseEventInit in web_sys - Rust so it can be ergonomically constructed.

5 Likes

They were optimizing for smaller size of automatic convertor. These functions are named like that in C (since C doesn't have true overloads). And then are exposed without changes in Rust bindings.

Also there are additional issue: since overloads can be added at any time, you would have to export all functions, even the ones which are not currently overloaded (and most functions are not overloaded), via tuples… which looks weird (you would have to type “two more parens” everywhere, not just where functions are overloaded). Long names sometimes sound like a better deal than double parens everywhere.

It's a bit chicken-and-the-egg problem: since we don't have a way to expose nice API for overloaded methods in Rust and most such APIs must go through C anyway (because C++ ABI is unstable on many platforms) the raw export of C interface often looks simpler than other approaches.

But there are efforts underway to make it possible to access C++ libraries from Rust directly (e.g. LLVM supports cross-language inlining) in which case there would be no C bridge and no reason to expose these super-long names.

And yes, idiomatic bindings are, often, better than raw bindings, that goes without saying. But asking to make them for all libraries, including really obscure ones, is not very realistic.

1 Like

I think that allowing both foo((1, 2)) as foo(1, 2) and foo(foo{bar: 1, baz: 2}) as foo{bar: 1, baz: 2} would be a plus.

EDIT: I used the trick that we can have both a function and a struct with the same (snake_case) name for struct: playground example.

foo{bar: 1, baz: 2} already have a meaning, I think you meant foo(bar: 1, baz: 2) to mean foo(foo{bar: 1, baz: 2}).

Thus warrant a separate discussion because it requires changes to Rust grammar and doesn't have a precedent.

Conversion from multiple arguments to tuple, on the other hand, doesn't change the language syntax, it fact it's already implemented (nightly-only interface for Fn/FnMut/FnOnce), so it's mostly matter of feasibility.

Yes, you are right.