[Pre-RFC] Keyword arguments

How would you overload with a struct?

argsA {
    pub a: i32
}

argsB {
    pub b: i32
}

fn foo(args : argsB) { ... }
fo foo(args : argsA) { ... }

Then you need to dispatch by argument type. You also need to declare structs for every permutation of types you might want to allow. Whereas using keyword arguments:

fn foo(b => arg : int32) { ... }
fn foo(a => arg : int32) { ... }

This means you can overload WITHOUT wrapping structs or having different types. I’ve already shown how this is useful by having both slice(to => 10) and slice(from => 5) that both take a usize but do different things. You can think of this proposal as syntactic sugar because nobody is going to write ACTUAL code with everything wrapped in a struct (it’s not as convenient as in JS).

The drawbacks of default arguments is that they have to have a default value! What if I don’t want a default value?

unwrap(self, or => default: T) -> T
unwrap(self) -> T

let foo : Option<i32> = None; if I call with just foo.unwrap() it should panic!, but if I call with foo.unwrap(or => 0) it should default to 0

This might be a bad example (let’s say you want to have unwrap be easy to find in the codebase so you can see what can panic!), but the point remains the same: sometimes you want to do something else when an argument is missing without any runtime checks.

1 Like

So, I didn’t clearly explain my view in this thread, but maybe it will help to answer your questions: (1) You don’t. If you have two functions to do different things, they should have different names. If they do the same thing, then one struct with optionals should be sufficient. (2) Ditto - in my opinion, the same logic you use to say that’s a bad example applies to pretty much that entire class of situations, where passing an argument would turn a function into something completely different.

Keeping one name for one purpose improves both greppability and readability.

However, I agree that you don’t always have a sensible ‘default value’. To deal with this, as part of the conversion from function arguments to struct, you could use the presence or absence of an argument to determine Some or None. In fact, it might be simpler to require this for optional arguments rather than having any actual concept of a default value - the function body could just use unwrap_or if it wanted a default.

I don’t want to derail the RFC, since this is just my personal opinion, but since this thread is on discuss rather than a GitHub issue… here’s a basic strawman for what I’d like to see:

struct DrawTextArgs<'a> {
   text: &'a str, // required
   color: Option<Color>, // optional
   font: Option<Font>, // optional
}
impl PaintContext {
    fn draw_text(&self, args: ..DrawTextArgs) {
        // use args.text, args.color, etc.
    }
}
// Convenience methods taking the same arguments are common, which is why tying the arguments to a separate struct declaration makes sense.
impl Window {
    fn draw_text(&self, args: ..DrawTextArgs) {
        self.paint_ctx.draw_text(..args)
    }
}
// usage
// Omitting keywords in calls to functions like the above, as well as using keywords on regular functions, should probably be possible, to avoid having two wildly different types of functions, although the latter would suddenly make argument names public API.  Swift has a(n ugly) syntax to differentiate 'external' and 'local' parameter names...
// So these would all be valid.
win.draw_text(text, color);
win.draw_text(text: text, font: font);
win.draw_text(color: color, text: text);

Unresolved:

  • What if you want a required argument of option type?
  • Using anything but : for keyword arguments would be hideous because it would be inconsistent with structs. Using : is apparently impossible due to type ascription. I suggest getting rid of type ascription.* ;p

*no, really. Having two operators (as, :) that both sort of mean “make A type T” (even if on a more detailed level they do totally different things) would be confusing as is, without the aforementioned conflict.

2 Likes

using Option for the arguments that are not there means you do a run-time check, while overloading is decided at compile time

Also, I suggest to get rid of : in structs in favor of = anyway

Here’s a relevant paper from when OCaml added named and optional arguments, which one should definitely read before trying the same for rust: http://caml.inria.fr/pub/papers/garrigue-labels-ppl01.pdf. They do a good job of documenting the tradeoffs they made and why they made them.

Some things that I haven’t seen covered yet:

  • My biggest issue: If named arguments are the default (i.e. all arguments are “named”), then changing the name of an argument to a public function is an API change and requires a major version bump. This is generally undesirable, and perhaps arguments should be unnamed by default.
  • Will this proposal make adding optional arguments later, easier?
  • Ocaml does this fun thing where if you have a local variable named x and a function takes a named argument named x, there’s shorthand syntax for calling that function such that the names line up. Surprisingly, this is a huge help in keeping names consistent across a codebase.

I’m really glad someone has stepped up to the plate to start addressing this. It’s hard to have confidence in the correctness of your code when argument order isn’t obviously-correct at the call site.

1 Like
  1. Named arguments are not the default, but just an additional option. My syntax distinguishes between the name of the argument (formal argument) and the internal variable name used in the function.
  2. Yes, because the named arguments can be optional. This means that you can add named arguments that are optional in positions other than the last argument. That means you can have two optional arguments (like my example of from=> and to=> that don’t require each other. All permutations are legal.

If you don't want a default value, then you use the overload or whatever else is there.

But if you do want a default value, then using the overload is a pain in the ass, especially when you need to duplicate (and maintain!) all the documentation just to have an extra argument. Not to mention that the extra function will bloat the docs too.

Examples: HashMap's load factor (defaults to 0.75 in Java). String's extra buffer space (defaults to 0, increased when I plan to append to the String later, e.g. String::from_str (..., reserve: 128)). A &str to "Modified UTF-8" function which I plan to write for FFI purposes. It should take an optional hint about the number of \u0000 codepoints expected in the string (defaults to 0) in order to use the memory optimally. Etc etc. I've used plenty of default arguments in Scala and none of them was optional, IIRC.

Also, I suggest to get rid of : in structs in favor of = anyway

That's a very big change, I hope with Rust becoming 1.0.0-alpha there will be less of these.

Yeah, but those are not related concerns. You can have overloading AND default values like:

fn draw_text<'a>(&self, &'a str, color => color: color = color::BLACK, font => font: font) { ... }
fn draw_text<'a>(&self, &'a str, text_renderer => text_renderer) { ... }

maybe even a specialized syntax like

fn draw_text<'a>(&self, &'a str, color => color: color = color::BLACK, (font => font: font)? /* font is optionally specified */) { ... }

So keywords, default arguments, overloading and optional arguments are not exactly the same concern and can exist as separate features added at different times

1 Like

I thought about this. Fn(A, foo => B) -> C might desugar to Fn<(Fn_args_struct, A, C), R> where

struct Fn_args_struct {
    foo: B
}
``` which is generated by the compiler for each function with keyword arguments

so basically keyword arguments are sugar for automatically making a struct for some of your arguments
so if you have two variants of the struct due to default arguments or optional arguments it will generate two different structs

not sure if that's the most efficient way to do it, but it makes sense on a conceptual level to me

You’ve got the desugaring wrong, it should be Fn<(A, Fn_args_struct), C>, there is no R. :smile:

If named parameters needs to be unordered, they all have to be packed into the same struct:

  Fn(A, foo => B, bar => C) -> D 
→ Fn<(A, struct { bar: C, foo: B }), Result=D> 

Yeah, I copy pasted the wrong part of your post, so my signatures don’t match

I meant that they would desugar like this:

Fn(A, foo => B, bar => C) -> D 
→ Fn<(struct { bar: C, foo: B }, A), Result=D>

with the keyword struct always first, because of futureproofing reasons

A bit left-field, but instead of named arguments, could we have warnings on non-matching argument names? (There would need to be numerous exceptions to these warnings…)

fn drawBox(height: u16, width: u16) ...

let height = calculateHeight() + 10;
let width = 45u16;
drawBox(width, height);

// Warning: local variables names do not match argument names on line X - "height" and "width" are switched.
1 Like

Some months ago, I wrote a RFC about adding default and keyword arguments to Rust, which seem to have been postponed: https://github.com/rust-lang/rfcs/pull/257

The idea behind this RFC came from an issue on the compiler repository (links in RFC PR) and from a MozPad I created in order to gather all design propositions.

I’ve read it a long time ago, that’s where I stole the default argument syntax from.

I think default arguments and keyword arguments are separate features, though. It does make it easier to have default arguments if they can occur anywhere as keyword arguments.

However, the difference between our proposals is that I view keyword arguments as API design (this API must have keyword arguments so it’s easier to read vs. another API that must be concise) vs. your proposal that adds keyword arguments for current Rust functions. It just seems like when this is added to a language most people don’t use it.

When people pass a { width: 10, height: 20 } to a constructor in JavaScript, that’s not a choice, that IS the API.

If you want a different type of function, why not change the function in general:

fn make_rect{width : i32, height : i32} -> Rect { ... }
fn make_rect for {width : i32, height : i32} -> Rect { ... }

let x = make_rect(height : 42, width : 99); // no more ambiguities since name is required if ambig-choice is made before parsing the args
let x = make_rect{height : 3, width : 93}; // no ambig if functions and structs don't share their namespace

any function defined with {} will have to be called with named arguments instead of positional. normal functions continue to exist.

My design allows you to mix the two:

pub fn filter<T>(option: Option<T>, include => test: bool) -> Option<T> { ... }
pub fn filter<T>(option: Option<T>, include_test => test: &Fn() -> bool) -> Option<T> { ... }

Not sure if this kind of pattern will be used a lot, but it looks like it could be useful

I made a pull request here, but I epically failed with git, somehow optional arguments branch snuck into the pull request.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.