#set document(title: "5.5 Generics", 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.5#h(0.6em)Generics Type generics let you write code once and use it across many types without duplicating logic. You don't need separate implementations or type checks for each case. Mojo generates specialized versions for each type you use. Most languages stop there, and their generics only parameterize over types. Mojo also supports value generics. Its parameter system accepts both types and compile-time values using the same \[\] syntax. Mojo distinguishes compile-time parameters from runtime arguments in its syntax. Parameters go in square brackets \[\] and resolve at compile time. Arguments go in parentheses and resolve at runtime. You see this distinction at every definition and call site, so you always know what the compiler specializes and what gets passed at runtime. \# T is a type parameter, threshold is a value parameter \# Both are compile-time. values is a runtime argument. def count\_above\[ T: Comparable & Copyable, threshold: T \](values: List\[T\]) -\> Int: var count = 0 for v in values: if v \> threshold: count += 1 return count Many generics use traits to constrain which types work with the code. A trait defines what a type must do, and generic code declares which traits it requires. The compiler enforces these requirements and generates specialized code for each concrete type at the call site. === The value of generics Generics eliminate duplicated logic. You write one implementation that adapts at compile time to the types and values you use, including types from libraries you didn't write. Type generics let one function, method, or struct work across many types. You declare requirements once using trait conformance, and the compiler enforces them. You avoid near-identical implementations and fix bugs in one place. Value generics let you parameterize over compile-time constants such as sizes, thresholds, or feature flags. The compiler can specialize code paths, remove dead branches, and optimize for specific values without runtime cost. Together, they let you express intent once and apply it broadly without sacrificing correctness, clarity, or performance. #notebox("Note", rgb("#8a94a6"), rgb("#556666"), rgb("#f7f8fa"))[ The compiler generates specialized code for each combination of concrete types and values at call sites. This enables static checking and optimization, but increases compile time and code size. ] === Type generics Type generics let you write code that works across many types. You define behavior once, and the compiler specializes it for each concrete type at the call site. ==== Setting generic parameter constraint annotations #emph[Constraints] define what a type must do. You express the annotations as traits or trait compositions. These annotations use trait requirements to restrict which types your generic code accepts. You must always constrain generic parameters. Traits define the operations and guarantees your code relies on. They also expose associated types (compile-time aliases) that your implementation can use. With these constraints in place, the compiler can check correctness and report missing requirements early. - The most permissive conformance is #link("https://mojolang.org/docs/std/builtin/anytype/AnyType/")[AnyType]. It places no behavioral requirements on a type and serves as the least restrictive option. - #link("https://mojolang.org/docs/std/builtin/anytype/ImplicitlyDeletable/")[ImplicitlyDeletable] is a common baseline for types with lifetimes. It marks types that need cleanup when they go out of scope. Generic code that stores or owns values often requires it. Without a fixed set of required features, generic code can't do meaningful work. The compiler has no guarantees about what operations are valid. Examples on this page use trait composition (such as Equatable & Copyable) to declare the exact capabilities an algorithm needs. Each trait defines required API elements that concrete types must provide at the call site. === Basic generics: compare two lists Consider comparing two lists to test whether they contain the same values in the same order. You could write a separate implementation for each element type. Or you can write one generic function that works for any list whose elements support the operations you need. This concrete version only works with integers: def all\_equal\_int(ref lhs: List\[Int\], ref rhs: List\[Int\]) -\> Bool: if len(lhs) != len(rhs): return False for left, right in zip(lhs, rhs): if left != right: return False return True The generic version doesn't care about the element type. It only requires the capabilities the algorithm uses: elements must support equality comparison and be copyable. With those requirements, this updated function walks both lists element by element, just like the integer version: def all\_equal\[ T: Equatable & Copyable \](ref lhs: List\[T\], ref rhs: List\[T\]) -\> Bool: if len(lhs) != len(rhs): return False for left, right in zip(lhs, rhs): if left != right: return False return True Both implementations follow the same logic: check lengths, return False on the first mismatch, and return True if no differences are found. ==== How this works The generic pieces in this example make a function reusable across many types. The first sign is the type parameter T. You declare type parameters in square brackets before the function arguments. Here, T represents the element type for both lists. #notebox("Note", rgb("#8a94a6"), rgb("#556666"), rgb("#f7f8fa"))[ Mojo favors names that show purpose over type. It follows naming conventions used in languages like Rust and C++, which keeps generic signatures readable. Type parameter names use PascalCase. They can be short (T, E) or descriptive (ErrorType, Element). Common conventions: - T, U, V general type parameters - K, V key and value types - H hasher types or functions - E error types, as in Result\[T, E\] Value parameter names use lower\_snake\_case. They should be descriptive (capacity, hasher, value, writer, tile\_x). ] When you call all\_equal(), the compiler infers T from the call site: print("Int (Expect True):\\t", all\_equal(\[1, 2, 3\], \[1, 2, 3\])) \# True print("Int (Expect False):\\t", all\_equal(\[1, 2, 3\], \[4, 5, 6\])) \# False print("String (Expect True):\\t", all\_equal(\["hello", "world"\], \["hello", "world"\])) \# True print("String (Expect False):\\t", all\_equal(\["hello", "world"\], \["goodbye", "world"\])) \# False The compiler generates a concrete, type-specific version of all\_equal() for each type you use. In this example, it creates one for Int and one for String. #notebox("Note", rgb("#8a94a6"), rgb("#556666"), rgb("#f7f8fa"))[ In type theory terms, the parametric all\_equal() function is #emph[polymorphic]. The generated type-specific versions are #emph[monomorphic]. ] ==== Type requirements Equatable is required by this algorithm. Copyable is often needed for loops and container elements. The compiler enforces both, so you don't have to guess. Keep requirements minimal: T: Equatable & Copyable The ampersand (&) composes traits. It means both requirements apply. Use the fewest constraints your code needs. That keeps your function usable with more types. For example, if you remove Equatable from all\_equal(), the code fails to compile because the compiler can't guarantee that != exists for all T. #notebox("Note", rgb("#8a94a6"), rgb("#556666"), rgb("#f7f8fa"))[ A type supports != by implementing \_\_ne\_\_(). ] When your code uses an operation not covered by the constraints, the compiler reports an error. The type is #emph[underspecified]. Required behavior isn't guaranteed by the current constraints. Fix this by adding the trait that provides the missing behavior. Use trait composition to extend the constraints. Adding constraints restricts which types you accept, but expands what your code can do. Each trait adds guaranteed operations, which lets the compiler check correctness and reason about lifetimes and effects. In practice, generic errors mean your constraints don't include the behavior your code uses. Add the missing trait. #notebox("Note", rgb("#8a94a6"), rgb("#556666"), rgb("#f7f8fa"))[ Constraints make your code sound: every operation you use is guaranteed to exist for any type that satisfies them. ] ==== Generic parameter types In this example, both parameters are Lists that use the same element type: lhs: List\[T\], rhs: List\[T\] Using T for both arguments ensures your loop compares like-with-like and prevents type mismatches. The previous example showed lists containing elements of type T. Generic types can also be used without embedding them into container types, like you see here: def my\_generic\_fn\[T: AnyType\](value: T): This function accepts any type, because its only limit is AnyType. AnyType is the root of the trait hierarchy. It's the baseline that all types conform to. Using AnyType means the value has no guaranteed destructor or lifetime management. Outside of #emph[reflection] (type introspection), this function is effectively useless. === Simplified conformance syntax with Some You can replace explicit type parameters and conformances with concise Some\[Trait(s)\] and SomeTypeList\[Trait(s)\] syntax in your declarations. These forms support both a single trait and trait compositions. This syntax lets you conform types where you use them instead of declaring a type parameter in one place and using it in another place. For example: def my\_generic\_fn\[T: Trait(s)\](value: T): becomes: def my\_generic\_fn(value: Some\[Trait(s)\]): You can use Some with any trait or trait composition, under any argument convention, and in the code where you've been using type parameters. There are no restrictions on whether a conformed type is inferred or explicit. You can use any convention your arguments require. If the compiler can't infer a concrete type, it errors and asks you to use explicit type parameters instead. ==== Arguments You can call \_\_int\_\_() on x with either style. The Some version is shorter and places the trait requirement directly on the argument: \# Before def foo\[T: Intable, //\](x: T) -\> Int: return x.\_\_int\_\_() \# After def foo(x: Some\[Intable\]) -\> Int: return x.\_\_int\_\_() ==== Function types Mojo supports Some with function types. This example moves a closure parameter conformance to an argument Some type: \# Before def sync\_parallelize\[ FuncType: def(Int) -\> None, \](func: FuncType): ... \# After def sync\_parallelize(func: Some\[def(Int) -\> None\]): ... ==== Variadics This example uses \*SomeTypeList to conform a variadic parameter pack instead of a single type parameter: \# Before def show\[\*Ts: Writable\](\*pack: \*Ts): ... \# After def show(\*pack: \*SomeTypeList\[Writable\]): ... ==== Operator overloads Operator overloads are a great use case for Some syntax because they place the trait that enables the operator directly on the declaration. \# Before def \_\_getitem\_\_\[I: Indexer, //\](self, idx: I) -\> ref\[self.x\] Self.T: ... \# After def \_\_getitem\_\_(self, idx: Some\[Indexer\]) -\> ref\[self.x\] Self.T: ... ==== Where Some won't work Some cannot replace type parameters in all cases. In the following example, Mojo cannot infer a concrete type from the context and errors: \@fieldwise\_init struct Struct(Writable): var x: Some\[Copyable & ImplicitlyDeletable & Writable\] \# comptime\[T: Copyable & ImplicitlyDeletable & Writable\] T' is not \# a concrete type, use '\[\]' to bind missing parameters def main(): var s = Struct(1) print(s) Move the conformances back to a generic type parameter to fix this: \@fieldwise\_init struct Struct\[T: Copyable & ImplicitlyDeletable & Writable\](Writable): var x: Self.T def main(): var s = Struct(1) print(s) \# Struct\[Int\](x=1) Struct\[Int\](x=1) === Generic types So far, you've seen generics applied to methods and functions. Mojo also lets you define generic types. These use compile-time parameters to define both their fields and their methods. Generic types let you package a reusable #emph[shape]: stored fields plus supported operations. Instead of coding PairInt, PairString, and so on, you write one Pair\[T\] and let the compiler generate specialized versions for each concrete T you use. comptime ComparableValue = Equatable & ImplicitlyCopyable & ImplicitlyDeletable \@fieldwise\_init struct Pair\[T: ComparableValue\](ComparableValue): var left: Self.T var right: Self.T def \_\_eq\_\_(self, other: Pair\[Self.T\]) -\> Bool: return self.left == other.left and self.right == other.right Like generic functions, generic types use placeholder parameters. The difference is that a generic type uses those parameters in its storage as well as in its methods. Here, Pair stores two values of the same element type and implements equality by comparing its fields. Traits are what make generic types practical. Those conformances describe the requirements the type definition depends on. In this example, Pair needs to compare values, so T must be equatable. It also needs to assign and copy values in common operations, so it applies a trait composition conformance on T: comptime ComparableValue = Equatable & ImplicitlyCopyable & ImplicitlyDeletable The Pair definition applies this conformance in two places: to the type itself (in parentheses) and to the type parameter (in square brackets), which is used to define its fields. struct Pair\[T: ComparableValue\](ComparableValue): - For the parentheses after the type, you can say the struct "conforms to" or "implements" this constraint. This is a promise. The Pair type is saying, "I am a ComparableValue." - For the square brackets, the conformance restricts which values callers can pass. This is a requirement: "Any value used with Pair must have type T, and T must be a ComparableValue." This makes Pair\[T\] usable anywhere a ComparableValue is required (because of the parentheses) and ensures all T values conform (because of the square brackets). So far, these examples use type parameters. Generic types can also take non-type parameters, which you'll see next. === Mixing type and value parameters Generics don't limit what parameters you can use. Add whatever you need to define the behavior. struct ExampleStruct: def example\[ T: Writable & Copyable, \# type parameter count: Int, \# value parameter \]( self, data: String, \# argument init\_value: T \# generic argument ) -\> String: By convention, Mojo uses lower\_snake\_case for compile-time value parameters. This visually distinguishes them from type parameters. === Downcasting safely: conforms\_to + trait\_downcast() Sometimes a generic conformance is broader than what you need. You may want operations from a specific trait that the conformance doesn't guarantee. Use a guarded downcast with conforms\_to() and trait\_downcast(). Downcast only when the underlying type implements the target trait. Check conformance first. Then downcast and use the value through that trait. This pattern keeps behavior explicit and safe: def process\[T: AnyType\](value: T): comptime if conforms\_to( T, Writable & ImplicitlyCopyable & ImplicitlyDeletable ): var w = trait\_downcast\[ Writable & ImplicitlyCopyable & ImplicitlyDeletable \](value) print(w) else: print("\") \# Or fail at compile time with \`comptime assert\` Key points: - conforms\_to(T, Writable & ImplicitlyCopyable & ImplicitlyDeletable) checks whether T satisfies the trait composition. - trait\_downcast() rebinds the value to a trait-constrained view at compile time. Only call trait\_downcast() inside the guarded branch. Handle the fallback in the else branch. If you always need the trait, add it as a constraint (for example, T: Writable) instead of downcasting. Use this pattern when you can't express the requirement as a constraint. It's common in reflection-style code that handles values based on their traits. See reflection. === Type generics and explicit destruction Explicitly destroyed types don't always work with generic code. The issue isn't generics, it's lifetime management. Explicit destruction gives you control over teardown. You can define destructors that take arguments, follow different paths, or raise errors. Generic code that copies or moves values can't see or honor that logic. Once a value is copied or transferred, you lose control over how (or whether) it's cleaned up, and the code won't compile. Watch for these cases: - Generic code that manages lifetimes - Generic code that copies or transfers values These show up most often in containers, collections, and iterators. They copy values or take ownership and decide when destruction happens. Safe cases: - Generic operations that don't affect lifetimes Think comparisons, predicates, and other pure operations. Rule of thumb: if your generic code needs to own, copy, or control when a value dies, avoid explicitly destroyed types. Add an ImplicitlyDeletable constraint to keep things working. === Value generics Value generics parameterize code over compile-time constants instead of types. You declare them in \[\] alongside type parameters, but they bind to values. ==== When to use value generics Use value generics when a value shapes structure or behavior and is known at compile time. Common cases: - #emph[Fixed sizes:] buffer lengths, array dimensions, matrix shapes - #emph[Thresholds and limits:] capacity caps, retry counts, precision levels - #emph[Feature selection:] algorithm variants, debug flags, mode switches - #emph[Numeric configuration:] SIMD widths, stride lengths, unroll factors The compiler specializes code for each distinct value. It can remove dead branches (comptime if), unroll loops (comptime for), replace inline constants, and optimize aggressively with no runtime cost. ==== Basic example This function creates a fixed-size list initialized with a default value: comptime MyCollectionElement = ImplicitlyCopyable & ImplicitlyDeletable def make\_filled\[T: MyCollectionElement, size: Int\]( splat\_value: T ) -\> List\[T\]: var result = List\[T\](capacity=size) for \_ in range(size): result.append(splat\_value) return result^ The size parameter resolves at compile time. Each call site with a different value gets its own specialized version: var three\_zeros = make\_filled\[Int, 3\](0) var five\_hellos = make\_filled\[String, 5\]("hello") print(three\_zeros) \# \[0, 0, 0\] print(five\_hellos) \# \[hello, hello, hello, hello, hello\] ==== Value parameters vs runtime arguments Put values known at compile time in \[\]. Put values only known at runtime in . Ask yourself: does the caller know this value when writing the code? If yes, use a parameter. If it depends on input, files, or runtime state, use an argument. \# size is compile-time: the compiler specializes def fixed\[size: Int\](): var buf = InlineArray\[Int, size\](fill=0) \# size is runtime: no specialization def dynamic(size: Int): var buf = List\[Int\](capacity=size) === When generics get more complex The examples on this page cover the most common cases. As you build larger APIs, you'll run into a few additional patterns: - #emph[Associated (dependent) types:] Some traits declare associated types. For example, collection and iteration traits use an associated element type (often Element) to describe the values they own. Associated types can also constrain behavior—for example, requiring elements to be hashable or implicitly deletable. - #emph[Generic methods in non-generic types:] A type doesn't have to be generic to use generics. Individual methods can declare their own type parameters when only part of the API needs them. - #emph[Multiple type parameters:] Some algorithms use more than one type parameter. Traits describe how those types relate, not just what each can do. Common examples include dictionaries (keys and values), caches (keys and values), result types (values and errors), and algorithms that operate across multiple types. - #emph[Compile-time computation:] Value parameters enable comptime if, comptime for, and compile-time arithmetic. This shows up in SIMD code, fixed-size containers, and performance-critical loops where the compiler generates different code paths for different values. These topics build on the same core ideas: type parameters, constraints, and specialization. You can apply constraints to functions, methods, types, or comptime aliases to keep requirements close to where they matter. You won't need these patterns for simple cases, but they matter as generic code grows. === Conditional trait conformance #notebox("Note", rgb("#8a94a6"), rgb("#556666"), rgb("#f7f8fa"))[ Conditional trait conformance is new and evolving. Certain capabilities may be unstable or unusable during roll-out. Use caution with Copyable, Movable, RegisterPassable, TrivialRegisterPassable, and ImplicitlyDeletable. ] #emph[Conditional trait conformance] uses checks before allowing a type to adopt a trait. If the condition is #emph[satisfied], the type conforms. It must fulfill the trait's requirements by providing required methods and associated types, and it gains any default implementation provided by the trait. The type can then be used anywhere the trait is required, such as when passing it to a function that expects a conforming type. When the condition #emph[isn't] satisfied, Mojo skips the conformance. You can't use the fully concrete type in contexts that require the trait. ==== Example: derived conformance In the following declaration, Mojo conforms Wrapper to Writable when its parameter, T, is also Writable: comptime BaseTraits = Copyable & ImplicitlyDeletable \@fieldwise\_init struct Wrapper\[T: BaseTraits\]( Writable where conforms\_to(T, Writable) ): var value: Self.T When conforming to Writable, Wrapper doesn't need to implement any methods to enable printing. The trait provides a default implementation of the write\_to() method. Now consider a type that isn't Writable, that you might add into a Wrapper: \@fieldwise\_init struct NotWritable(BaseTraits): var data: Int When instantiated with an Int or String (both Writable), the Wrapper gains Writable conformance. With NotWritable, you can build a struct, but you can't print it: var w\_int = Wrapper\[Int\](42) \# Int is Writable print(w\_int) \# Wrapper\[Int\](value=42) var w\_str = Wrapper\[String\]("Hello") \# String is Writable print(w\_str) \# Wrapper\[String\](value=Hello) \# OK: only \`Writable\` conformance is unavailable var w\_not\_writable = Wrapper\[NotWritable\](NotWritable(10)) \# print(w\_not\_writable) \# Compile-time error: \# invalid call to 'print': could not convert element of 'values' with \# type 'Wrapper\[NotWritable\]' to expected type 'Writable' Since NotWritable doesn't conform to Writable, the conditional conformance check fails and Wrapper\[NotWritable\] doesn't adopt the Writable conformance. In contrast, Int and String are Writable and their wrappers gain the conformance. This allows their instances to print by using the write\_to() method provided by Writable. This pattern is standard for single-type containers like Optional\[T\], Box\[T\], Lazy\[T\], and List\[T\]. It says: "This type can do X if its inner type can do X." ==== Example: parts conformance Conditional conformance has another standard pattern for types with multiple distinct components: Result\[T, E\], Pair\[L, R\], Dict\[K, V\], and similar. It says: "This type can do X if each of its parts can do X": comptime BaseTraits = Copyable & ImplicitlyDeletable \@fieldwise\_init struct Pair\[L: BaseTraits, R: BaseTraits\]( Hashable where conforms\_to(L, Hashable) and conforms\_to(R, Hashable) ): var left: Self.L var right: Self.R \@fieldwise\_init struct NotHashable(BaseTraits): var data: Int In this example, Pair uses two distinct type parameters. When both are Hashable, the concrete Pair type becomes Hashable, gaining the hash() method from the trait: var pair = Pair\[Int, String\](left=1, right="one") var hash = hash(pair) print(hash) \# Prints the hash of the pair \# OK: only hashing is unavailable var pair2 = Pair\[Int, NotHashable\](left=1, right=NotHashable(10)) \# Constructible \# var hash2 = hash(pair2) \# Compile time error ==== Example: conditional method access When a type adopts a trait, it must satisfy the trait's required methods. The same conditions that determine whether a type can conditionally conform to a trait are usually the same conditions used to #emph[gate] required method implementations. You express these method tests with where clauses. If true, the method is available. If not, the method can't be used for that specialized type. The following example walks you through this process and how it works. You can use Boolable instances in if statements. You make your Wrapper testable by conforming to the Boolable trait. This trait requires a new method, \_\_bool\_\_(): \@fieldwise\_init struct Wrapper\[T: BaseTraits\]( Writable where conforms\_to(T, Writable), Boolable where conforms\_to(T, Boolable), \# New conditional conformance ): var value: Self.T \# New method def \_\_bool\_\_(self) -\> Bool where conforms\_to(Self.T, Boolable): return trait\_downcast\[Boolable\](self.value).\_\_bool\_\_() In this example, the condition on \_\_bool\_\_() is the same as the one used for the Boolable conditional conformance. The where clause gates the method. If the condition is true, the method is available. If not, it isn't. Since the method and the conformance use the same condition, they stay aligned. You won't end up in a situation where the wrapped type conforms but the method isn't available, or where the method exists without the corresponding conformance. You see this in the following examples: var w\_str = Wrapper\[String\]("Hello") if w\_str: \# Chooses the non-empty branch print(t"Non-empty string \\"{w\_str.value}\\" is truthy") else: print(t"Empty string \\"{w\_str.value}\\" is falsy") var w\_empty\_str = Wrapper\[String\]("") if w\_empty\_str: \# Chooses the empty branch print(t"Non-empty string \\"{w\_empty\_str.value}\\" is truthy") else: print(t"Empty string \\"{w\_empty\_str.value}\\" is falsy") The NotWritable type from earlier in this section has no traits beyond the base Copyable and ImplicitlyDeletable. Therefore it isn't Boolable and the condition on the \_\_bool\_\_() method will fail: \@fieldwise\_init struct NotWritable(BaseTraits): var data: Int var w\_not\_writable = Wrapper\[NotWritable\](NotWritable(10)) \# The instance can't be used as a boolean value due to the \# conditional method access for \`\_\_bool\_\_()\`. \# Compiler error: the method condition is false if w\_not\_writable: print(t"NotWritable with data {w\_not\_writable.value.data} is truthy") else: print(t"NotWritable with data {w\_not\_writable.value.data} is falsy") This condition on your method isn't conditional trait conformance. It's conditional method #emph[access]. In this example, the method and the conformance use the same condition, so they stay aligned. That said, where clauses on methods and functions are useful even when you're not working with trait conformance. For example, you might gate a method so it only works with non-empty lists, real numbers, or with values that fall within the index bounds of a collection. ==== Conditional trait composition Mojo supports flexible condition composition, as shown in these examples: - Unconditional: No special clauses. struct Foo(Copyable, ImplicitlyDeletable): - Simple condition: as shown in Wrapper. struct Wrapper\[T: BaseTraits\]( Writable where conforms\_to(T, Writable) ): - Hybrid: Mixes unconditional and conditional traits. struct Foo\[T: AnyType\]( Copyable, Writable where conforms\_to(T, Writable) ) - Multiple aligned conditions: as shown in Pair. struct Pair\[L: BaseTraits, R: BaseTraits\]( Hashable where conforms\_to(L, Hashable) and conforms\_to(R, Hashable) ): - Multiple independent conditions: struct Foo\[T: AnyType\]( Writable where conforms\_to(T, Writable), Hashable where conforms\_to(T, Hashable), ) ==== Conditional conformance with value parameters Conditional conformance also works with value parameters. You can gate both conformance and methods on compile-time value conditions. This is useful when a type's capabilities depend on a numeric parameter. For example, a fixed-capacity wrapper might only be Writable when it has capacity and its elements are writable. The Sized conformance is unconditional. struct SizedListWrapper\[capacity: Int, T: Copyable & ImplicitlyDestructible\]( Sized, Writable where conforms\_to(T, Writable) and capacity \> 0 ): var data: List\[Self.T\] def \_\_init\_\_(out self, value: Self.T): self.data = List\[Self.T\](capacity=Self.capacity) for \_ in range(Self.capacity): self.data.append(value.copy()) def \_\_len\_\_(self) -\> Int: return len(self.data) def write\_to(self, mut writer: Some\[Writer\]): writer.write(repr(self.data)) You can gate methods on value conditions: def first(self) -\> Self.T where Self.capacity \> 0: return self.data\[0\].copy() When capacity is one or more, the type conforms to Writable and the first() method is available. When it is zero or less, neither is usable: var s = SizedListWrapper\[5, Int\](42) print(s) \# List\[Int\](\[Int(42), Int(42), Int(42), Int(42), Int(42)\]) print(s.first()) \# 42 \# var s = SizedListWrapper\[0, Int\](42) \# print(s) \# Error: Writable not satisfied \# print(s.first()) \# Error: constraint is false Value conditions follow the same rules as type conditions. You can combine them with and and mix them with conforms\_to checks in the same where clause.