[don't try this at home] emulating function overloading using enum

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!

playground


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
9 Likes

(disclaimer: this post's tone is mostly in jest, but the code works; this is legitimately how I would do overloading if an API was significantly better with it)

Oh no, your new Rectangle::new does overload resolution at runtime (matching on the enum) as opposed to at compile-time, as when you had separate constructors! Let's fix that by adding more monomorphization:

trait NewRectangle {
    fn new_rectangle(this: Self) -> Rectangle;
}
impl NewRectangle for WithPoints {
    // NB: the `: Self` can potentially be removed with future features
    fn new_rectangle(WithPoints { top, right, bottom, left }: Self) -> Rectangle {
        Rectangle {
            // Real code would add a few asserts
            top_left: Position(top, left),
            width: right - left,
            height: top - bottom,
        }
    }
}
impl NewRectangle for WithArea {
    fn new_recrangle(WithArea { top, left, width, height }: Self) -> Rectangle {
        Rectangle {
            top_left: Position(top, left),
            width,
            height,
        }
    }
}
impl Rectangle {
    fn new<T: NewRectangle>(args: T) -> Self {
        NewRectangle::new_rectangle(args)
    }
}

Perfectly clear, and now your different constructors are monomorphized separately -- with_points -> new::<WithPoints>, with_area -> new::<WithArea> -- removing the runtime space/time overhead of the enum dispatched solution!

(But do note that this pushes actual codegen of new onto every downstream that uses it, rather than only being codegenned once as part of your crate. Hey core teams: when do we get to expose pre-compiled monomorphizations, so this only has compile/write cost in the author crate and no compile cost in the downstream crates? (But definitely still a write complexity cost))

5 Likes

Not bad, not bad! But I think I have something better:

// WithPoints and WithArea needs to be declared somewhere

impl From<WithPoints> for Rectangle {
    fn from(args: WithPoints) -> Self {
        Rectangle {
            top_left: Position(args.top, args.left),
            width: args.right - args.left,
            height: args.top - args.bottom,
        }
    }
}
impl From<WithArea> for Rectangle {
    fn from(args: WithArea) -> Self {
        Rectangle {
            top_left: Position(args.top, args.left),
            width: args.width,
            height: args.height,
        }
    }
}

impl Rectangle {
    pub fn new<Args: Into<Self>>(args: Args) -> Self {
        args.into()
    }
}

Like your proposition, the call site is really clean.

fn main() {
    Rectangle::new( WithPoints { top: 3, right: 10, bottom: 0, left: 0 });
    Rectangle::new( WithArea { top: 3, left: 0, width: 10, height: 3 });
}

And if anonymous struct are ever added this would be even less boilerplate by removing the need to create the WithPoints and WithArea structs. It would no longer be possible to implement From, but it's not an issue since all we need is Into. This would be the complete implementation:

impl Into<Rectangle> for { top: u32, right: u32, bottom: u32, left: u32 }
    fn into(self) -> Rectangle {
        Rectangle {
            top_left: Position(self.top, self.left),
            width: self.right - self.left,
            height: self.top - self.bottom,
        }
    }
}
impl Into<Rectangle> for { top: u32, left: u32, width: u32, height: u32 }
    fn into(self) -> Rectangle {
        Rectangle {
            top_left: Position(self.top, self.left),
            width: self.width,
            height: self.height,
        }
    }
}
impl Rectangle {
    pub fn new<Args: Into<Self>>(args: Args) -> Self {
        args.into()
    }
}
fn main() {
    Rectangle::new( { top: 3, right: 10, bottom: 0, left: 0 });
    Rectangle::new( { top: 3, left: 0, width: 10, height: 3 });
}
3 Likes

You have basically just invented a crate I made for April fools day last year: function_group. I didn't add support for static methods but the same rules apply.

struct TestStruct(usize);

function_group! {
    fn add_to_struct(&mut self : TestStruct) {
        (one : usize) {
            self.0 += one;
        }
        (one : usize, two : usize){
            self.0 += one + two; 
        }
    }
}

let mut x = TestStruct(10);
x.add_to_struct((5,)); //unfortunately the calls site is a little, uh funky
x.add_to_struct((1,2));
assert!(x.0 == 18);

The macro above expands into roughly (cleaned up)

trait add_to_struct_TestStruct<Arg> {
    fn add_to_struct(&mut self, _args: Arg) -> ();
}
impl add_to_struct_TestStruct<(usize,)> for TestStruct {
    fn add_to_struct(&mut self, (one,): (usize,)) -> () {
            self.0 += one;
    }
}
impl add_to_struct_TestStruct<(usize, usize)> for TestStruct {
    fn add_to_struct(&mut self, (one, two): (usize, usize)) -> () {
            self.0 += one + two;
    }
}
2 Likes

That is not overloading. This is overloading.

6 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.