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?
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`
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();