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`
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. – Glochidiatelabel
, and then when you uselabel = "Test"
this in turns create an object similar to whatLabel { 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 withpanel.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