#set document(title: "5.3 Parameterization", 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")) == 5.3#h(0.6em)Parameterization Many programming languages offer systems for writing generic or polymorphic code, which let you write code once, and generate efficient, specialized code at compile time. Mojo's compile-time parameter system lets you define reusable code. A parameter is a compile-time input to a struct or function. Parameters appear in square brackets after the struct or function name. Parameters can take ordinary values, like Int or String: def multiplier\[factor: Int\](x: Int) -\> Int: return x \* factor def main(): comptime times\_ten = multiplier\[10\] x10 = times\_ten(3)Parameters accept both types and values at compile time. When a parameter accepts a type, the result is type-generic code. When it accepts a value, the result is value-generic code. struct MyList\[T: AnyType\]: \# ... type-generic struct FixedBuffer\[size: Int\]: \# ... value-genericMojo's parameters are similar to C++ template parameters or Rust generic parameters. In Mojo, "parameter" always means a compile-time value, and "argument" always means a run-time value. In most other languages, a parameter is part of a declaration and an argument is the value you pass at the call site. Mojo changes the meaning of "parameter" to refer specifically to compile-time values. Mojo makes this distinction visible in syntax: use \[\] for parameters and for arguments. In addition to parameterizing structs and functions, you can also define parameterized comptime values. === Parameterized functions To define a #emph[parameterized function], add parameters in square brackets ahead of the argument list. Each parameter is formatted just like an argument: a parameter name, followed by a colon and a type. In the following example, the function has a single parameter, count of type Int. def repeat\[count: Int\](msg: String): comptime for i in range(count): print(msg)The comptime keyword shown here causes the for loop to be fully unrolled at compile time. The comptime for requires the loop limits to be known at compile time. Since count is a parameter, range(count) can be calculated at compile time. Calling a parameterized function, you provide values for the parameters, just like function arguments: repeat\[3\]("Hello")Hello Hello HelloThe compiler resolves the parameter values during compilation, and creates a concrete version of the repeat\[\]() function for each unique parameter value. After resolving the parameter values and unrolling the loop, the repeat\[3\]() function would be roughly equivalent to this: def repeat\_3(msg: String): print(msg) print(msg) print(msg)#notebox("Note", rgb("#8a94a6"), rgb("#556666"), rgb("#f7f8fa"))[ This doesn't represent actual code generated by the compiler. By the time parameters are resolved, Mojo code has already been transformed to an intermediate representation in #link("https://mlir.llvm.org/")[MLIR]. ] If the compiler can't resolve all parameter values to constant values, compilation fails. ==== Overloading on parameters Functions and methods can be overloaded on their parameter signatures. For information on overload resolution, see Overloaded functions. === Parameters at a glance Parameters to a function or struct appear in square brackets after a function or struct name. Parameters always require type annotations. When you're looking at a function or struct signature, you may see some special characters such as / and \* in the parameter list. Here's an example: def my\_sort\[ \# infer-only parameters dtype: DType, width: Int, //, \# positional-only parameter values: SIMD\[dtype, width\], /, \# positional-or-keyword parameter compare: def(Scalar\[dtype\], Scalar\[dtype\]) thin -\> Int, \*, \# keyword-only parameter reverse: Bool = False, \]() -\> SIMD\[dtype, width\]:Here, compare is a function-typed parameter. Because the comparator is a noncapturing function value, the function type explicitly uses thin. Here's a quick overview of the special characters in the parameter list: - Double slash (//): parameters declared before the double slash are infer-only parameters. - Slash (/): parameters declared before a slash are positional-only parameters. Positional-only and keyword-only parameters follow the same rules as positional-only and keyword-only arguments. - A parameter name prefixed with a star, like \*Types identifies a variadic parameter (not shown in the example above). Any parameters following the variadic parameter are keyword-only. - Star (\*): in a parameter list with no variadic parameter, a star by itself indicates that the following parameters are keyword-only parameters. - An equals sign (=) introduces a default value for an optional parameter. === Parameters and generics Generics let functions work across multiple types, and let containers store values of many types. For example, #link("https://mojolang.org/docs/std/collections/list/List/")[List] takes a type parameter, so List\[Int\] holds integers and List\[String\] holds strings. In Mojo, generics use compile-time parameters. A function parameterized on a type is type-generic. A function parameterized on a value is value-generic. Both use \[\]. This function uses both a type parameter and a value parameter: def repeat\[ MsgType: Writable, // infer-only count: Int \](msg: MsgType): comptime for \_ in range(count): print(msg) def main(): repeat\[2\](42) \# prints 42 on two linesMsgType is type-generic. It accepts any Writable type. count is value-generic. It accepts a compile-time integer. Together, they let you write one function that works across types and specializes for different repeat counts. MsgType uses // to mark it as an infer-only parameter. The compiler infers the type from msg, so you only pass count explicitly. For more on generics, including trait conformance, conditional conformance, and value generics, see generics. === Parameterized structs You can also add parameters to structs. You can use parameterized structs to build generic collections. For example, a generic array type might include code like this: struct GenericArray\[ElementType: Copyable & ImplicitlyDeletable\]: var data: UnsafePointer\[Self.ElementType, MutUntrackedOrigin\] var size: Int def \_\_init\_\_(out self, var \*elements: Self.ElementType): self.size = len(elements) self.data = alloc\[Self.ElementType\](self.size) for i in range(self.size): (self.data + i).init\_pointee\_move(elements\[i\].copy()) def \_\_del\_\_(deinit self): for i in range(self.size): (self.data + i).destroy\_pointee() self.data.free() def \_\_getitem\_\_(self, i: Int) raises -\> ref\[self\] Self.ElementType: if i \< self.size: return self.data\[i\] else: raise Error("Out of bounds")This struct has a single parameter, ElementType, which is a placeholder for the data type you want to store in the array, sometimes called a #emph[type parameter]. ElementType conforms to the #link("https://mojolang.org/docs/std/builtin/value/Copyable/")[Copyable] trait and therefore to the #link("https://mojolang.org/docs/std/builtin/value/Movable/")[Movable] trait. As with parameterized functions, you need to pass in parameter values when you use a parameterized struct. In this case, when you create an instance of GenericArray, you need to specify the type you want to store, like Int, or Float64. (This is a little confusing, because the #emph[parameter value] you're passing in this case is a #emph[type]. That's OK: a Mojo type is a valid compile-time value.) You'll see that Self.ElementType is used throughout the struct where you'd usually see a type name. For example, as the formal type for the elements in the constructor, and the return type of the \_\_getitem\_\_() method. Here's an example of using GenericArray: var array = GenericArray(1, 2, 3) for i in range(array.size): end = ", " if i \< array.size - 1 else "\\n" print(array\[i\], end=end)1, 2, 3A parameterized struct can use the Self type to represent a concrete instance of the struct (that is, with all its parameters specified). For example, you could add a static factory method to GenericArray with the following signature: struct GenericArray\[ElementType: Copyable & ImplicitlyDeletable\]: ... \@staticmethod def splat(count: Int, value: Self.ElementType) -\> Self: \# Create a new array with count instances of the given valueHere, Self is equivalent to writing GenericArray\[Self.ElementType\]. That is, you can call the splat() method like this: var array = GenericArray\[Float64\].splat(8, 0)The method returns an instance of GenericArray\[Float64\]. ==== Referencing struct parameters As shown in the previous section, you reference a struct parameter using dot syntax, just like a struct method or field (for example, Self.ElementType). This struct parameter access works anywhere, not just inside a struct's methods. You can access parameters as attributes on the type itself: def on\_type(): print(SIMD\[DType.float32, 2\].size) \# prints 2Or as attributes on an #emph[instance] of the type: def on\_instance(): var x = SIMD\[DType.int32, 2\](4, 8) print(x.dtype) \# prints int32 ==== comptime members You can also define comptime values as members of a struct or trait declaration: struct Circle\[radius: Float64\]: comptime pi = 3.14159265359 comptime circumference = 2 \* Self.pi \* Self.radiusThese comptime members have a number of uses: - Constant values specific to the type. - Constant values calculated based on the struct's parameters. - Associated types based on the struct's parameters. The difference between parameters and comptime members is that parameter values are specified by the user, but comptime members represent either constant values or values derived from the input parameters. A required value is a comptime member without an initializer. Conforming types must provide a value for it. This is useful when the trait needs a compile-time constant that varies across conforming types. For example, a Measurable trait might require a unit string and an always\_positive boolean to validate measurements. Common trait-provided values include constants (like seeds), and trait compositions for refinements. Referencing comptime members works just like referencing struct parameters. You can reference a member using dot syntax (such as Self.IteratorType). comptime members as enumerationsSome Mojo types use comptime members to express enumerations. For example, the following code defines a Sentiment type that defines comptime constants for different sentiment values: \@fieldwise\_init struct Sentiment(Equatable, ImplicitlyCopyable): var \_value: Int comptime NEGATIVE = Sentiment(0) comptime NEUTRAL = Sentiment(1) comptime POSITIVE = Sentiment(2) def \_\_eq\_\_(self, other: Self) -\> Bool: return self.\_value == other.\_value def \_\_ne\_\_(self, other: Self) -\> Bool: return not (self == other) def is\_happy(s: Sentiment): if s == Sentiment.POSITIVE: print("Yes. 😀") else: print("No. ☹️")This pattern provides a type-safe enumeration. The #link("https://mojolang.org/docs/std/builtin/dtype/DType/")[DType] struct implements a simple enum using comptime members like this. This allows clients to use values like DType.float32 in parameter expressions or run-time expressions. comptime members as associated typesAssociated types are a common use for comptime members. For example, a List\[T\] struct holds values of type T. The list's \_\_iter\_\_() method returns a list iterator that returns values of type T. List uses a comptime member, IteratorType, to define the type of the returned iterator. The following code excerpt shows a simplified version of some of the List code, showing the List and its associated IteratorType: \@fieldwise\_init struct \_ListIter\[ mut: Bool, //, T: Copyable, origin: Origin\[mut\], \](ImplicitlyCopyable, Iterable, Iterator): comptime Element = Self.T \# Required by the Iterator trait var index: Int var src: Pointer\[List\[Self.Element\], Self.origin\] \# ... implementation omitted struct List\[T: Copyable\]( Boolable, Copyable, Defaultable, Iterable, Sized ): comptime IteratorType\[ iterable\_mut: Bool, //, iterable\_origin: Origin\[iterable\_mut\] \]: Iterator = \_ListIter\[Self.T, iterable\_origin\] \# ... code omitted def \_\_iter\_\_(ref self) -\> Self.IteratorType\[origin\_of(self)\]: return {0, Pointer(to=self)} \# ... code omittedThe IteratorType member is parameterized on an origin, so it can represent both mutable and immutable iterators. ==== Struct methods A struct's method can take its own parameters. For example, the SIMD.slice() method takes a size parameter: var m = SIMD\[DType.int32, 4\](1, 3, 5, 7) var n = m.slice\[2\]() print(n) \# prints \[1, 3\]A struct's lifecycle methods (\_\_init\_\_() and \_\_del\_\_()) are an exception to this rule—they can't take parameters. ==== Case study: the SIMD type For a real-world example of a parameterized type, let's look at the #link("https://mojolang.org/docs/std/builtin/simd/SIMD/")[SIMD] type from Mojo's standard library. #link("https://en.wikipedia.org/wiki/Single_instruction,_multiple_data")[Single instruction, multiple data (SIMD)] is a parallel processing technology built into many modern CPUs, GPUs, and custom accelerators. SIMD allows you to perform a single operation on multiple pieces of data at once. For example, if you want to take the square root of each element in an array, you can use SIMD to parallelize the work. Processors implement SIMD using low-level vector registers in hardware that hold multiple instances of a scalar data type. To use the SIMD instructions on these processors, the data must be shaped into the proper SIMD width (data type) and length (vector size). Processors may support 512-bit or longer SIMD vectors, and support many data types from 8-bit integers to 64-bit floating point numbers, so it's not practical to define all of the possible SIMD variations. Mojo's #link("https://mojolang.org/docs/std/builtin/simd/SIMD/")[SIMD] type (defined as a struct) exposes the common SIMD operations through its methods, and takes the SIMD data type and size values as parameters. This allows you to directly map your data to the SIMD vectors on any hardware. Here's a cut-down (non-functional) version of Mojo's SIMD type definition: struct SIMD\[dtype: DType, size: Int\]: var value: … \# Some low-level MLIR stuff here \# Create a new SIMD from a number of scalars def \_\_init\_\_(out self, \*elems: SIMD\[Self.dtype, 1\]): ... \# Fill a SIMD with a duplicated scalar value. \@staticmethod def splat(x: SIMD\[Self.dtype, 1\]) -\> SIMD\[Self.dtype, Self.size\]: ... \# Cast the elements of the SIMD to a different elt type. def cast\[target: DType\](self) -\> SIMD\[target, Self.size\]: ... \# Many standard operators are supported. def \_\_add\_\_(self, rhs: Self) -\> Self: ...So you can create and use a SIMD vector like this: var vector = SIMD\[DType.int16, 4\](1, 2, 3, 4) vector = vector \* vector for i in range(4): print(vector\[i\], end=" ")1 4 9 16As you can see, a simple arithmetic operator like \* applied to a pair of SIMD vector operates on the corresponding elements in each vector. Defining each SIMD variant with parameters is great for code reuse because the SIMD type can express all the different vector variants statically, instead of requiring the language to pre-define every variant. Because SIMD is a parameterized type, the self argument in its functions carries those parameters—the full type name is SIMD\[type, size\]. Although it's valid to write this out (as shown in the return type of splat()), this can be verbose, so we recommend using the Self type (from #link("https://peps.python.org/pep-0673/")[PEP673]) like the \_\_add\_\_() example does. === Using parameterized types and functions You can use parameterized types and functions by passing values to the parameters in square brackets. For example, for the SIMD type above, dtype specifies the data type and size specifies the length of the SIMD vector (which must be a power of 2): \# Make a vector of 4 floats. var small\_vec = SIMD\[DType.float32, 4\](1.0, 2.0, 3.0, 4.0) \# Make a big vector containing 1.0 in float16 format. var big\_vec = SIMD\[DType.float16, 32\](1.0) \# Do some math and convert the elements to float32. var bigger\_vec = (big\_vec + big\_vec).cast\[DType.float32\]()Note that the cast() method also needs a parameter to specify the type you want from the cast (the method definition above expects a target parameter value). Thus, just as the SIMD struct is a generic type definition, the cast() method is a generic method definition. At compile time, the compiler creates a concrete version of the cast() method with the target parameter bound to DType.float32. The code above shows the use of concrete types (that is, the parameters are all bound to known values). But the major power of parameters comes from the ability to define parameterized algorithms and types (code that uses the parameter values). For example, here's how to define a parameterized algorithm with Scalar that is datatype agnostic: from std.math import sqrt def rsqrt\[dt: DType\](x: Scalar\[dt\]) -\> Scalar\[dt\]: return 1 / sqrt(x) def main(): var v = Scalar\[DType.float16\](42) print(rsqrt(v))0.15429687When you write a type expression with square brackets, like List\[Int\], you must bind (specify a value for) all of the type's parameters. There are two exceptions to this rule: - You can explicitly unbind one or more parameters to create a partially-bound or unbound type. - You can omit parameters if Mojo can infer them from context. ==== Parameter inference The Mojo compiler can often #emph[infer] parameter values, so you don't always have to specify them. For example, in the previous section, this is how we called the parameterized rsqrt() function: var v = Scalar\[DType.float16\](42) print(rsqrt(v))The compiler infers the dt parameter based on the type of the v value passed into it, as if you wrote rsqrt\[DType.float16\](v) explicitly. Figure 1 shows a mental model for how parameter inference works. #figure(figph[Diagram: images parameters parameter inference], alt: "Diagram: images parameters parameter inference", caption: none) Parameter inference can seem a little confusing: it might seem like the compiler is inferring compile-time parameter values from run-time argument values. But in fact it's inferring parameters from the statically-known #emph[types] of the arguments. #notebox("Note", rgb("#8a94a6"), rgb("#556666"), rgb("#f7f8fa"))[ #emph[Inference failures] If parameter inference fails, the compiler reports an error, usually "failed to infer parameter 'param\_name'". Unfortunately, the compiler also sometimes reports this error incorrectly, for example, when the actual error is a type mismatch. In these cases, specifying the missing parameters explicitly often allows Mojo to report the correct error. ] Mojo can also infer the values of struct parameters from the arguments passed to a constructor or static method. For example, consider the following struct: struct One\[Type: Writable & Copyable & ImplicitlyDeletable\]: var value: Self.Type def \_\_init\_\_(out self, value: Self.Type): self.value = value.copy() def use\_one(): s1 = One(123) \# equivalent to One\[Int\](123) s2 = One("Hello") \# equivalent to One\[String\]("Hello")Note that you can create an instance of One without specifying the Type parameter—Mojo can infer it from the value argument. You can also infer parameters from a parameterized type passed to a constructor or static method: struct Two\[Type: Writable & Copyable & ImplicitlyDeletable\]: var val1: Self.Type var val2: Self.Type def \_\_init\_\_(out self, one: One\[Self.Type\], another: One\[Self.Type\]): self.val1 = one.value.copy() self.val2 = another.value.copy() print(String(self.val1), String(self.val2)) \@staticmethod def fire(thing1: One\[Self.Type\], thing2: One\[Self.Type\]): print("🔥", String(thing1.value), String(thing2.value)) def use\_two() raises: s3 = Two(One("infer"), One("me")) \# prints: infer me Two.fire(One(1), One(2)) \# prints: 🔥 1 2 \# Two.fire(One("mixed"), One(0)) \# Error: parameter inferred to two different valuesTwo takes a Type parameter, and its constructor takes values of type One\[Type\]. When constructing an instance of Two, you don't need to specify the Type parameter, since it can be inferred from the arguments. Similarly, the static fire() method takes values of type One\[Type\], so Mojo can infer the Type value at compile time. Note that passing two instances of One with different types doesn't work. #notebox("Note", rgb("#8a94a6"), rgb("#556666"), rgb("#f7f8fa"))[ If you're familiar with C++, you may recognize this as similar to Class Template Argument Deduction (CTAD). ] === Parameter declarations When you declare parameters on a struct or function, you have many of the same options as you have with arguments—you can define optional parameters with default values; keyword-only parameters; and variadic parameters. In addition, you can define #emph[infer-only parameters], which provide a flexible way of defining dependencies between parameterized types. ==== Optional parameters and keyword parameters Just as you can specify optional arguments in function signatures, you can also define an optional #emph[parameter] by giving it a default value. You can also pass parameters by keyword, just like you can use keyword arguments. For a function or struct with multiple optional parameters, using keywords allows you to pass only the parameters you want to specify, regardless of their position in the function signature. For example, here's a function with two parameters, each with a default value: def speak\[a: Int = 3, msg: String = "woof"\](): print(msg, a) def use\_defaults(): speak() \# prints 'woof 3' speak\[5\]() \# prints 'woof 5' speak\[7, "meow"\]() \# prints 'meow 7' speak\[msg="baaa"\]() \# prints 'baaa 3'Recall that when a parameterized function is called, Mojo can infer the parameter values. That is, it can determine its parameter values from the parameters attached to an argument. If the parameterized function also has a default value defined, then the inferred parameter value takes precedence. For example, in the following code, we update the parameterized speak\[\]() function to take an argument with a parameterized type. Although the function has a default parameter value for a, Mojo instead uses the inferred a parameter value from the bar argument (as written, the default a value can never be used, but this is just for demonstration purposes): \@fieldwise\_init struct Bar\[v: Int\]: pass def speak\[a: Int = 3, msg: String = "woof"\](bar: Bar\[a\]): print(msg, a) def use\_inferred(): speak(Bar\[9\]()) \# prints 'woof 9'As mentioned above, you can also use optional parameters and keyword parameters in a struct: struct KwParamStruct\[greeting: String = "Hello", name: String = "🔥mojo🔥"\]: def \_\_init\_\_(out self): print(Self.greeting, Self.name) def use\_kw\_params(): var a = KwParamStruct\[\]() \# prints 'Hello 🔥mojo🔥' var b = KwParamStruct\[name="World"\]() \# prints 'Hello World' var c = KwParamStruct\[greeting="Hola"\]() \# prints 'Hola 🔥mojo🔥'#notebox("Note", rgb("#8a94a6"), rgb("#556666"), rgb("#f7f8fa"))[ Mojo supports positional-only and keyword-only parameters, following the same rules as positional-only and keyword-only arguments. ] ==== Variadic parameters Mojo also supports variadic parameters, similar to Variadic arguments: struct MyTensor\[\*dimensions: Int\]: passVariadic parameters currently have some limitations that variadic arguments don't have: - Variadic parameters must be homogeneous—that is, all the values must be the same type. - The parameter type must be register-passable. Variadic keyword parameters (for example, \*\*kwparams) are not supported yet. ==== Infer-only parameters Sometimes you need to declare functions where parameters depend on other parameters. Because the signature is processed left to right, a parameter can only #emph[depend] on a parameter earlier in the parameter list. For example: def dependent\_type\[dtype: DType, value: Scalar\[dtype\]\](): print("Value: ", value) print("Value is floating-point: ", dtype.is\_floating\_point()) dependent\_type\[DType.float64, Float64(2.2)\]()Value: 2.2000000000000002 Value is floating-point: TrueYou can't reverse the position of the dtype and value parameters, because value depends on dtype. However, because dtype is a required parameter, you can't leave it out of the parameter list and let Mojo infer it from value: dependent\_type\[Float64(2.2)\]() \# Error!Infer-only parameters are a special class of parameters that are #strong[always] either inferred from context or specified by keyword. Infer-only parameters are placed at the #strong[beginning] of the parameter list, set off from other parameters by the // sigil: def example\[T: Copyable, //, list: List\[T\]\]()Transforming dtype into an infer-only parameter solves this problem: def dependent\_type\[dtype: DType, //, value: Scalar\[dtype\]\](): print("Value: ", value) print("Value is floating-point: ", dtype.is\_floating\_point())dependent\_type\[Float64(2.2)\]()Value: 2.2000000000000002 Value is floating-point: TrueBecause infer-only parameters are declared at the beginning of the parameter list, other parameters can depend on them, and the compiler always attempts to infer the infer-only values from bound parameters or arguments. There are sometimes cases where it's useful to specify an infer-only parameter by keyword. For example, the #link("https://mojolang.org/docs/std/memory/span/Span/")[Span] type is parameterized on origin: struct Span\[mut: Bool, //, T: Copyable, origin: Origin\[mut\]\]: \# ... implementation omittedHere, the mut parameter is infer-only. The value is usually inferred when you create an instance of Span. Binding the mut parameter by keyword lets you define a Span that requires a mutable origin. def mutate\_span(span: Span\[mut=True, Byte, ...\]): for i in range(0, len(span), 2): if i + 1 \< len(span): span.swap\_elements(i, i + 1)If the compiler can't infer the value of an infer-only parameter, and it's not specified by keyword, compilation fails. === Parameter expressions are just Mojo code A parameter expression is any code expression (such as a+b) that occurs where a parameter is expected. Parameter expressions support operators and function calls, just like run-time code, and all parameter types use the same type system as the run-time program (such as Int and DType). Because parameter expressions use the same grammar and types as run-time Mojo code, you can use many #link("https://en.wikipedia.org/wiki/Dependent_type")["dependent type"] features. For example, you might want to define a helper function to concatenate two SIMD vectors: def concat\[ dtype: DType, ls\_size: Int, rh\_size: Int, // \](lhs: SIMD\[dtype, ls\_size\], rhs: SIMD\[dtype, rh\_size\]) -\> SIMD\[ dtype, ls\_size + rh\_size \]: var result = SIMD\[dtype, ls\_size + rh\_size\]() comptime for i in range(ls\_size): result\[i\] = lhs\[i\] comptime for j in range(rh\_size): result\[ls\_size + j\] = rhs\[j\] return resultNote that the resulting length is the sum of the input vector lengths, and a simple + operation expresses this. ==== Powerful compile-time programming While simple expressions are useful, sometimes you want to write imperative compile-time logic with control flow. You can even do compile-time recursion. For instance, here is an example "tree reduction" algorithm that sums all elements of a vector recursively into a scalar: def slice\[ dtype: DType, size: Int, // \](x: SIMD\[dtype, size\], offset: Int) -\> SIMD\[dtype, size // 2\]: comptime new\_size = size // 2 var result = SIMD\[dtype, new\_size\]() for i in range(new\_size): result\[i\] = Scalar\[dtype\](x\[i + offset\]) return result def reduce\_add(x: SIMD) -\> Int: comptime if x.size == 1: return Int(x\[0\]) elif x.size == 2: return Int(x\[0\]) + Int(x\[1\]) \# Extract the top/bottom halves, add them, sum the elements. comptime half\_size = x.size // 2 var lhs = slice(x, 0) var rhs = slice(x, half\_size) return reduce\_add(lhs + rhs) def main(): var x = SIMD\[DType.int, 4\](1, 2, 3, 4) print(x) print("Elements sum:", reduce\_add(x))\[1, 2, 3, 4\] Elements sum: 10This makes use of the comptime if statement, which is an if statement that runs at compile-time. It requires that its condition be a valid parameter expression, and ensures that only the live branch of the if statement is compiled into the program. This is similar to use of the comptime for loop shown earlier. === Parameterized comptime values A #emph[parameterized comptime value] is a compile-time expression that takes a list of parameters and returns a compile-time constant value: comptime AddOne\[a: Int\] : Int = a + 1 comptime nine = AddOne\[8\]As you can see in the previous example, a parameterized comptime value is a little like a #emph[compile-time-only function]. A regular function or method can also be invoked at compile time: def add\_one(a: Int) -\> Int: return a + 1 comptime ten = add\_one(9)A major difference between a function and a parameterized comptime value is that the value of a comptime expression can be a type, while a function can't return a type as a value. \# Does not work—-dynamic type values not permitted def int\_type() -\> AnyType: return Int \# Works comptime IntType = IntBecause a comptime value can be a type, you can use parameterized comptime values to express new types: comptime TwoOfAKind\[dt: DType\] = SIMD\[dt, 2\] twoFloats = TwoOfAKind\[DType.float32\](1.0, 2.0) comptime StringKeyDict\[ValueType: Copyable & ImplicitlyDeletable\] = Dict\[ String, ValueType \] var b: StringKeyDict\[UInt8\] = {"answer": 42}Parameterized comptime declarations support the same features as parameterized structs or functions: infer-only parameters, keyword-only and optional parameters, automatic parameterization, and so on. comptime Floats\[size: Int, half\_width: Bool = False\] = SIMD\[ (DType.float16 if half\_width else DType.float32), size \] var floats = Floats\[2\](6.0, 8.0) var half\_floats = Floats\[2, True\](10.0, 12.0) === Partially-bound and unbound types A parameterized type with its parameters specified is said to be #emph[fully-bound]. That is, all of its parameters are bound to values. As mentioned before, you can only instantiate a fully-bound type (sometimes called a #emph[concrete type]). However, parameterized types can be #emph[unbound] or #emph[partially bound] in some contexts. For example, you can use comptime to create a type alias to a partially-bound type to create a new type that requires fewer parameters: comptime StringKeyDict = Dict\[String, \_\] var b: StringKeyDict\[UInt8\] = {"answer": 42}Here, StringKeyDict is a type alias for a Dict that takes String keys. The underscore \_ in the parameter list indicates that the second parameter, V (the value type), is unbound. You specify the V parameter later, when you use StringKeyDict. When used as a comptime value, any default values on unbound parameters are retained until a concrete type is formed (for example, by calling a struct's constructor): \@fieldwise\_init struct HasDefault\[x: Int, y: Int = 0\]: pass comptime UseDefault = HasDefault\[10\]var instance1 = UseDefault() \# instance of HasDefault\[10, 0\]When defining parameterized APIs, you can use partially-bound and unbound types to express type constraints with less boilerplate, a feature called automatic parameterization: \# standard declaration--explicit parameter declarations def take\_simd\[dtype: DType, size: Int\](value: SIMD\[dtype, size\]): pass \# automatically parameterized declaration def take\_floats(value: SIMD\[\_, \_\]): passYou can specify a partially-bound or unbound type several ways: - Explicitly unbind one or more parameters using an underscore (\_) in place of a parameter value: comptime StringKeyDict = Dict\[String, \_\] def take\_floats(floats: SIMD\[DType.float32, \_\]): passWhen writing a type expression like this, you must bind or explicitly unbind every parameter, unless Mojo can infer the parameter from context. For example, this produces an error: comptime Bad = Dict\[String\] \# error: 'Dict' failed to infer parameter 'V' - Explicitly unbind an arbitrary number of parameters at the end of a parameter list using an ellipsis (...): comptime PartiallyBound = SomeComplicatedType\[String, ...\] comptime Unbound = SomeComplicatedType\[...\] def take\_simd(v: SIMD\[...\]): passUsing an ellipsis unbinds any remaining parameters in the list, including keyword parameters. - Use the bare identifier with no square brackets to specify an unbound type: comptime SomeAlias = SomeComplicatedType def take\_simd2(v: SIMD): passAs a matter of style, SIMD\[...\] or SIMD\[\_, \_\] is usually preferable to the bare SIMD. The former versions are more explicit, and provide a visual cue to readers that they're looking at a parameterized type. ==== Partially-bound types versus parameterized comptime values You may notice that the comptime examples in this section look similar to the examples in the section on parameterized comptime values. For example, you could define the StringKeyDict alias using either syntax: \# partially-bound type comptime StringKeyDict = Dict\[String, \_\] \# parameterized comptime value comptime StringKeyDict\[V\] = Dict\[String, V\]For simple type aliases, you can use either a partially-bound type or a parameterized comptime value. For more complex aliases, parameterized comptime values give you a great deal more flexibility. === Automatic parameterization Writing heavily-parameterized APIs often produces long, repetitive signatures. For example, to define a function that takes any kind of SIMD value, you could write this: def take\_simd\[dtype: DType, size: Int, //\](vec: SIMD\[dtype, size\]): passThis signature represents a function that takes any SIMD value, inferring its dtype and size parameters. But it's a lot of code to do something pretty simple. To make it easier to write signatures like this, Mojo supports "automatic" parameterization. Instead of explicitly naming each parameter in the argument type, you specify a partially-bound or unbound type: def take\_simd(vec: SIMD\[...\]): print(vec.dtype) print(vec.size)var v = SIMD\[DType.float64, 4\](1.0, 2.0, 3.0, 4.0) take\_simd(v)float64 4In the above example, the take\_simd() function is automatically parameterized. The vec argument takes a value of type SIMD\[...\]—an unbound parameterized type. Mojo treats the unbound parameters on vec as infer-only parameters on the function. This is roughly equivalent to the following code: def take\_simd\[t: DType, s: Int, //\](vec: SIMD\[t, s\]): print(t) print(s)When you call take\_simd() you must pass it a concrete instance of the SIMD type—that is, one with all of its parameters specified, like SIMD\[DType.float64, 4\]. The Mojo compiler #emph[infers] the parameter values from the input argument. You can also use automatic parameterization with a partially bound type: def take\_floats(floats: SIMD\[DType.float32, \_\]): passThere are two important differences between a manually-parameterized signature and an automatically-parameterized signature: - With a manually-parameterized function, you can access the parameters by name (for example, t and s in the previous example), which is not an option in an automatically parameterized function. However, you can always access a type's parameters and comptime members using dot syntax—as in the automatic parameterization example, which used vec.dtype and vec.size to access parameters on the argument. - With the manually-parameterized function, you can pass the parameter value directly; that's not an option with automatically-parameterized functions. The unbound parameters are always inferred. In addition to using automatic parameterization in the argument list of a function, you can also use it in the parameter lists of functions, structs, and parameterized comptime values. ==== Examples of automatic parameterization This section shows more examples of using automatic parameterization. Automatic parameterization of parametersYou can also take advantage of automatic parameterization in the parameter list of a function, struct, or parameterized comptime value. For example: def simd\_param\[value: SIMD\[...\]\](): pass \# Equivalent to: def simd\_param\[dtype: DType, size: Int, //, value: SIMD\[dtype, size\]\](): passHere's another example using a parameterized comptime value: comptime SomeComptime\[s: SIMD\[...\]\] = SomeStruct\[s\] \# Equivalent to: comptime SomeComptime2\[dtype: DType, size: Int, //, S: SIMD\[dtype, size\]\] = SomeStruct\[S\]Automatic parameterization and type expressionsAs previous examples showed, you can access the parameters of an argument or parameter value using dot syntax (arg.param). You can also use this syntax inside a signature. For example, if you want your function to take two SIMD vectors with the same type and size, you can write code like this: def interleave(v1: SIMD\[...\], v2: type\_of(v1)) -\> SIMD\[v1.dtype, v1.size \* 2\]: var result = SIMD\[v1.dtype, v1.size \* 2\]() comptime for i in range(v1.size): result\[i \* 2\] = v1\[i\] result\[i \* 2 + 1\] = v2\[i\] return resultvar a = SIMD\[DType.int16, 4\](1, 2, 3, 4) var b = SIMD\[DType.int16, 4\](0, 0, 0, 0) var c = interleave(a, b) print(c)\[1, 0, 2, 0, 3, 0, 4, 0\]As shown in the example, you can use the magic type\_of(x) expression if you just want to match the type of an argument. In this case, it's more convenient and compact than writing the equivalent SIMD\[v1.dtype, v1.size\]. Automatic parameterization with partially-bound typesMojo also supports automatic parameterization: with partially-bound parameterized types (that is, types with some but not all of the parameters specified). For example, suppose you have a Fudge struct with three parameters: \@fieldwise\_init struct Fudge\[sugar: Int, cream: Int, chocolate: Int = 7\](Writable): passYou can write a function that takes a Fudge argument with just one bound parameter (it's #emph[partially bound]): def eat(f: Fudge\[5, ...\]): print("Ate", f)The eat() function takes a Fudge struct with the first parameter (sugar) bound to the value 5. The second and third parameters, cream and chocolate are unbound. The unbound cream and chocolate parameters become implicit parameters on the eat function. In practice, this is roughly equivalent to writing: def eat\[cr: Int, ch: Int, //\](f: Fudge\[5, cr, ch\]): print("Ate", String(f))In both cases, you can call the function by passing in an instance with the cream and chocolate parameters bound: eat(Fudge\[5, 5, 7\]()) eat(Fudge\[5, 8, 9\]())Ate Fudge (5,5,7) Ate Fudge (5,8,9)If you try to pass in an argument with a sugar value other than 5, compilation fails, because it doesn't match the argument type: eat(Fudge\[12, 5, 7\]()) \# ERROR: invalid call to 'eat': argument \#0 cannot be converted from 'Fudge\[12, 5, 7\]' to 'Fudge\[5, 5, 7\]'You can also explicitly unbind individual parameters. This gives you more freedom in specifying unbound parameters. For example, you might want to let the user specify values for sugar and chocolate, and leave cream constant. To do this, replace each unbound parameter value with a single underscore (\_): def devour(f: Fudge\[\_, 6, \_\]): print("Devoured", String(f))Again, the unbound parameters (sugar and chocolate) are added as implicit parameters on the function. You can also unbind parameters by keyword, or mix positional and keyword parameters, so the following function is roughly equivalent to the previous one: the first parameter, sugar is explicitly unbound with the underscore character. The chocolate parameter is unbound using the keyword syntax, chocolate=\_. And cream is explicitly bound to the value 6: def devour(f: Fudge\[\_, chocolate=\_, cream=6\]): print("Devoured", String(f))Both versions of the devour() function work with the following calls: devour(Fudge\[3, 6, 9\]()) devour(Fudge\[4, 6, 8\]())Devoured Fudge (3,6,9) Devoured Fudge (4,6,8) === Assert parameterized type equality with rebind() One of the consequences of Mojo not performing function instantiation in the parser like C++ is that Mojo cannot always figure out whether some parameterized types are equal and complain about an invalid conversion. This typically occurs in static dispatch patterns. For example, the following code won't compile: def take\_simd8(x: SIMD\[DType.float32, 8\]): pass def generic\_simd\[nelts: Int\](x: SIMD\[DType.float32, nelts\]): comptime if nelts == 8: take\_simd8(x)The parser will complain: error: invalid call to 'take\_simd8': argument \#0 cannot be converted from 'SIMD\[f32, nelts\]' to 'SIMD\[f32, 8\]' take\_simd8(x) ~~~~~~~~~~^~~This is because the parser fully type-checks the function without instantiation, and the type of x is still SIMD\[f32, nelts\], and not SIMD\[f32, 8\], despite the static conditional. The remedy is to manually assert the type of x, using the #link("https://mojolang.org/docs/std/builtin/rebind/rebind/")[rebind()] builtin, which inserts a compile-time assertion that the input and result types resolve to the same type after elaboration: def take\_simd8(x: SIMD\[DType.float32, 8\]): pass def generic\_simd\[nelts: Int\](x: SIMD\[DType.float32, nelts\]): comptime if nelts == 8: take\_simd8(rebind\[SIMD\[DType.float32, 8\]\](x))The compiler still checks that the types match, but does so later, during elaboration. If the types don't match, compilation fails. There are fairly simple rules for when to use rebind(): - #strong[Do] use rebind() when you know that two parametric types will be identical after elaboration. - #strong[Don't] use rebind() to cast between arbitrary data types. rebind() returns a reference to the rebound value. If you need to transfer the rebound value or assign it to a variable, use the #link("https://mojolang.org/docs/std/builtin/rebind/rebind_var/")[rebind\_var()] function.