How to best *fake* keyword style function arguments in Rust?
Asked Answered
O

3

17

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?

Oldie answered 13/1, 2017 at 7:56 Comment(3)
I've deleted my answer, but it still seems that the builder pattern can be used more elegantly than shown in the question. If integrated into the API, your caller can use something like: panel.button("Test").align(Center).icon(CIRCLE).make(). That is exactly equivalent to Python keyword args (provides default values, allows arbitrary order), and doesn't look awkward at all.Glochidiate
Agree, I was using the builder style, and was suggested declaring a struct might be an alternative... while Im sure some macro could avoid some of the overhead, it looks non-trivial and even then will be awkward.Oldie
Boost.Process (C++) has an interesting abuse of operator overloading. It creates a constant label, and then when you use label = "Test" this in turns create an object similar to what Label { label: "Test" } would do. Boost process combines this with variadic arguments to get a list of such objects, which it then supplements with the defaults. You do end up with panel.button(label = "Test", align = Center, icon = CIRCLE); calls... however it's unclear how it would translate in Rust (would have to pick another operator than = and might need to pass an array since no variadics?).Slushy
I
5

I think macros are the best solution to this problem. You can use the builder API and provide an easier macro-based sugar for those who dislike the builder pattern. Using the example in the question:

pub enum Shape { Circle }
pub enum Alignment { Center }
pub struct Button();

impl Button {
    pub fn new() -> Button {Button()}
    pub fn label(self, x: &str) -> Button { self }
    pub fn align(self, x: Alignment) -> Button { self }
    pub fn icon(self, x: Shape) -> Button { self }
}


macro_rules! button {
    ( $($i:ident = $e:expr),* ) => { 
        {
            let btn = Button::new();
            $(
                btn = btn.$i($e);
            )*
            btn
        }
    };
}

fn main() {
    let my_button = button!(label="hello", align=Alignment::Center, icon=Shape::Circle);
    // Equivalent to
    // let my_button = Button::new().label("hello").align(Alignment::Center).icon(Shape::Circle);
}
Inferior answered 27/7, 2021 at 14:17 Comment(0)
S
5

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`
Slushy answered 14/1, 2017 at 18:9 Comment(0)
I
5

I think macros are the best solution to this problem. You can use the builder API and provide an easier macro-based sugar for those who dislike the builder pattern. Using the example in the question:

pub enum Shape { Circle }
pub enum Alignment { Center }
pub struct Button();

impl Button {
    pub fn new() -> Button {Button()}
    pub fn label(self, x: &str) -> Button { self }
    pub fn align(self, x: Alignment) -> Button { self }
    pub fn icon(self, x: Shape) -> Button { self }
}


macro_rules! button {
    ( $($i:ident = $e:expr),* ) => { 
        {
            let btn = Button::new();
            $(
                btn = btn.$i($e);
            )*
            btn
        }
    };
}

fn main() {
    let my_button = button!(label="hello", align=Alignment::Center, icon=Shape::Circle);
    // Equivalent to
    // let my_button = Button::new().label("hello").align(Alignment::Center).icon(Shape::Circle);
}
Inferior answered 27/7, 2021 at 14:17 Comment(0)
D
0

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();
Deniable answered 13/1, 2017 at 8:10 Comment(6)
Button = ("button", Center, Circle).into(); looks more like regular function arguments since there are no keywords to name each argument. How would this work for optional arguments? so it could take both let b: Button = ("button", Center, Circle).into(); and let b: Button = ("button").into(); ?Oldie
I expanded the answer to include this functionality.Deniable
Does this mean you would need to declare a from function for every combination of arguments? Also, if you had 2 or more optional arguments of the same type, not sure how this could be done in a sane way.Oldie
You don't need From implemented for every combination, it depends on your use cases; in my example I only implemented 2 cases and they could suffice, as long as you either fill all details or just the name. Also the optional arguments can have the same type, there is no issue here - a tuple argument allows multiple homogeneous components.Deniable
@ljedrz: I am afraid that this solution is offered to the wrong question; this solves overloading (to an extent) but that is orthogonal to named arguments as far as I can see.Slushy
@MatthieuM. True (no argument names here), though it is a bit "less awkward" (at least e.g. when writing lots of test cases) than the referenced builder pattern and much easier than a macro simulating named arguments.Deniable

© 2022 - 2024 — McMap. All rights reserved.