Is there a simple way to overload functions?

I don't agree with this, because there isn't a rigid definition of behavior. Unless your definition is strictly a thing's methods.

Function overloading is extremely useful for specifying the granularity of functions. Cutting out if statements, match statements and other "validation" logic. Because you have a thing that isn't defined by its methods, but it's properties. Example, I have an enum of things. Each enum variant has different fields and so each variant's property is different from the other. I, as a function, want to do some logic based on which variant I get. Enter the match/if/switch control flow, the place where bugs go to hide. If instead I clearly had different functions, named the same, that take as their parameter a specific enum variant (awaiting variant types rfc), then I simply dispatch to that name and each specific variant gets matched to a specific function.

Function overloading is polymorphism for data and I don't agree with the line that it's a misconception.

1 Like

Still - if your enum types are able to perform some common behaviour, then the behaviour should be traitized (and then probably whole enum). Otherwise, switching on the enum should not execute function with same name - because there is no common behaviour for those fields.

Function overloading is in Rust it's just called "traits". The main restriction however is that all the overloads need to have the same number of arguments. The other restriction is that your overloads don't overlap.

To give a C++ example:

int f(int) { .. }
String f(String) { .. }

is valid when translated to rust but:

int f(int) { .. }

template <class T>
T f(T x) { .. }

Is not, at least not in the standard compiler (you can do some things like this in the nightly compiler but it's probably not as flexible as C++ though).

Finally, you'll need to store all your overloads either the crate the "trait" is defined, except you can put an overload in the crate where the overloaded type is defined. But you can't put overloads in a crate where neither the "trait" nor a type the overload is based on is defined.

But if you're not doing anything funky that breaks the above three rules, then overloading is perfectly easy to do in Rust. Just create a trait and your overloads are the impls for that trait.

1 Like

Actually you can emulate overloading functions in rust:

https://play.rust-lang.org/?version=nightly&mode=release&edition=2018&gist=5248a9d31f72f0f92d02fd92e88481bc

1 Like

Functions don't need to model some concept of behavior. I still don't understand what the definition of behavior is here and just seems to be used colloquially, which isn't helpful.

Functions that operate on certain data, the enum, are in their place outside the enum for a specific reason, by design by the programmer. When the response is "just attach those functions to the data enum/struct/Vec/u32/&[u8] then you end up with either:

  1. All functions that are needing to be polymorphic over data types must be implemented with traits for each data type. Leading to a ridiculously restrictive design space and a highly potential damaging one. Worse yet, the traits are often garbage code of the empty interface type trait EmptyTraitSoThisWillAcceptTheType {}.

  2. Brittle control flow validation logic for all those types everywhere with complimenting 400 line procedures. Don't forget to change each of these scary procedures whenever a new data type can be handled violating the open/closed principle.

How I end up with EmptyTraitSoThisWillAcceptTheType? If I have no function in any Trait I bound my type to, I literally can't do anything with them - even to allocate I need Sized. I cannot even access its fields. I really cannot imagine situation you are talking about.

Empty interface types are common outside of Rust. Address the other points, there's a whole post there, not just the low hanging fruit of empty trait, which I prefaced with empty interface. I would like to understand what's the situation that you can't imagine.

I just gave you an example where function overloading can eliminate bugs by not necessitating control flow. I gave you an example where function overloading can reduce code density at the function level. I gave you an example where function overloading can enable the open closed principle, improving code quality. I don't understand how "functions shouldn't do that because functions are just behavior" is helpful or what behavior actually means.

The trouble is that it's hard to see how you're coming to any of the conclusions that you are reaching.
Your statement about empty traits is the easiest thing to respond to because it provides the clearest indication of where there may be a misunderstanding; you seem to be equating traits with types.

An empty trait is nothing like an empty interface, because an empty interface is a type. The closest analogue to the empty interface in Rust is Box<dyn Any>. The only conceivable use case of an empty trait is to seal a trait from further implementations (by using private module trickery to prevent downstream crates from naming the trait).

I fail to see how this is any different from introducing a trait for that generic function.

enum Expr {
    Local(ExprLocal),
    Const(ExprConst),
    Add(ExprAdd),
    Mul(ExprMul),
}

struct ExprLocal(String);
struct ExprConst(Value);
struct ExprAdd(Box<Expr>, Box<Expr>);
struct ExprMul(Box<Expr>, Box<Expr>);

trait Eval {
    fn eval(&self) -> Value;
}

impl Eval for Expr {
    fn eval(&self) -> Value {
        match self {
            Expr::Local(e) => e.eval(),
            Expr::Const(e) => e.eval(),
            Expr::Add(e) => e.eval(),
            Expr::Mul(e) => e.eval(),
        }
    }
}

impl Eval for ExprLocal { fn eval(&self) -> Value { ... } }
impl Eval for ExprConst { fn eval(&self) -> Value { ... } }
impl Eval for ExprAdd { fn eval(&self) -> Value { ... } }
impl Eval for ExprMul { fn eval(&self) -> Value { ... } }

I also just want to respond to this, because I really don't see where you're coming from calling match brittle. if and if let, totally, but not match! Here's my typical workflow for adding a new choice for some setting in my program's configuration file:

  1. I add a new variant to the enum:
#[derive(Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq)]
#[serde(rename_all="kebab-case")]
pub enum SupercellSpec {
    Target([f64; 3]),
    Dim([u32; 3]),
    Matrix([[i32; 3]; 3]), // <-- new variant
}
  1. The compiler tells me all of the places I need to fix. (for this type there happens to only be one):
error[E0004]: non-exhaustive patterns: `Matrix(_)` not covered
    --> src/tasks/cmd/mod.rs:1074:19
     |
1074 |             match *self {
     |                   ^^^^^ pattern `Matrix(_)` not covered
     |
     = help: ensure that all possible cases are being handled, possibly by adding wildcards or more match arms

I religiously use match for any enum that might concieveably be extended in the future, even in places where I easily could have used if (e.g. match e { Enum::A => true, Enum::B => false } rather than e == Enum::A) to help ensure that nothing can possibly get missed on a refactor.

8 Likes

The trouble is that it's hard to see how you're coming to any of the conclusions that you are reaching. Your statement about empty traits is the easiest thing to respond to because it provides the clearest indication of where there may be a misunderstanding; you seem to be equating traits with types.

Empty garbage interfaces are used to plug in holes in other languages and that was the point. It wasn't a statement about traits in Rust, the subject of the thread has moved on to be more about is function overloading needed in programming, is it just some misconception. Addressing the one low hanging fruit and walking away patting your hands isn't an appropriate response, so I asked for the other points to be addressed.

I fail to see how this is any different from introducing a trait for that generic function.

I would consider the Eval trait to be a garbage trait, offering no real insight into the design surface of the program and is only there to be a plug to make up for function overloading. That match statement is a potential source of bugs and I think this is better

fn eval(e: ExprLocal...) { ... }
fn eval(e: ExprConst...) { ... }
fn eval(e: ExprAdd...) { ... }
fn eval(e: ExprMul...) { ... }

I also just want to respond to this, because I really don't see where you're coming from calling match brittle. if and if let , totally, but not match ! Here's my typical workflow for adding a new choice for some setting in my program's configuration file:

Control flow can and will find its way into match arms. Enums can have properties that may need to be checked in control flow. Even the venerable Option.

match result {
    Ok(v) if v > some_val => { ... },
    Ok(v) { ... },
    None if should_continue => { ... },
    None => { ... },
}

Pattern matching does not eliminate control flow if it was needed to begin with. And I've seen some gnarly match statements using every possible guard syntax. I only just used an if in the example.

Regardless, match statements alone can be brittle when trying to make up for a lack of function overloading because match statements have escape hatches for non exhaustive checking.

enum A {
  One,
  Two,
  Three
}

struct S;
impl S {
   fn foo(&self, a: A) {
       match a {
           // honestly each of these => {} curly braces are just functions waiting to get out
           A::One => {...},
           A::Two => {...},
           _ => {...}
       }
   }
}

// sometime later we include a new variant in A

enum A {
  One,
  Two,
  Three,
  Four
}

fn main() {
    ......
    ......
    let four = A::Four;
    our_struct.foo(four); // Nothing happens, _ => {} match arm fall through, we didn't expect this.
}

// with function overloading and variant types

impl S {
   fn foo(&self, o: A::One) {
       ....
   }

   fn foo(&self, o: A::Two) {
       ....
   }
}

fn main() {
    ......
    ......
    let four = A::Four;
    our_struct.foo(four); // error
}

This is a dead simple example of brittleness caused by the gap of not having function overloading. Per the definition from wikipedia software brittleness is the increased difficulty in fixing older software that may appear reliable, but fails badly when presented with unusual data or altered in a seemingly minor way.

In any case, match statements are still control flow. You and I can and will screw it up. You are trying to constrain the surface area of possible types inside an area and trigger logic for each square foot in that surface. Why not just constrain each area to one possible square foot. I don't understand how 6 functions with match statements scrubbing out types is better than 6 functions with highly constrained types. We only talked about enums here, this gets ridiculously more in favor of function overloading when we start using examples with primitives or any other non variadic data structures defined by their values. Say hello to your good friend if and else. I hope it has a unit test.

And how do you plan to use those four functions on your enum without a match?

Those are stucts in your example? With enums, we need variant types https://github.com/rust-lang/rfcs/pull/2593. It's my most awaited rfc and in combination with generics may be good enough (or even better) for me to not miss function overloading as much.

Even with variant types, you still need to write

match expr {
    e @ Expr::Local(..) => eval(e),
    e @ Expr::Const(..) => eval(e),
    e @ Expr::Add(..) => eval(e),
    e @ Expr::Mul(..) => eval(e),
}

Those are stucts in your example?

Making each enum variant a struct is a fairly common pattern for cases that require a large amount of flexibility in static dispatch versus selecting at runtime (see e.g. the AST types in syn).

3 Likes

Yes, with just variant types alone you would still need to dispatch to the rightly type aligned function. It's an improvement over this

fn foo_something(e: Expr) {
   // nauseating redundancy and more code, more checks, more places for bugs and an example of why code duplication is in fact a bad thing and not a myth
   match e {
      Expr::Something => {},
      _ =>
    }
}

fn dispatch(e: Expr) {
    match e {
       Expr::Something => foo_something(e.clone()) //of course, gotta clone because we're in the middle of some logic
       ....
    }
}

With variant types

fn foo_something(something: Expr::Something) {
    // no match we know the exact type
}

fn dispatch(e: Expr) {
    match e {
       Expr::Something => foo_something(e.clone())
       ....
    }
}

But with function overloading no more dispatch plumbing no more cloning just copy. In fact, what happened to all the control flow? Poof gone

fn foo(e: Expr::Something) {}
fn foo(e: Expr::SomethingElse) {}
fn foo(e: Expr::SomethingElseEven) {}

Yeah, matches may be messy. So why not use a &dyn Eval (or Box<dyn Eval>, or Rc<dyn Eval>, or some other smart reference type), and have Rust figure out the dispatch for you? Then there are absolutely no matches to concern yourself with!

3 Likes

I'm trying to do the below, but stuck:

struct get_max;
trait Callable<Args> {
    type Output;
    fn call(&self, args: Args) -> Self::Output;
}

impl Callable<i32, i32> for get_max(a: i32, b: i32) -> i32  { 
    if a > b {a} else {b}
}
impl Callable<f32, f32> for get_max(a: f32, b: f32) -> f32  {
    if a > b {a} else {b}
}

fn main() {
    println!("Max num is: {}", get_max(5,2));
}
1 Like

Did you check out the redit post from @frondeus. The just of it is this feature is a side effect of other work on Fn traits but allows this

#![feature(fn_traits, unboxed_closures)]
#[allow(non_camel_case_types)]
struct get_max;
fn get_max_fn_body<T: PartialOrd>(a: T, b: T) -> T {
    if a > b { a } else { b }
}
impl FnOnce<(i32, i32)> for get_max {
    type Output = i32;
    #[inline(always)]
    extern "rust-call" fn call_once(self, args: (i32, i32)) -> Self::Output {
        get_max_fn_body.call_once(args)
    }
}
impl FnMut<(i32, i32)> for get_max {
    #[inline(always)]
    extern "rust-call" fn call_mut(&mut self, args: (i32, i32)) -> Self::Output {
        get_max_fn_body.call_once(args)
    }
}
// you must impl any Fn trait that will be used, like FnMut,
// for every function signature you want to overload you must
// impl in this manner 
...

Why it gave me an error:

error[E0061]: this function takes 1 parameter but 2 parameters were supplied
  --> src/main.rs:26:21
   |
26 |     let x = get_max.call_mut(1,2);
   |                     ^^^^^^^^ expected 1 parameter

The full code I used is playground:

#![feature(fn_traits, unboxed_closures)]
#[allow(non_camel_case_types)]

struct get_max;
fn get_max_fn_body<T: PartialOrd>(a: T, b: T) -> T {
    if a > b { a } else { b }
}

impl FnOnce<(i32, i32)> for get_max {
    type Output = i32;
    #[inline(always)]
    extern "rust-call" fn call_once(self, args: (i32, i32)) -> Self::Output {
        get_max_fn_body.call_once(args)
    }
}

impl FnMut<(i32, i32)> for get_max {
    #[inline(always)]
    extern "rust-call" fn call_mut(&mut self, args: (i32, i32)) -> Self::Output {
        get_max_fn_body.call_once(args)
    }
}


fn main() {
    let x = get_max.call_mut(1,2);
    println!("x: {}", x);
}

Because call_mut takes one parameter - a tuple of all arguments to the original function, as you can see from its signature.

You can call it directly then you don't have to worry about passing as a tuple
Get_max()

Sorry for not formatting I'm on a phone

It is ok, and thanks, now it it is fine as:

fn main() {
    let x = get_max(10, 2);
    println!("x: {}", x);
}

So, as overall, it is working now as:

#![feature(fn_traits, unboxed_closures)]
#[allow(non_camel_case_types)]

struct get_max;
fn get_max_fn_body<T: PartialOrd>(a: T, b: T) -> T {
    if a > b { a } else { b }
}

impl FnOnce<(i32, i32)> for get_max {
    type Output = i32;
    #[inline(always)]
    extern "rust-call" fn call_once(self, args: (i32, i32)) -> Self::Output {
        get_max_fn_body.call_once(args)
    }
}

impl FnOnce<(f64, f64)> for get_max {
    type Output = f64;
    #[inline(always)]
    extern "rust-call" fn call_once(self, args: (f64, f64)) -> Self::Output {
        get_max_fn_body.call_once(args)
    }
}

fn main() {
    let x = get_max(10, 2);
    println!("x: {}", x);
    
    let y = get_max(10.3, 10.5);
    println!("y: {}", y);
}