Mojo allows multiple functions with the same name to coexist, provided their argument or parameter signatures are different. Function overloading features are seen in other languages as well, such as C++.
Function overloading allows semantically similar functions to share a name, facilitates easier customization of implementations based on argument or parameter signatures, and empowers the compiler to more rigorously type check at the function call site.
This article covers Mojo syntax for function overloading. In future articles we may dive into details of overload resolution at function call site and discuss best practices for overloading.
To begin, let’s examine overloading via argument signatures.
A note on code examples
The code examples in this article provide function signatures and invocations, and they typically skip implementation details. This is to maintain a focus on function overloading without distracting particulars of data structures and algorithms.
Argument signature overloading
Consider a struct representing a list of strings:
struct StringList:# Create empty listfn__init__(inoutself): ...# Overload 1# Insert `element` at end of list fn insert(inoutself, element: String):print("In Overload 1")# Overload 2# Insert `element` at `index`. Raises exception if `index` is outside [0, len],# where len is current length of this `StringList`fn insert(inoutself, element: String, index: Int):print("In Overload 2")# Overload 3# Insert all `elements` at end of listfn insert(inoutself, elements: StringList): print("In Overload 3")
1
Overload 1
2
Overload 2
3
Overload 3
The three insert functions share the same name. They are legal overloads since their arguments differ by count or by type. Overload 1 and Overload 2 differ in argument counts, with two and three arguments respectively. And Overload 1 and Overload 3 differ in the type of second argument — String and StringList respectively. Based on the number and type of values at the call site, the compiler attempts to resolve the call to one of the overloads. Let’s examine this next.
Call resolution
The Mojo compiler resolves the call at invocation site, selecting one of the overloads based on the argument list. Let’s examine the output of executing the insert call in this code:
var tasks: StringList = StringList()let task1: String = String("Review proposed design")# Call Site Atasks.insert(task1)
At Call Site A, insert is invoked with two values. The first is the implicit value tasks of type StringList and the second is the explicit value task1 of type String. Based on the number of values, both Overload 1 and Overload 2 are candidates. Among these two, only Overload 1 matches the argument types. Consequently, the compiler resolved the call at Call Site A to Overload 1.
Let’s take a look at another example:
# Call Site Btasks.insert(task1, 0)
1
Call Site B
In Overload 2
At Call Site Binsert is invoked with three values, of types StringList, String, and IntLiteral. This matches Overload 2. Note that the compiler implicitly converts the literal 0 from type IntLiteral to Int. We’ll discuss type conversions for call resolution in greater depth later in a later article.
No overloading on result type
Mojo functions cannot be overloaded on result type alone. Let’s look at what happens when this struct declaration is compiled:
struct StringList:# Overload 1# Count all itemsfn count(self) -> UInt32: ...# Overload 2# Count all itemsfn count(self) -> UInt64: ...
1
Overload 1
2
Overload 2
error: Expression [24]:8:5: redefinition of function 'count' cannot overload on return type only
fn count(self) -> UInt64: ... # <2>
^
Expression [24]:5:5: previous definition here
fn count(self) -> UInt32: ... # <1>
^
expression failed to parse (no further compiler diagnostics)
The compiler generated an error. The accompanying message indicates that Overload 2 of count is not legal since Overload 1 already exists.
Overloaded constructors
Mojo allows constructors to be overloaded as well. For example:
struct StringList:# Overload 1# Create empty listfn__init__(inoutself):print("In Overload 1")# Overload 2# Create list initialized with `count` number of copies of `element`fn__init__(inoutself, element: String, count: Int):print("In Overload 2")# Overload 3# Create list initialized with strings from `elements` between `start_index` and # `end_index`fn__init__(inoutself, elements: StringList, start_index: Int, end_index: Int):print("In Overload 3")
1
Overload 1
2
Overload 2
3
Overload 3
The argument lists of the three overloads differ by count or types, in keeping with Mojo’s overloading rules.
Here are invocations of each of them:
# Call Site Alet names = StringList()# Call Site Blet statuses = StringList("Pending", 3)# Call Site Clet copied_statuses = StringList(statuses, 1, 3)
When invoked with different types, a parameterized function effectively generates overloads with a shared function name.
Consider the simple parameterized swap function below that exchanges the referenced values:
# Swap the values of `value1` and `value2`fn swap[valueType: Copyable](inout value1: valueType, inout value2: valueType):let temp = value1 value1 = value2 value2 = temp
The compiler instantiates a function based on the type of arguments. Here’s an example with Int values:
var i1 =2var i2 =3print("Before swap:", i1, i2)# Call Site Aswap(i1, i2)print("After swap:", i1, i2)
1
Call Site A
Before swap: 2 3
After swap: 3 2
And now we use the same parameterized function with String:
var s1 ="Mojo"var s2 ="Programming"print("Before swap:", s1, s2)# Call Site Bswap(s1, s2)print("After swap:", s1, s2)
1
Call Site B
Before swap: Mojo Programming
After swap: Programming Mojo
Explicit parameter specification
Note that in swap invocations at Call Site A and Call Site B we relied on the compiler to determine the valueType parameter. An alternative is to spell out the parameter as shown below:
Before swap: 2 3
After swap: 3 2
Before swap: Mojo Programming
After swap: Programming Mojo
Parameter signature overloading
Mojo functions can also be overloaded on parameter signatures. The parameters of the overloads just need to differ in type or arity.
Take a look at this StringUtils struct with format functions overloaded via parameter signatures. They are a rudimentary version of string formatting functions found in other languages such as Python and C++.
Each overload takes the same logical arguments. It uses the format_str argument as a template, and replaces each occurrence of substring “{}” by the remaining arguments in order, converted to strings.
struct StringUtils: # Each overload of `format` returns a copy of the `format_str` with an occurrence # of substring "{}" replaced by the remaining argument, converted to strings.# There should be exactly one occurrence of the substring. If not, `format` raises # an `Error`.## Overload 2 extends overloads 1 by taking an additional `replacement_field` # parameter that enables customization of the field demarcator to other than the # default "{}".# Overload 1@staticmethodfnformat[ param1Type: Stringable ]( format_str: String, arg1: param1Type ) raises-> String:print("In Overload 1")return""# Placeholder return# Overload 2@staticmethodfnformat[ replacement_field: StringLiteral, param1Type: Stringable ]( format_str: String, arg1: param1Type ) raises-> String:print("In Overload 2")return""# Placeholder return
1
Overload 1
2
Overload 2
These format functions are valid overloads since their parameters differ in count. Overload 1 has one parameter - param1Type, while Overload 2 has two - replacement_field and param1Type.
Let’s try some invocations next.
Call resolution
If the replacement_field value parameter is not used, the Mojo compiler attempts to resolve the call to Overload 1. Let’s examine the output of executing calls to format.
Our first application of format generates a labelled username:
# Call Site Alet username = StringUtils.format("Username: {}", "dev#1")
While both overloads are candidates, Overload 1 is the simpler match and the one the compiler picked. We’ll dive into the compiler’s resolution rules in greater detail in a future article.
Let’s examine another example - this time leveraging the second overload, which carries the replacement_field parameter. Consider a case in which the format_str argument has the “{}” substring embedded, however it is to be preserved; not replaced. In this example, we have a JSON object with an name/value pair where the value is the empty JSON object “{}” and needs to be preserved. So, we instead use “$$” as the replacement field:
# Call Site Blet empty_object = StringUtils.format["$$"]('{"$$": {}}', "postal_address")
While both overloads are candidates, Overload 2 is the only one that accepts a StringLiteral value parameter and the one the compiler picked.
Explicit parameter specification
Note that Call Site B did not explicitly pass in String as a parameter and the Mojo compiler successfully inferred the type automatically. In contrast, here is invocation with two explicit parameters:
let empty_object = StringUtils.format["$$", String]('{"$$": {}}', "postal_address")
In Overload 2
Combined argument and parameter signatures overloading
Mojo allows function overloading via both argument and parameter signature overloading at the same time.
Let’s expand our StringUtils example from the previous section to introduce more overloads of the format function to support multiple replacement fields in format_str. While the overloads vary in argument and parameter signature, they all serve the purpose as before - format a string based on an input string template and replacements for demarcated fields:
struct StringUtils: # Each overload of `format` returns a copy of the `format_str` with all occurrences # of substring "{}" replaced by the remaining arguments in order, converted to # strings. The number of occurrences of the substring must be the same as the # number of remaining arguments. If not, `format` raises an `Error`.## Overloads 2, 4, and 6 extend overloads 1, 3, and 5 respectively with an# additional `replacement_field` parameter that enables customization of the field# demarcator to other than the default "{}".# Overload 1@staticmethodfnformat[param1Type: Stringable]( format_str: String, arg1: param1Type ) raises-> String:print("In Overload 1")return""# Placeholder return# Overload 2@staticmethodfnformat[ replacement_field: StringLiteral, param1Type: Stringable ]( format_str: String, arg1: param1Type ) raises-> String:print("In Overload 2")return""# Placeholder return# Overload 3@staticmethodfnformat[param1Type: Stringable, param2Type: Stringable]( format_str: String, arg1: param1Type, arg2: param2Type ) raises-> String:print("In Overload 3")return""# Placeholder return# Overload 4@staticmethodfnformat[ replacement_field: StringLiteral, param1Type: Stringable, param2Type: Stringable ]( format_str: String, arg1: param1Type, arg2: param2Type ) raises-> String:print("In Overload 4")return""# Placeholder return# Overload 5@staticmethodfnformat[ param1Type: Stringable, param2Type: Stringable, param3Type: Stringable ]( format_str: String, arg1: param1Type, arg2: param2Type, arg3: param3Type ) raises-> String:print("In Overload 5")return""# Placeholder return# Overload 6@staticmethodfnformat[ replacement_field: StringLiteral, param1Type: Stringable, param2Type: Stringable, param3Type: Stringable ]( format_str: String, arg1: param1Type, arg2: param2Type, arg3: param3Type ) raises-> String:print("In Overload 6")return""# Placeholder return
1
Overload 1
2
Overload 2
3
Overload 3
4
Overload 4
5
Overload 5
6
Overload 6
Call resolution
Let’s take a look at the output of executing some calls to format. The following generates a JSON object with one name/value pair:
# Call Site Clet key_value = StringUtils.format("format {}{}", "usage_count", 6)
The output demonstrates that Overload 5 was invoked.
Call Site D invokes format without replacement_field value parameter and four arguments. Since Overload 5 is the only candidate without a StringLiteral value parameter and 4 arguments, it is the only one examined further by the compiler. The first argument is a String and the remaining of Stringable types as required by that overload. Consequently, the compiler fully resolved the call to Overload 5.
Summary
Function overloading allows multiple functions to share a name. This enables meaningful grouping of functions based on their names, alternative implementations when needed, and stronger type checking at call sites.
Mojo supports overloading of functions via distinctive argument and parameter signatures. This capability extends to constructors as well. The compiler attempts to automatically resolve a call to an overloaded function based on types and arity of arguments and parameters.
We will examine Mojo’s overload resolution rules in greater depth in a future article.
Note that in C++ the words argument and parameter have different and overlapping meanings when compared to their meanings in Mojo. In C++, variables defined in function declaration and used within function definition are called parameters, while values used for function invocation are called arguments.