Did you ever created a Rectangle
struct. Did you ever dreamed that instead of having multiple named constructor, you could just use the good old new()
? Wish no more, I have something for you!
I take no responsibility if you do this at home. The operation may be dangerous and requires careful analysis of the risks taken.
In the bad old days, you would have used:
#[derive(Debug)]
struct Position(pub u32, pub u32);
#[derive(Debug)]
struct Rectangle {
top_left: Position,
width: u32,
height: u32,
}
impl Rectangle {
pub fn with_points(top: u32, right: u32, bottom: u32, left: u32) -> Self {
Rectangle {
top_left: Position(top, left),
width: right - left,
height: top - bottom,
}
}
pub fn with_area(top: u32, left: u32, width: u32, height: u32) -> Self {
Rectangle {
top_left: Position(top, left),
width: width,
height: height,
}
}
}
fn main() {
dbg!(Rectangle::with_points(3, 10, 0, 0));
dbg!(Rectangle::with_area(3, 0, 10, 3));
}
Ah, the bad old days! That's such a pity to have to find two name for the two different constructors.
Since the Rectangle
we are going to create are all the same, I will just paste the output once:
Rectangle {
top_left: Position(
3,
0,
),
width: 10,
height: 3,
}
But we can do much better. First let's create a few types
use derive_more::*;
struct WithPoints {
pub top: u32,
pub right: u32,
pub bottom: u32,
pub left: u32,
}
struct WithArea {
pub top: u32,
pub left: u32,
pub width: u32,
pub height: u32,
}
#[derive(From)]
enum Args {
WithPoints(WithPoints),
WithArea(WithArea),
}
Note: I'm using derive more automatically get the implementation of impl From<WithPoints> for Args
and impl From<WithArea> for Args
, but I could have done it myself.
And now, let's create our overloaded constructor:
impl Rectangle {
pub fn new(args: Args) -> Self {
match args {
Args::WithPoints(args) => Rectangle {
// Real code would add a few asserts
top_left: Position(args.top, args.left),
width: args.right - args.left,
height: args.top - args.bottom,
},
Args::WithArea(args) => Rectangle {
top_left: Position(args.top, args.left),
width: args.width,
height: args.height,
},
}
}
}
fn main() {
dbg!(Rectangle::new( WithPoints { top: 3, right: 10, bottom: 0, left: 0 }.into()));
dbg!(Rectangle::new( WithArea { top: 3, left: 0, width: 10, height: 3 }.into()));
}
That's such a relief to not have to find two different names for the various constructor! And as you can see, we even got named arguments for free. But that's not all, it is also possible to change the order of the arguments (since the order of structure's fields doesn't matter), and have default parameters (by implementing Default
on WithPoints
and/or WithArea
). Your life is going to be so much better!
The above approach is obviously satiric, but I think it is still interesting. Especially, this approach doesn't share the issues mentioned with other alternatives.
More specifically:
- this cannot generate monomorphization-time errors in generics
- we don't need specialization to know which overload must be called, even in the presence of generics
This is due to a major shift with function overloading design is most other languages: there is a closed set of overload functions, all defined at the same place.
Adding anonymous enum/union types to Rust are being discussed. This would allow to be able to not have to declare the Args
enum by doing it inline in the declaration of Rectangle::new
:
impl Rectangle {
pub fn new(args: enum { WithPoints, WithArea}) -> Self { ... }
}
When calling the function, we would need to create an instance of the anonymous struct from an instance of WithPoints
or WithArea
.
If this operation is explicit, it would be something like:
fn main() {
Rectangle::new( WithPoints { top: 3, right: 10, bottom: 0, left: 0 } as enum)); // postfix operator
Rectangle::new( become WithPoints { top: 3, right: 10, bottom: 0, left: 0 })); // prefix operator
}
But if function call is an implicit coercion point, then the syntax can become slightly lighter:
fn main() {
Rectangle::new( WithPoints { top: 3, right: 10, bottom: 0, left: 0 }));
}
There are also discussion on adding anonymous struct in Rust. This would allow to create the variants inline:
enum Args {
{
top: u32,
right: u32,
bottom: u32,
left: u32,
},
{
top: u32,
left: u32,
width: u32,
height: u32,
}
}
This suddenly removes (for real unlike the technique using current) the need to find a name for each overloads. As long as each anonymous struct have different fields types and names, it wouldn't be ambiguous.
fn main() {
Rectangle::new( Args::{ top: 3, right: 10, bottom: 0, left: 0 });
Rectangle::new( Args::{ top: 3, left: 0, width: 10, height: 3 });
}
And finally if both are implemented (anonymous struct and anonymous enum):
impl Rectangle {
pub fn new(args: enum Args {
{
top: u32,
right: u32,
bottom: u32,
left: u32,
},
{
top: u32,
left: u32,
width: u32,
height: u32,
}
}) -> Self { ... }
}
We would start to have something that definitively looks like function overloading:
fn main() {
Rectangle::new( { top: 3, right: 10, bottom: 0, left: 0 });
// the syntax could be even more simplified
Rectangle::new{ top: 3, left: 0, width: 10, height: 3 };
}
Conclusion:
- Should this technique be used: I don't think so
- Could it be a direction we want to take: I don't know, but it's something we can consider