Idea: reducing boilerplate in APIs that use ADTs

Rust doesn’t have named/keyword arguments yet, and there has already been several RFCs and discussions about it over hundreds of posts long. I admit I did not read most of that, so if I’m just repeating what had already been said, feel free to point me to the relevant discussions.

Motivation

The core idea is that I think Rust’s structs and enums are already quite good for expressing APIs, if only they could be a little more ergonomic (less boilerplate).

Consider, for example, an API like this:

pub enum FooColor { Red, Green, Blue }

pub struct FooArgs<'a> {
    pub alpha: &'a str,
    pub color: FooColor,
    #[doc(hidden)]
    pub __reserved: (),
}

impl<'a> Default for FooArgs<'a> { … }

fn foo(args: FooArgs) { … }

On the user side, they would have to use it like this:

use some_mod::{FooArgs, FooColor, foo};
foo(FooArgs {
    alpha: "blah",
    color: FooColor::Red,
    .. Default::default()
})

There is quite a bit of redundancy in this code that would be nice to eliminate:

  • FooArgs and FooColor both need to be brought in scope, but this feels rather wasteful if both are specific to the foo() API.
  • FooArgs needs to be mentioned explicitly, even though it’s clear from the context that the only acceptable struct is FooArgs.
  • FooColor:: needs to be prepended to the enum value, at least if you don’t want to pollute the namespace with highly generic names (Red, Green, and Blue).
  • Default::default() is a pretty long expression to type out every time you want to call foo!

It would be nice if the user’s code could be simplified to just:

use some_mod::foo;
foo(_ {
    alpha: "blah",
    color: _::Red,
    ..
})

Idea

Specifically what we have done here are:

  • Struct name omission (see also: phaylon’s idea): Automatically infer the struct type in _ { … } if there is sufficient information from the surrounding context. (The reason for not allowing just { … } is that it would be hard to parse.)

    Beyond the use case above, it would also encourage the use of structs over tuples in general, leading to more readable code.

    This idea could also be extended to patterns.

  • Enum name omission: Automatically infer the enum type in _::Whatever(…) if there is sufficient information from the surrounding context. (The reason for _:: is to avoid ambiguities if Whatever is a function in scope.)

    Beyond the use case above, it would also encourage the use of domain-specific enums over generic enums (Option, Result), leading to more readable code.

    This idea could also be extended to patterns.

  • Default struct update (see also: pnkfelix’s idea): Allow

    SomeStruct { … .. Default::default() }
    

    to be shortened to

    SomeStruct { … .. }
    

This idea helps mostly the users of the library. One could devise ways to help the library writers too, but that is beyond the scope of this post.

Downsides

Syntactically, this adds three new subtle changes. Of the three, I think _:: might be most controversial, since it is possible that this syntax might be used for some other feature in the future. Alternatively, one could add an enum prefix or something else.

The new syntax is also arguably more punctuation heavy, partly because of the use of _ and also because we killed off a lot of the unnecessary words.

The biggest changes here are not so much the syntax however: the type inference algorithm would become more complicated and less elegant as a result of this change. There would be more situations where the type of this value must be known in this context. However, the change is backward compatible, and should not alter the inference of existing code.

5 Likes

Can Rust just add some form of named arguments instead (just pick ANY of them)?

Tks.

I think the features mentioned in this proposal would be useful independent of named arguments. I often find myself writing seemingly redundant struct and enum names. It’s part of the reason that newtypes involve so much boilerplate currently.

3 Likes

Have you seen this library? It seems like it might be a good (atleast partial) answer:

I have heard of it, but it would be preferable to have something built into the language instead, since the idea is focused on further lowering the barriers in the use of algebraic data types.

I like. _ in type context already means “please infer this”, so using it for struct literals makes sense. FRU is a documentation example for Default, and I can’t imagine any other no-parameter thing that would make sense. Plus the symmetry with pattern syntax is nice. (I also think that it’s cool that pseudo-code already tends to look like { a: 4, .. }, and now it’d compile :laughing:)

Here are some more places where the same or similar things have come up:

Maybe it just needs someone to write an RFC? (Or maybe three, since they’re separable.)

2 Likes

_::Variant for enums is great. The analogous feature in Swift is very useful.

1 Like

I think this needs very careful consideration of its effects outside the specific context (complex function arguments) it is meant to be used in. Specifically, I’m worried about surprising and/or counterproductive effects if someone writes _ { ... } in a different “surrounding context” than a function call.

I’m also a bit “bleh” on _:: on aesthetic grounds. Can we somehow make it at most a one-character sigil?

Could you give an example? I would assume type name inference would only work in places normal inference can already happen.

Well, it is the single character sigil _. The ::Variant part is normal enum variant syntax. The fact that _ would continue to be a type stand-in seems very valuable to me. Plus I find that in general less special casing pays off when generating code from macros, for example.

Specifically, I'm worried about surprising and/or counterproductive effects if someone writes _ { ... } in a different "surrounding context" than a function call.

If that was literal, _ { .. } just becomes a struct-specific way of saying Default::default() that I actually find less surprising.

If that was a pseudocode ellipsis, I'm not afraid of this context dependence for struct literals since it mirrors what happens with numeric literals:

let x = 1i32;    let p = Point { x: 1, y: 2 };
let x: i32 = 1;  let p: Point = _ { x: 1, y: 2 };

Simple demo of combining _ and ..:

fn get_basis_vectors() -> [Vector3d; 3] {
    [
        _ { x: 1, .. },
        _ { y: 1, .. },
        _ { z: 1, .. },
    ]
}

I think the inference and defaulting makes it easier to focus on the core of what's happening.

2 Likes

Yeah, _:: is not very pretty, but OTOH it’s a totally logical combination of other Rust syntax.

Can there be other syntax that is not a one-off special case for this?

Arguably Rust painted itself into a syntactic corner here. Swift uses Enum.Variant which can be shortened to just .Variant, but shortening Enum::Variant to ::Variant doesn’t work because that already means something. (And would already be a bit annoying to type, even without the _.)

1 Like

Here’s a new RFC for the enum elision idea: https://github.com/rust-lang/rfcs/pull/1949

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