5
$\begingroup$

In most languages that support function overloading, this is done rather ad hoc. To create a set of overloaded function you simply define all functions in that desired set with the same name in the same scope.

This has a few downsides when you want to refer to a particular overload. For example to pass it as a callable or to disambiguate which one you wish to call.

What are the alternatives to this? What are the pros and cons of those approaches?

$\endgroup$
1

5 Answers 5

6
$\begingroup$

The trivial option: just don't have overloads

Assuming function overloads are not dynamically dispatched based on the arguments' types, there is little need for them beyond the fact that it's nice for different versions of the same method to have the same name. But every program with function overloads can trivially be rewritten to not use overloads, by just giving the functions different names. So overloads are fundamentally just a convenience feature, and you can leave them out of a language without needing to offer an alternative.

In most cases it really is that simple, but there are two notable counterexamples:

  • Overloaded constructors or initialisers in most languages can't be given different names, because they either don't have names (e.g. Java, Javascript), or their name is mandated (e.g. Python). This can be addressed by using static factory methods instead of constructor overloads, albeit with the small inconvenience that subclass constructors cannot invoke static factory methods instead of a superclass constructor.
  • When two or more interfaces use the same name for methods with different signatures, if the interfaces belong to third-party libraries then the user cannot just rename them. In this case, overloading is necessary to implement both interfaces. However, this still leaves the problem where two interfaces have the same method name with the same signature, but you want to implement each interface differently.

Neither of these concerns really necessitate overloads, and overloads don't fully solve the second issue anyway, so you might well just decide to go the simple route and not have overloads at all.

Default arguments

Java's method overloads are mainly useful for things like this:

void foo() {
    foo(0);
}

void foo(int bar) {
    // ...
}

In many other languages such as Python, this is achieved more simply by giving bar a default value:

def foo(bar=0):
    # ...

It is difficult to support both overloads and default arguments, because some calls might be compatible with multiple overload signatures, where neither is "more specific". Normally languages only have one or the other; Typescript is a notable exception, but Typescript's overloads are ordered by priority and only affect the function's signature, not its implementation. So in Typescript, even if multiple overload signatures are applicable at a call-site, the highest-priority overload is selected, but also the runtime behaviour would be the same regardless.

The upsides of default arguments are that they can make code more readable, and don't require the function signatures to be duplicated; particular if there are multiple arguments with default values, there might need to be exponentially many overloads. That is, if three arguments all have default values then in a language like Java which has overloads instead of defaults, the user would need to write 23 = 8 different overloads to handle all the combinations.

One downside of default arguments is that they don't allow different "versions" of the function to have drastically different implementations. But in practice, overloads are mainly used to mimic default arguments, and even when they aren't, branching based on the argument values is generally sufficient.

$\endgroup$
4
  • $\begingroup$ Objective-C gets around the constructor issue by not necessarily having constructors in the traditional sense; +alloc returns uninitialized memory and your -init... methods initialize it. Where C++ would have new Foo(3), Objective-C has [[Foo alloc] initWithValue:3]. $\endgroup$
    – Bbrk24
    Commented Jun 22, 2023 at 3:22
  • $\begingroup$ There is nothing that says you cannot allow for a name to be added to a constructor in your language, $\endgroup$ Commented Jun 22, 2023 at 7:58
  • 1
    $\begingroup$ Templates are another strong reason to have overloads $\endgroup$
    – abel1502
    Commented Jun 22, 2023 at 21:28
  • $\begingroup$ As another example, C++ supports both overloading and default parameters $\endgroup$ Commented Jun 27, 2023 at 17:30
5
$\begingroup$

Explicit overload set

You first define all the possible function you wish to include in the overload set and then define the overload set and reference all other functions by name. For example Odin uses this.

add :: proc{add_ints, add_floats, add_numbers}

This has a consequences:

  • Each function in the overload set is explicit, there are no hidden surprises when you reuse a common name.
  • The set is entirely defined in a single location and is closed and cannot be expanded after the fact.
  • It provides an order to the functions in the set which could be used as a tie breaker in case ambiguity.
  • You can create multiple sets that include a particular function without needing to duplicate that function or needing to create a function that only forwards.
  • You can create that overload set local to a scope.
$\endgroup$
5
$\begingroup$

Julia provides an alternative approach: dynamic dispatch based on types. When you define a function and there is already another with the same name, you're actually modifying the old function by adding a new way to call it. This gives you another way to do overloading, and the disambiguation is done at runtime.

$\endgroup$
4
  • $\begingroup$ Is this like how TypeScript does it? $\endgroup$
    – Bbrk24
    Commented Jun 24, 2023 at 0:22
  • $\begingroup$ @Bbrk24 sounds similar, but I don't know TS well enough to say yes. $\endgroup$
    – ice1000
    Commented Jun 24, 2023 at 0:31
  • $\begingroup$ The way you overload a function in TypeScript is by forward-declaring it (yes, like in C) multiple times with different signatures, and then providing a singular implementation that works for all of the forward-declared versions. $\endgroup$
    – Bbrk24
    Commented Jun 24, 2023 at 0:40
  • $\begingroup$ Oh, then it's completely different ;) $\endgroup$
    – ice1000
    Commented Jun 24, 2023 at 0:44
3
$\begingroup$

Rust-style Traits

In trait/typeclass-based type systems, instead of overloading functions, you put the function in a trait, and implement that trait with different types of arguments. For example, the Add trait has an add function, which has a signature along the lines of fn add(self, other: Rhs) -> Output.

This function can then be referred to in a few ways. You can refer to add functions in general with Add::add; passing this as an argument to something would allow you to ignore what types are actually added. But if you want to be more specific, you can specify, for example, i64::add.

$\endgroup$
3
$\begingroup$

TypeScript: on the callee

Function overloads in TypeScript are the same function at runtime, and it’s on the callee to handle all possible sets of inputs and change behavior accordingly.

This approach only works if your compilation target is dynamically typed and okay with mismatched argument counts, as JavaScript is. It wouldn’t work in a strongly-typed language with a strict calling convention, such as Swift.

$\endgroup$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .