How to best *fake* keyword style function argument

2019-06-22 05:21发布

问题:

I'm interested to have something functionally similar to keyword arguments in Rust, where they're currently not supported.

For languages that provide keyword argument, something like this is common:

panel.button(label="Some Button")
panel.button(label="Test", align=Center, icon=CIRCLE)

I've seen this handled using the builder-pattern, eg:

ui::Button::new().label("Some Button").build(panel)
ui::Button::new().label("Test").align(Center).icon(CIRCLE).build(panel)

Which is fine but at times a little awkward compared with keyword arguments in Python.


However using struct initialization with impl Default and Option<..> members in Rust could be used to get something very close to something which is in practice similar to writing keyword arguments, eg:

ui::button(ButtonArgs { label: "Some Button".to_string(), .. Default::default() } );

ui::button(ButtonArgs {
    label: "Test".to_string(),
    align: Some(Center),
    icon: Some(Circle),
    .. Default::default()
});

This works, but has some down-sides in the context of attempting to use as keyword args:

  • Having to prefix the arguments with the name of the struct
    (also needing to explicitly include it in the namespace adds some overhead).
  • Putting Some(..) around every optional argument is annoying/verbose.
  • .. Default::default() at the end of every use is a little tedious.

Are there ways to reduce some of these issues, (using macros for example) to make this work more easily as a replacement for keyword access?

回答1:

Disclaimer: I advise against using this solution, because the errors reported are horrid. The cleanest solution, codewise, is most probably the builder pattern.


With that out of the way... I whipped together a proof-of-concept demonstrating operator abuse.

Its main advantage over using struct syntax to pass arguments, or using a builder, is that it allows reuse across functions taking different sets of the same parameters.

On the other hand, it does suffer from having to import a whole lot of symbols (each name to be used).

It looks like:

//  Rust doesn't allow overloading `=`, so I picked `<<`.
fn main() {
    let p = Panel;
    p.button(LABEL << "Hello", ALIGNMENT << Alignment::Center);

    p.button(LABEL << "Hello", Alignment::Left);
    p.button(Label::new("Hello"), Alignment::Left);
}

Note that the name is really optional, it merely servers as a builder for the argument itself, but if you already have the argument it can be eschewed. This also means that it's probably not worth creating a name for "obvious" parameters (Alignment here).

The normal definition of button:

#[derive(Debug)]
struct Label(&'static str);

#[derive(Debug)]
enum Alignment { Left, Center, Right }

struct Panel;

impl Panel {
    fn button(&self, label: Label, align: Alignment) {
        println!("{:?} {:?}", label, align)
    }
}

Requires some augmentation:

impl Carrier for Label {
    type Item = &'static str;
    fn new(item: &'static str) -> Self { Label(item) }
}

impl Carrier for Alignment {
    type Item = Alignment;
    fn new(item: Alignment) -> Self { item }
}

const LABEL: &'static Argument<Label> = &Argument { _marker: PhantomData };
const ALIGNMENT: &'static Argument<Alignment> = &Argument { _marker: PhantomData };

And yes, this does mean that you can augment a function/method defined in a 3rd party library.

This is supported by:

trait Carrier {
    type Item;
    fn new(item: Self::Item) -> Self;
}

struct Argument<C: Carrier> {
    _marker: PhantomData<*const C>,
}

impl<C: Carrier> Argument<C> {
    fn create<I>(&self, item: I) -> C
        where I: Into<<C as Carrier>::Item>
    {
        <C as Carrier>::new(item.into())
    }
}

impl<R, C> std::ops::Shl<R> for &'static Argument<C>
    where R: Into<<C as Carrier>::Item>,
          C: Carrier
{
    type Output = C;
    fn shl(self, rhs: R) -> C {
        self.create(rhs)
    }
}

Note that this does NOT address:

  • out of order argument passing
  • optional arguments

If a user is patient enough to enumerate all combinations of optional parameters, a solution like @ljedrz is possible:

struct ButtonArgs {
    label: Label,
    align: Alignment,
    icon: Icon,
}

impl From<Label> for ButtonArgs {
    fn from(t: Label) -> ButtonArgs {
        ButtonArgs { label: t, align: Alignment::Center, icon: Icon::Circle }
    }
}

impl From<(Label, Alignment)> for ButtonArgs {
    fn from(t: (Label, Alignment)) -> ButtonArgs {
        ButtonArgs { label: t.0, align: t.1, icon: Icon::Circle }
    }
}

impl From<(Label, Icon)> for ButtonArgs {
    fn from(t: (Label, Icon)) -> ButtonArgs {
        ButtonArgs { label: t.0, align: Alignment::Center, icon: t.1 }
    }
}

impl From<(Label, Alignment, Icon)> for ButtonArgs {
    fn from(t: (Label, Alignment, Icon)) -> ButtonArgs {
        ButtonArgs { label: t.0, align: t.1, icon: t.2 }
    }
}

impl From<(Label, Icon, Alignment)> for ButtonArgs {
    fn from(t: (Label, Icon, Alignment)) -> ButtonArgs {
        ButtonArgs { label: t.0, align: t.2, icon: t.1 }
    }
}

will then allow all of the following combinations:

fn main() {
    let p = Panel;
    p.button( LABEL << "Hello" );
    p.button((LABEL << "Hello"));
    p.button((LABEL << "Hello", ALIGNMENT << Alignment::Left));
    p.button((LABEL << "Hello", ICON << Icon::Circle));
    p.button((LABEL << "Hello", ALIGNMENT << Alignment::Left, ICON << Icon::Circle));
    p.button((LABEL << "Hello", ICON << Icon::Circle, ALIGNMENT << Alignment::Left));

    p.button(Label::new("Hello"));
    p.button((LABEL << "Hello", Alignment::Left, Icon::Circle));
}

The extra set of parentheses is necessary when there is more than one argument.

However there is big downside: the user experience is degraded when using the wrong set of parameters.

The result of calling p.button("Hello"); is:

error[E0277]: the trait bound `ButtonArgs: std::convert::From<&str>` is not satisfied    --> <anon>:124:7
    | 124 |     p.button("Hello");
    |       ^^^^^^ the trait `std::convert::From<&str>` is not implemented for `ButtonArgs`
    |
    = help: the following implementations were found:
    = help:   <ButtonArgs as std::convert::From<Label>>
    = help:   <ButtonArgs as std::convert::From<(Label, Alignment)>>
    = help:   <ButtonArgs as std::convert::From<(Label, Icon)>>
    = help:   <ButtonArgs as std::convert::From<(Label, Alignment, Icon)>>
    = help: and 1 others
    = note: required because of the requirements on the impl of `std::convert::Into<ButtonArgs>` for `&str`


回答2:

You can take advantage of the From trait; that way you can drop some of the boilerplate:

use self::Shape::*;
use self::Alignment::*;

#[derive(Debug)]
struct Button {
    label: String,
    align: Option<Alignment>,
    icon: Option<Shape>,
}

#[derive(Debug)]
enum Shape { Circle }

#[derive(Debug)]
enum Alignment { Center }

impl From<(&'static str, Alignment, Shape)> for Button {
    fn from((l, a, i): (&'static str, Alignment, Shape)) -> Self {
        Button {
            label: l.to_owned(),
            align: Some(a),
            icon: Some(i)
        }
    }
}

fn main() {
    let b: Button = ("button", Center, Circle).into();

    println!("{:?}", b);
}

This implementation will work specifically for (&'static str, Alignment, Shape) tuples; however, you could additionally implement From<&'static str> that would produce a Button with the given label and None for the other components:

impl From<&'static str> for Button {
    fn from(l: &'static str) -> Self {
        Button {
            label: l.to_owned(),
            align: None,
            icon: None
        }
    }
}

let b2: Button = "button2".into();