Pre-RFC: Named arguments

What about if it worked as follows:

fn parse( pub from: &str, pub into: MyStructure );

defined the function named, "parse$from$into" (or "parse__from_into" if you prefer). Then, at the call site:

parse(from: s, into: my_structure);

Is resolved unambiguously as "parse$from$into". Also, if you want a function reference or you want to "use" the function name, it is simply "parse$from$into". Absolutely no new syntax required and 100% unambiguous.

Does that resolve the objection?

EDIT:

This also resolves the issue for C-FFI. In that case, if you have more than 1 function with the same root name but differing parameter names, you would simply have 2 different symbols exported and callable from C without any ambiguity. So, no special rules are required around C-FFI. Also, this would make unique symbols in the debug format as well as the compiled code for purposes of linking. In effect, this is just another way of declaring the name of the function. You now have the root of the function name, appended with the "pub" parameter names.

Does this start to sound more palatable?

Why does this need argument-based overloading at all? Why not use different function names? That would give unique unambiguous names and symbols, with no special syntax.

That's still new syntax, it's just different syntax.

6 Likes

I'm not understanding why you think that is new syntax. Can't you already use "$" in symbol names (am I mis-remembering)? Oops. You can't currently use $ in symbol names. Ok, then the function would be named, "parse__from_into". That has no new syntax. You can even call the function using that full name without using the parameter names. So, you can call this function like this:

parse(from: ..., into: ...);

or this:

parse__from_into( ..., ... );

You can import/use the function like this:

use somemod::parse__from_into;

You can get a function pointer like this:

let fnptr = parse__from_into;

None of this requires new syntax (except at the definition site and at the calling site when specifying the parameter names). However, the function name can be de-sugared using the root name and the parameter names 100% unambiguously and require no further special handling by the compiler.

No?

EDIT: The "parse( pub from: &str, pub into: &mut MyStruct )" is a very poor example, but it is simple enough to illustrate the point.

Couldn't this also be valid? You could just consider the number of parameters as part of the function name.

parse_from_into(some_str,some_structure)?;
parse_from_into(some_str,some_structure,some_filter)?;

My question is: why is this bound to named parameters? Why is this essential to them?

2 Likes

I'd consider this a straw-man. It doesn't really have anything to do with the proposal.

Overloading also doesn't have anything to do with named arguments.

In general, unless there's some fundamental reason that two features need to go in simultaneously, I'd suggest separating them into separate proposals so they can be considered separately. The primary reason I'd suggest putting otherwise unrelated features into the same proposal is if you want to make an argument of the form "adding A and B together improves the language, but adding A without B makes the language worse; we shouldn't add A without B".

Do you think named arguments without overloading would be worse than no named arguments at all? If not, is there some reason to not let named arguments be considered on its own, separate from overloading

7 Likes

I think this is an excellent demonstration of why it's essential to have a scope discussion first, and then say why the chosen mechanism is better than other alternatives that could also meet those requirements :+1:

I don't think so, so I'll elaborate it out a bit.

Rust doesn't have arity-based overloading today. But at any point in the past 7+ years Rust could have done an analogous "no new syntax" change to say that fn foo(_: i32) would actually be named foo__1, and thus if you also have fn foo(_: i32, _: u32) that'd be fine, because it would actually be foo__2, and not conflict. Then you could have use whatever::foo; import both, but have use whatever::foo__1 if you only wanted one of them.

That's actually much easier than doing it for named arguments, since it doesn't need the annotations inside the arguments. But yet while arity overloading has come up occasionally over the years, it's never really gotten close, in my impression, to the kind of groundswell needed to make it actually happen as a language change, despite being a feature that's incredibly common in other languages. And is something that would be handy for compatible upgrades to existing things -- all the _in methods that also pass allocators, for example.

So what is it about overloading with named arguments that makes it substantially more worth doing than overloading on arity? Or are they both about the same value, and something has been missed about the arity version that says it's actually important to do?

(And I think the emphasis on "no new syntax" is a bit overblown, since it opens up all the questions about ambiguity between fn parse__from_into(); -- which is legal today, albeit discouraged -- and a future fn parse(from: ..., into: ...);. If this is worth doing, I wouldn't mind at all a bit of extra syntax for it, especially if it can fit in already-reserved space like 3101-reserved_prefixes - The Rust RFC Book .)

2 Likes

Yes, I think not having overloading (which I don't really think it is overloading, but nevertheless) with named arguments is not worth the effort. I really think these things go hand in hand.

Without the "overloading" part of it you can't make "sentence-like API's" that start with the same verb. With it, you can. Being able to have a call site read like a sentence is incredibly useful for clarity of the code and makes understanding someone else's code much easier.

As I said above, it makes it possible to have API's that read like sentences at the call site and also have sentences that start with the same verb but contain differing additional clauses.

I'll enter my position here:

I'm generally positive on the concept of named parameters. However, I want to see a full proposal for sugaring (optionally anonymous?) "options" structs as an alternative, to compare the solution space. Especially with previously discussed potential struct field default sugaring, I find it plausible that options struct sugar can translate the benefit of named arguments while fitting into existing Rust design better.

Plus, options structs have a notable benefit of composing trivially, whereas function argument labels don't compose transparently. This can be beneficial either way β€” perhaps you want to expose all of the configuration of a wrapped function, or perhaps you want to fix some arguments and expose others β€” but composing can always decay back to listing out names like with argument labels.

Many features which shine in support of options structs suggest to also compose well with the rest of the language (e.g. struct name inference and struct field-level defaults) so that to me says that the direction needs to be explored more before a direction is properly picked.

20 Likes

Overloading at least needs to be considered in the proposal. In particular without overloading this:

fn foo(pub bar: Bar, pub baz: Baz) {}
...
let bar = ..;
let baz = ..;
foo(bar, baz); // no explicit labels, the names match

seems almost obviously reasonable. If we are going to support overloading, or future proof for overloading, then this should require foo(bar: bar, baz: baz).

(Personally: I'd be in support of a position of "we're just not going to support any pre-monomorphization overloading" and design around that.)

2 Likes

pre- or post-?

I don't know what to call these...

In some cases you can use the same "name" to refer to different implementations of a function. The fully qualified path is still unambiguous, but:

struct F;

trait Foo<T> {
    fn foo(&self, t: T);
}

impl Foo<(i32,)> for F {
    fn foo(&self, _: (i32,)) {
        println!("one!");
    }
}

impl Foo<(i32, i32)> for F {
    fn foo(&self, _: (i32, i32)) {
        println!("two!");
    }
}

pub fn main() {
    let f = F;
    f.foo((1,));
    f.foo((2, 2));
}

This is pre-monomorphization (but I don't think this is "overload" in the strict sense). If we look at the MIR:

fn main() -> () {
    // ...

    bb0: {
        _3 = &_1;                        // scope 1 at src/main.rs:21:5: 21:16
        (_4.0: i32) = const 1_i32;       // scope 1 at src/main.rs:21:11: 21:15
        _2 = <F as Foo<(i32,)>>::foo(move _3, move _4) -> bb1;
    }

    bb1: {
        _6 = &_1;                        // scope 1 at src/main.rs:22:5: 22:18
        (_7.0: i32) = const 2_i32;       // scope 1 at src/main.rs:22:11: 22:17
        (_7.1: i32) = const 2_i32;       // scope 1 at src/main.rs:22:11: 22:17
        _5 = <F as Foo<(i32, i32)>>::foo(move _6, move _7) -> bb2;
    }

    bb2: {
        return;                          // scope 0 at src/main.rs:23:2: 23:2
    }
}

I can't think of any mainstream programming language that overloads simply on arity. Type and arity yes, but just arity? I think allowing overloading based on arity alone would be terrible and not serve any useful purpose. It definitely wouldn't make things clearer/easier to read at the call site (unlike this proposal for named parameters).

Again, I get back to, this proposal is not overloading a function name. It is always making unique function names. The function name is the root name appended with the named parameter names. It is an alternative way of defining and calling a function, but, I really believe sincerely it should not be thought of as "overloading".

That's an incredibly heavy-weight solution to the problem. It requires a new-type to be declared for each parameter that one would instead just give a meaningful name to.

1 Like

Could you clarify a bit what you mean by this an what problem it presents?

By that definition, "overloading" does not exist. Overloading on the types of the arguments also isn't "real overloading", it is just implicitly putting the types of the arguments into the function name. Please don't try to define away the problem by changing terminology.

Most people, at least the ones that are raising concerns here, seem to use "overloading" to refer to the situation where in a function call fun(arg), I have to use some information from arg (be it their number, their types, or the names they are given) to determine which code is being called. So for the purpose of this discussion, can we agree that this is what "overloading" means? We can call it "argument-based name resolution" or make up a new word altogether, but that won't change anything. It is this influence of the (arg) part on name resolution that worries people, so please don't try to belittle that concern by saying it should be called something else.

Instead, let's focus on whether overloading is truly needed to make named arguments an improvement to Rust. Now please excuse me, I will have to re-read parts of the RFC to learn the arguments the author has already made for this discussion.

19 Likes

My definition of heavy weight is considerably different:

Using types in a statically-typed language is the default. Adding a whole new alternative design is the heavy handed situation.

I must question though, if you think using types is so heavy handed, why are you using Rust in the first place? Surely, a dynamic language like Python would fit better with your particular set of design considerations.

Put another way, asking a pizza restaurant to serve dumplings is a bit more tenuous than having said place serve more kinds of pizza toppings. Of course, there's nothing wrong with wanting dumplings, you can always just go to a different restaurant that specialises in that kind of cuisine.

1 Like

First, I just want to say I have immense respect for you and don't consider myself anywhere in your class. But, I'm going to try to argue a few nits with you because I do feel pretty strongly about them.

I can agree with that definition, but I can't agree that overloading based on type, arity, or name are of the same kind and so should be considered the same. It is always possible to find a more restrictive or less restrictive definition for a concept. I disagree that the definition you give, being less restrictive, better characterizes the issues. I don't mean to "belittle" by trying to define it away, but I really consider that "overloading" based on arity vs. types vs. parameter names are entirely different animals and objections that apply to one don't necessarily apply to the others.

At the call site, if I have overloading by arity alone, I need to carefully count the arguments, then go check the docs to see which function applies, then see what the parameter types and names are before I can understand the code that is written in any meaningful sense. If overloading by type, I need to carefully determine what the types are, go look in the documentation for the function that has those specific types, and then read the documentation to understand the call site. With parameter name-based overloading, given a meaningful verby name for the function and meaningful parameter names by the designer of the API, I can simply read what is written and have a fairly good idea of what is going on without reading any documentation or looking up any docs.

In the case of the named parameters, there is additional information given at the call site whereas the arity-based and type-based provide no additional information at the call site to help facilitate deciphering what the code is doing.

7 Likes