#set document(title: "2.2 Functions", author: "Modular Inc. / XYZ Homework") #set page(width: 8.5in, height: auto, margin: 1in) #import "@preview/cetz:0.5.2" #set text(font: ("STIX Two Text", "Libertinus Serif", "New Computer Modern"), size: 10.5pt, lang: "en") #show math.equation: set text(font: ("STIX Two Math", "New Computer Modern Math")) #set par(justify: true, leading: 0.62em, spacing: 0.9em) #set enum(spacing: 1.1em) // room between list items so tall inline fractions don't collide #set list(spacing: 1.1em) #set table(stroke: 0.5pt + rgb("#c7ccd3")) #let BLUE = rgb("#183B6F") // brand navy — section bars + example/solution labels (white on navy 11.09:1) #let ORANGE = rgb("#A94509") // brand primary-700 — AA-safe deep orange for TEXT (5.93:1 on white; raw brand #F37021 is 2.94:1 and must never carry text) #let RED = rgb("#DC2626") // brand error-600 #let GREEN = rgb("#059669") // brand success-600 (decoration only; small green text uses green-text #007942) #show heading.where(level: 1): it => block(width: 100%, above: 0pt, below: 16pt, fill: gradient.linear(BLUE, rgb("#2C5AA0")), inset: (x: 14pt, y: 12pt), radius: 3pt, text(fill: white, weight: "bold", size: 19pt, it.body)) #show heading.where(level: 2): it => block(width: 100%, above: 18pt, below: 10pt, fill: BLUE, inset: (x: 10pt, y: 6pt), radius: 2pt, text(fill: white, weight: "bold", size: 12pt, it.body)) #show heading.where(level: 3): it => text(fill: ORANGE, weight: "bold", size: 12.5pt, it.body) #show heading.where(level: 4): it => text(fill: BLUE, weight: "bold", size: 10.5pt, it.body) #let examplebox(label, title, body) = block(width: 100%, breakable: true, fill: rgb("#EFF1F5"), stroke: 0.5pt + rgb("#CFDDF0"), radius: 4pt, inset: 10pt, above: 12pt, below: 12pt)[ #block(below: 6pt)[#box(fill: BLUE, inset: (x: 6pt, y: 2pt), radius: 2pt, text(fill: white, weight: "bold", size: 8.5pt, label)) #h(0.4em) #strong[#title]] #body] // rail = decorative left rule (raw brand token); labelcolor = AA-safe label text shade #let notebox(label, rail, labelcolor, tint, body) = block(width: 100%, breakable: true, fill: tint, stroke: (left: 3pt + rail), inset: (left: 10pt, rest: 8pt), radius: (right: 4pt), above: 11pt, below: 11pt)[ #text(fill: labelcolor, weight: "bold", size: 7.5pt, tracking: 0.5pt)[#upper(label)] #linebreak() #body] #let solutionbox(body) = block(above: 4pt, below: 8pt)[ #text(fill: BLUE, weight: "bold", size: 8.5pt)[Solution] #linebreak() #body] #let figph(msg) = block(width: 100%, height: 60pt, fill: rgb("#f6f7f9"), stroke: (paint: rgb("#c7ccd3"), dash: "dashed"), radius: 4pt, inset: 10pt)[ #align(center + horizon, text(fill: rgb("#889"), style: "italic", size: 9pt, msg))] // Standardize inlined figure sizes: measure the natural CeTZ canvas, then scale to a // consistent envelope (aspect-aware; see build_typst.py FIG_* constants). Unlike the // print preamble, dimensions are FLOORED: in an editor a user can trim a figure to a // degenerate 1-D shape (a bare line), and w/h or tw/w would then divide by zero. #let _STD_W = 3.5 #let _WIDE_W = 5.6 #let _MAX_H = 3.4 #let _ASPECT_WIDE = 2.2 #let _UPSCALE_MAX = 1.15 #let stdfig(body) = context { let m = measure(body) let w = calc.max(m.width / 1in, 0.01) let h = calc.max(m.height / 1in, 0.01) let tw = if w / h > _ASPECT_WIDE { _WIDE_W } else { _STD_W } let s = calc.min(tw / w, _MAX_H / h, _UPSCALE_MAX) align(center, box(scale(x: s * 100%, y: s * 100%, reflow: true, body))) } #show figure: set block(breakable: false) #set figure(gap: 8pt) #show figure.caption: set text(size: 8.5pt, fill: rgb("#555")) == 2.2#h(0.6em)Functions Mojo uses the def keyword to define functions. Functions declared inside a struct are called "methods," but they have all the same qualities as "functions" described here. #notebox("Note", rgb("#8a94a6"), rgb("#556666"), rgb("#f7f8fa"))[ The fn keyword is deprecated as of Mojo v26.2. Use def for all function declarations. The fn keyword will be removed in a future release. ] === Anatomy of a function You'll find these elements in Mojo functions: def function\_name\[ parameters ... \]( arguments ... ) -\> return\_value\_type: function\_body Functions can have: - Parameters: A function can optionally take one or more compile-time #emph[parameter] values used for metaprogramming. - Arguments: A function can also optionally take one or more run-time #emph[arguments]. - Return value: A function can optionally return a value. - Function body: Statements that are executed when you call the function. Function definitions must include a body. All of the optional parts of the function can be omitted, so the minimal function is something like this: def do\_nothing(): pass If a function takes no parameters, you can omit the square brackets, but the parentheses are always required. Although you can't leave out the function body, you can use the pass statement to define a function that does nothing. ==== Arguments and parameters Functions take two kinds of inputs: #emph[arguments] and #emph[parameters]. Arguments are familiar from many other languages: they are run-time values passed into the function. def add(a: Int, b: Int) -\> Int: return a+b On the other hand, you can think of a parameter as a compile-time variable that becomes a run-time constant. For example, consider the following function with a parameter: def add\_tensors\[rank: Int\](a: MyTensor\[rank\], b: MyTensor\[rank\]) -\> MyTensor\[rank\]: \# ... In this case, the rank value needs to be specified in a way that can be determined at compilation time, such as a literal or expression. When you compile a program that uses this code, the compiler produces a unique version of the function for each unique rank value used in the program, with rank treated as a constant within each specialized version. This usage of "parameter" is probably different from what you're used to from other languages, where "parameter" and "argument" are often used interchangeably. In Mojo, "parameter" and "parameter expression" refer to compile-time values, and "argument" and "expression" refer to run-time values. By default, both arguments and parameters can be specified either by position or by keyword. These forms can also be mixed in the same function call. \# positional x = add(5, 7) \# Positionally, a=5 and b=7 \# keyword y = add(b=3, a=9) \# mixed z = add(5, b=7) \# Positionally, a=5 For more information on arguments, see Function arguments on this page. For more information on parameters, see Parameterization: compile-time metaprogramming. === Function requirements A function has the following requirements: - You must declare the type of each function parameter and argument. - If a function doesn't return a value, you can either omit the return type or declare None as the return type. \# The following function definitions are equivalent def greet(name: String): print("Hello,", name) def greet(name: String) -\> None: print("Hello,", name) - If the function returns a value, you must either declare the return type using the #strong[-\>] #emph[type] syntax or provide a named result in the argument list. \# The following function definitions are equivalent def incr(a: Int) -\> Int: return a + 1 def incr(a: Int, out b: Int): b = a + 1 For more information, see the Return values section of this page. === Function arguments #notebox("Note", rgb("#8a94a6"), rgb("#556666"), rgb("#f7f8fa"))[ #emph[Functions with / and \* in the argument list] You might see the following characters in place of arguments: slash (/) and/or star (\*). For example: def myfunc(pos\_only, /, pos\_or\_keyword, \*, keyword\_only): Arguments #strong[before] the / can be passed only by position. Arguments #strong[after] the \* can be passed only by keyword. For details, see Positional-only and keyword-only arguments You may also see argument names prefixed with one or two stars (\*): def myfunc2(\*names, \*\*attributes): An argument name prefixed by a single star character, like \*names identifies a variadic argument, while an argument name prefixed with a double star, like \*\*attributes identifies a variadic keyword-only argument. ] ==== Optional arguments An optional argument is one that includes a default value, such as the exp argument here: def my\_pow(base: Int, exp: Int = 2) -\> Int: return base \*\* exp def use\_defaults(): \# Uses the default value for \`exp\` var z = my\_pow(3) print(z) However, you can't define a default value for an argument that's declared with the mut argument convention. Any optional arguments must appear after any required arguments. Keyword-only arguments, discussed later, can also be either required or optional. ==== Keyword arguments You can also use keyword arguments when calling a function. Keyword arguments are specified using the format #emph[argument\_name] = #emph[argument\_value]. You can pass keyword arguments in any order: def my\_pow(base: Int, exp: Int = 2) -\> Int: return base \*\* exp def use\_keywords(): \# Uses keyword argument names (with order reversed) var z = my\_pow(exp=3, base=2) print(z) ==== Variadic arguments Variadic arguments let a function accept a variable number of arguments. To define a function that takes a variadic argument, use the variadic argument syntax \*#emph[argument\_name]: def sum(\*values: Int) -\> Int: var sum: Int = 0 for value in values: sum = sum + value return sum The variadic argument values here is a placeholder that accepts any number of passed positional arguments. You can define zero or more arguments before the variadic argument. When calling the function, any remaining positional arguments are assigned to the variadic argument, so any arguments declared #strong[after] the variadic argument can only be specified by keyword (see Positional-only and keyword-only arguments). Variadic arguments can be divided into two categories: - Homogeneous variadic arguments, where all of the passed arguments are the same type—all Int, or all String, for example. - Heterogeneous variadic arguments, which can accept a set of different argument types. The following sections describe how to work with homogeneous and heterogeneous variadic arguments. #notebox("Note", rgb("#8a94a6"), rgb("#556666"), rgb("#f7f8fa"))[ #emph[Variadic parameters] Mojo also supports variadic #emph[parameters], but with some limitations—for details see variadic parameters. ] Homogeneous variadic arguments When defining a homogeneous variadic argument (all arguments must be the same type), use \*#emph[argument\_name]: #emph[argument\_type]: def greet(\*names: String): ... Inside the function body, the variadic argument is available as an iterable list for ease of use. Concretely, that type is named #link("https://mojolang.org/docs/std/builtin/variadics/VariadicList/")[VariadicList]. Here is a simple example: def sum(\*values: Int) -\> Int: var sum: Int = 0 for value in values: sum = sum+value return sum Iterating over this list directly with a for..in loop currently produces a reference to the element, which can be mutable with a mut variadic list. Use the ref binding pattern to capture a mutable reference if you want to mutate the elements of the list: def make\_worldly(mut \*strs: String): for ref i in strs: i += " world" You can also directly index the list with integers as well: def make\_worldly(mut \*strs: String): for i in range(len(strs)): strs\[i\] += " world" Heterogeneous variadic arguments Implementing heterogeneous variadic arguments (each argument type may be different) is somewhat more complicated than homogeneous variadic arguments. To handle multiple argument types, the function must be generic, which requires using traits and parameters. So the syntax may look a little unfamiliar if you haven't worked with those features. The signature for a function with a heterogeneous variadic argument looks like this: def count\_many\_things\[\*ArgTypes: Intable\](\*args: \*ArgTypes): ... The parameter list, \[\*ArgTypes: Intable\] specifies that the function takes an ArgTypes parameter, which is a list of types, all of which conform to the #link("https://mojolang.org/docs/std/builtin/int/Intable/")[Intable] trait. The asterisk in \*ArgTypes indicates that ArgTypes is a #strong[variadic type parameter] (a list of types). The argument list, (\*args: \*ArgTypes) has the familiar \*args for the variadic argument, but instead of a single type, its type is defined as the variadic type list \*ArgTypes. The asterisk in \*args indicates a #strong[variadic argument], and the asterisk in \*ArgTypes refers to the variadic type parameter. This means that each argument in args has a corresponding type in ArgTypes, so args\[#emph[n]\] is of type ArgTypes\[#emph[n]\]. Inside the function, args becomes a #link("https://mojolang.org/docs/std/builtin/variadics/VariadicPack/")[VariadicPack] because the syntax \*args: \*ArgTypes creates a heterogeneous variadic argument. That means each element in args can be a different type that requires a different amount of memory. To iterate through the VariadicPack, the compiler must know each element's type, so you must use a comptime for loop: def count\_many\_things\[\*ArgTypes: Intable\](\*args: \*ArgTypes) -\> Int: var total = 0 comptime for i in range(args.\_\_len\_\_()): total += Int(args\[i\]) return total def main(): print(count\_many\_things(5, 11.7, 12)) 28 Notice that when calling count\_many\_things(), you don't actually pass in a list of argument types. You only need to pass in the arguments, and Mojo generates the ArgTypes list itself. Variadic keyword arguments Mojo functions also support variadic keyword arguments (\*\*kwargs). Variadic keyword arguments allow the user to pass an arbitrary number of keyword arguments. To define a function that takes a variadic keyword argument, use the variadic keyword argument syntax \*\*#emph[kw\_argument\_name]: def print\_nicely(\*\*kwargs: Int): for item in kwargs.items(): print(item.key, "=", item.value) \# prints: \# \`a = 7\` \# \`y = 8\` print\_nicely(a=7, y=8) In this example, the argument name kwargs is a placeholder that accepts any number of keyword arguments. Inside the body of the function, you can access the arguments as a dictionary of keywords and argument values (specifically, an instance of #link("https://mojolang.org/docs/std/collections/dict/OwnedKwargsDict/")[OwnedKwargsDict]). There are currently a few limitations: - Variadic keyword arguments are always implicitly treated as if they were declared with the var argument convention, and can't be declared otherwise: \# Not supported yet. def read\_var\_kwargs(read \*\*kwargs: Int): ... - All the variadic keyword arguments must have the same type, and this determines the type of the argument dictionary. For example, if the argument is \*\*kwargs: Float64 then the argument dictionary will be a OwnedKwargsDict\[Float64\]. - The argument type must conform to the #link("https://mojolang.org/docs/std/builtin/value/Copyable/")[Copyable] trait. - Dictionary unpacking is not supported yet: def takes\_dict(d: Dict\[String, Int\]): print\_nicely(\*\*d) \# Not supported yet. - Variadic keyword #emph[parameters] are not supported yet: \# Not supported yet. def var\_kwparams\[\*\*kwparams: Int\](): ... ==== Positional-only and keyword-only arguments When defining a function, you can restrict some arguments so that they can be passed only as positional arguments, or they can be passed only as keyword arguments. To define positional-only arguments, add a slash character (/) to the argument list. Any arguments before the / are positional-only: they can't be passed as keyword arguments. For example: def min(a: Int, b: Int, /) -\> Int: return a if a \< b else b This min() function can be called with min(1, 2) but can't be called using keywords, like min(a=1, b=2). There are several reasons you might want to write a function with positional-only arguments: - The argument names aren't meaningful for the caller. - You want the freedom to change the argument names later on without breaking backward compatibility. For example, in the min() function, the argument names don't add any real information, and there's no reason to specify arguments by keyword. For more information on positional-only arguments, see #link("https://peps.python.org/pep-0570/")[PEP 570 – Python Positional-Only Parameters]. Keyword-only arguments are the inverse of positional-only arguments: they can be specified only by keyword. If a function accepts variadic arguments, any arguments defined #emph[after] the variadic arguments are treated as keyword-only. For example: def sort(\*values: Float64, ascending: Bool = True): ... In this example, the user can pass any number of Float64 values, optionally followed by the keyword ascending argument: var a = sort(1.1, 6.5, 4.3, ascending=False) If the function doesn't accept variadic arguments, you can add a single star (\*) to the argument list to separate the keyword-only arguments: def kw\_only\_args(a1: Int, a2: Int, \*, double: Bool) -\> Int: var product = a1 \* a2 if double: return product \* 2 else: return product Keyword-only arguments often have default values, but this is not required. If a keyword-only argument doesn't have a default value, it is a #emph[required keyword-only argument]. It must be specified, and it must be specified by keyword. Any required keyword-only arguments must appear in the signature before any optional keyword-only arguments. That is, arguments appear in the following sequence in a function signature: - Required positional arguments. - Optional positional arguments. - Variadic arguments. - Required keyword-only arguments. - Optional keyword-only arguments. - Variadic keyword arguments. For more information on keyword-only arguments, see #link("https://peps.python.org/pep-3102/")[PEP 3102 – Keyword-Only Arguments]. === Overloaded functions All function declarations must specify argument types, so if you want a function to work with different data types, you need to implement separate versions of the function that each specify different argument types. This is called "overloading" a function. For example, here's an overloaded add() function that can accept either Int or String types: def add(x: Int, y: Int) -\> Int: return x + y def add(x: String, y: String) -\> String: return x + y If you pass anything other than Int or String to the add() function, you'll get a compiler error. That is, unless Int or String can implicitly cast the type into their own type. For example, String includes an overloaded version of its constructor (\_\_init\_\_()) that supports implicit conversion from a StringLiteral value. Thus, you can also pass a StringLiteral to a function that expects a String. When resolving an overloaded function call, the Mojo compiler picks the candidate that best fits the call according to the rules in Overload resolution, or reports the call as ambiguous if no single candidate is best. #notebox("Note", rgb("#8a94a6"), rgb("#556666"), rgb("#f7f8fa"))[ #emph[Overload Sets] An "overload set" is a collection of function overloads that share the same name but different signatures. - Overload sets can't be extended by imports, aliases, or parameters. - To avoid issues with local functions using the same name as an imported one, use an aliased import. from package import foo as imported\_foo won't conflict with a local function named foo. ] ==== Overload resolution When resolving an overloaded function, Mojo 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 constructor is \@implicit. Mojo does #strong[not] look at the return type, the raises effect, or any context surrounding the call. Two functions that differ only in return type or in whether they raises are duplicate definitions—the compiler rejects the second declaration. The overload resolution logic filters for candidates according to the following rules, in order of precedence: + Candidates requiring the smallest number of implicit conversions (in both arguments and parameters). + Candidates without variadic arguments. + Candidates without variadic parameters. + Candidates with the shortest parameter signature. + Non-\@staticmethod candidates (over \@staticmethod ones, if available). If the compiler can't figure out which function to use, you can resolve the ambiguity by explicitly casting your value to a supported argument type. For example, the following code calls the overloaded foo() function, but both implementations accept an argument that supports implicit conversion from String. So, the call to foo("Hello") is ambiguous and creates a compiler error. You can fix this by casting the value to the type you really want: struct MyString: \@implicit def \_\_init\_\_(out self, string: String): pass struct YourString: \@implicit def \_\_init\_\_(out self, string: String): pass def foo(name: MyString): print("MyString") def foo(name: YourString): print("YourString") def call\_foo(): \# foo("Hello") \# error: ambiguous call to 'foo' ... This call is ambiguous because two \`foo\` functions match it foo(MyString("Hello")) For the full overload-resolution rules and edge cases, see the #link("https://mojolang.org/docs/reference/function-declarations/#function-overloads")[function declarations reference]. === Return values Return value types are declared in the signature using the #strong[-\>] #emph[type] syntax. Values are passed using the return keyword, which ends the function and returns the identified value (if any) to the caller. def get\_greeting() -\> String: return "Hello" By default, the value is returned to the caller as an owned value. As with arguments, a return value may be implicitly converted to the named return type. For example, the previous example calls return with a string literal, "Hello", which is implicitly converted to a String. #notebox("Note", rgb("#8a94a6"), rgb("#556666"), rgb("#f7f8fa"))[ #emph[Returning a reference] A function can also return a mutable or immutable reference using a ref return value. For details, see Lifetimes, origins, and references. ] ==== Named results Named function results allow a function to return a value that can't be moved or copied. Named result syntax lets you specify a named, uninitialized variable to return to the caller using the out argument convention: def get\_name\_tag(var name: String, out name\_tag: NameTag): name\_tag = NameTag(name^) The out argument convention identifies an uninitialized variable that the function must initialize. (This is the same as the out convention used in struct constructors.) The out argument for a named result can appear anywhere in the argument list, but by convention, it should be the last argument in the list. A function can declare only one return value, whether it's declared using an out argument or using the standard #strong[-\>] #emph[type] syntax. A function with a named result argument doesn't need to include an explicit return statement, as shown above. If the function terminates without a return, or at a return statement with no value, the value of the out argument is returned to the caller. If it includes a return statement with a value, that value is returned to the caller, as usual. The fact that a function uses a named result is transparent to the caller. That is, these two signatures are interchangeable to the caller: def get\_name\_tag(var name: String) -\> NameTag: ... def get\_name\_tag(var name: String, out name\_tag: NameTag): ... In both cases, the call looks like this: tag = get\_name\_tag("Judith") Because the return value is assigned to this special out variable, it doesn't need to be moved or copied when it's returned to the caller. This means that you can create a function that returns a type that can't be moved or copied, and which takes several steps to initialize: struct ImmovableObject: var name: String def \_\_init\_\_(out self, var name: String): self.name = name^ def create\_immovable\_object(var name: String, out obj: ImmovableObject): obj = ImmovableObject(name^) obj.name += "!" \# obj is implicitly returned def main(): my\_obj = create\_immovable\_object("Blob") By contrast, the following function with a standard return value doesn't work: def create\_immovable\_object2(var name: String) -\> ImmovableObject: obj = ImmovableObject(name^) obj.name += "!" return obj^ \# Error: ImmovableObject is not copyable or movable Because create\_immovable\_object2 uses a local variable to store the object while it's under construction, the return call requires it to be either moved or copied to the callee. This isn't an issue if the newly-created value is returned immediately: def create\_immovable\_object3(var name: String) -\> ImmovableObject: return ImmovableObject(name^) \# OK === Raising and non-raising functions By default, when a function raises an error, the function terminates immediately and the error propagates to the calling function. If the calling function doesn't handle the error, it continues to propagate up the call stack. def raises\_error() raises: raise Error("There was an error.") Mojo functions are #emph[non-raising] by default. To declare that a function can propagate an error to its caller, add the raises keyword to the function signature. A non-raising function that calls a raising function #strong[must handle any possible errors.] \# This function will not compile def unhandled\_error(): raises\_error() \# Error: can't call raising function in a non-raising context \# Explicitly handle the error def handle\_error(): try: raises\_error() except e: print("Handled an error:", e) \# Explicitly propagate the error def propagate\_error() raises: raises\_error() All of the examples above use the built-in Error type. Mojo also supports #emph[typed errors], where you specify a custom error type a function can raise: def validate(value: Int) raises ValidationError -\> Int: ... For more information, see Errors, error handling, and context managers.