For the complete Mojo documentation index, see llms.txt. Markdown versions of all pages are available by appending .md to any URL (e.g. /docs/manual/basics.md).
Mojo function overload reference
A function overload is one of two or more function declarations that share a name but differ in their signature. The compiler picks one of them at each call site. This is static dispatch: there's no runtime lookup. The choice is fixed when the call is type-checked.
An overload set is the collection of overloads the compiler considers at a call site. It contains the declarations that share the same name in the same scope.
Use overloads to give one operation more than one shape: different
argument types, different argument counts, different keyword names,
different self conventions, or different compile-time parameter
signatures.
def add(x: Int, y: Int) -> Int:
return x + y
def add(x: String, y: String) -> String:
return x + y
def main():
print(add(1, 2)) # 3
print(add("Hi, ", "Mojo")) # Hi, Mojo
Where overload sets form
Each scope builds its own overload set:
- Module scope. Declarations with the same name in the same module form one overload set.
- Struct scope. Methods on a struct (including
@staticmethod) form one overload set per method name. - Trait scope. Required and provided methods on a trait form one overload set per method name.
An overload set can't be extended across scopes. An import brings the name in as a non-function reference: you can't add another overload to it from your own module, and you can't redefine it. A local declaration that collides with an import produces an error.
To avoid the error, use an alias:
from some_package import add as imported_add
def add(x: Float64, y: Float64) -> Float64:
return x + y
# `add` resolves to the local definition.
# `imported_add` resolves to the imported one.
What the compiler considers
Overload resolution looks at:
- The number, position, and keyword of each argument.
- The type of each argument and each compile-time parameter.
- The argument conventions on each argument.
- Whether the candidate is an instance method or
@staticmethod. - Whether a deinitializer is
@implicit.
Overload resolution doesn't look at the return type or any other context surrounding the call.
Resolution rules
The compiler discards every candidate whose signature can't be satisfied by the call. It then compares the remaining candidates pairwise.
It applies the following rules in order until one wins. The compiler selects that candidate.
-
Pick the candidate that uses fewer implicit conversions between arguments and parameters. An empty match against an
*argsargument counts as an implicit conversion, so an exact match beats it. -
Pick the candidate that doesn't bind non-empty variadic arguments. A signature without
*argsbeats one whose*argsargument receives at least one value. -
Pick the candidate with fewer mismatched argument conventions.
-
Pick the candidate with a shorter parameter list. A function with no compile-time parameters beats one that declares a parameter. The parameter list also counts implicit parameters synthesized from argument types (for example, the unbound parameters of a
SIMD[...]argument become implicit parameters on the function).
After fitness comparisons, the compiler applies two tiebreakers:
-
Pick the candidate that is an instance method over a
@staticmethodwith the same name. -
Pick the candidate that is a non-implicit deinitializer over an implicit one.
If two candidates are equally good after these steps, the call is ambiguous. The compiler rejects it.
Overloading parameters
Functions can overload on compile-time parameters as well as on arguments:
def take_param[a: Int, b: Int]():
print("take_param[a: Int, b: Int]")
def take_param[a: Int, b: String]():
print("take_param[a: Int, b: String]")
def main():
take_param[1, 2]() # take_param[a: Int, b: Int]
take_param[1, "hi"]() # take_param[a: Int, b: String]
Overloading the self convention
A method can be overloaded by its self convention. The default call site
uses an immutable reference for self, so ref self wins. To reach the
var self overload, the caller transfers with ^ at the call site:
@fieldwise_init
struct Counter(Copyable):
var n: Int
def which(ref self) -> Int:
return 1
# Constrain `var self` to types where `^` actually transfers.
# For `TrivialRegisterPassable` types, `^` is a no-op. This
# overload would otherwise be silently unreachable.
def which(var self) -> Int where not conforms_to(
Self, TrivialRegisterPassable
):
return 2
def main():
var c1 = Counter(0)
print(c1.which()) # 1: default call uses an immutable self reference
var c2 = Counter(0)
print((c2^).which()) # 2: caller transfers self
Instance methods beat static methods
When both an instance method and a @staticmethod have the same
name, a method-call expression picks the instance method (rule 5):
struct StaticOverload:
def __init__(out self):
pass
def foo(mut self):
print("instance method")
@staticmethod
def foo():
print("static method")
def main():
var a = StaticOverload()
a.foo() # instance method
To call the static method explicitly, use the type name:
StaticOverload.foo() # static method
Variadic candidates lose ties
A signature without *args beats a variadic signature when both
match the call, because the variadic version costs one implicit
conversion for the empty pack (rule 1) or, with values supplied,
loses on rule 2:
def take(x: Int):
print("take(x: Int)")
def take(*xs: Int):
print("take(*xs: Int)")
def main():
take(1) # take(x: Int)
take(1, 2, 3) # take(*xs: Int): the only match.
Ambiguous calls
When two candidates are equally good, the call fails:
struct MyString:
@implicit
def __init__(out self, s: String):
pass
struct YourString:
@implicit
def __init__(out self, s: String):
pass
def foo(name: MyString):
print("MyString")
def foo(name: YourString):
print("YourString")
def main():
# Error because the call is ambiguous: both overloads need exactly
# one implicit conversion from `String`
foo("Hello")
Resolve ambiguity by casting at the call site:
def main():
foo(MyString("Hello")) # MyString
foo(YourString("Hello")) # YourString
Literals can trigger the same kind of ambiguity. An IntLiteral
converts to both Int and Float64 at equal cost, so a call like
take_param[1, 2]() against the two overloads below is ambiguous:
def take_param[a: Int, b: Int]():
pass
def take_param[a: Int, b: Float64]():
pass
def main():
# Error because `IntLiteral` converts to both `Int` and `Float64`
# at equal cost; the compiler can't pick a winner
take_param[1, 2]()
To resolve, remove the ambiguity or remove the overload by renaming one version.
Return types don't disambiguate
Two overloads that differ only in their return type are indistinguishable to overload resolution:
def parse(s: String) -> Int:
return 0
# Error because `parse` cannot overload on return type only;
# the differing return type doesn't form a new overload
def parse(s: String) -> Float64:
return 0.0
Use different argument types, an extra parameter, or a different function name instead.
raises doesn't disambiguate
Two functions that differ only in whether they raises have the
same signature for overload-set purposes. The compiler rejects the
second declaration:
def maybe_raise(x: Int) -> Int:
return x
# Error because `maybe_raise` already has this signature;
# `raises` isn't part of the signature for overload purposes
def maybe_raise(x: Int) raises -> Int:
raise Error("nope")
If both behaviors are needed, give them distinct names or make the raising version take a different argument shape.
Best practices
- Use overloads when each version implements the same operation on a different shape of input. Don't overload to mean different things under the same name.
- Don't rely on two implicit initializers being reachable from the same source type. Cast at the call site or make one initializer non-implicit.
- Prefer overloading on argument type over overloading on convention. Convention-based overloads work, but they're easy to misread.
- When an overload set should accept many types, write one
parameterized function constrained with
whereinstead of many near-duplicates. The compiler picks the concrete signature when both are present (rule 4). - Don't define a function with the same name as one you imported. The compiler rejects it. Import under an alias when you need both names.