15. Metaprogramming
Metaprogramming blurs the line between code and data by allowing programs to treat other programs or themselves as data. This enables the creation of more dynamic and flexible code that can adapt to different situations at runtime or compile-time. Metaprogramming techniques can be used to generate code, modify existing code, or create domain-specific languages (DSLs) that are tailored to specific problem domains.
Mojo supports serveral metaprogramming features, some of which are shown below. Mojo metaprogramming features are primarily focused on compile-time reflection and therefore may result in longer compile times.
15.1. type_of
The type_of function in Mojo is a built-in function that returns the type of a given expression or variable at compile-time. This can be useful for debugging, type checking, and implementing generic programming patterns.
fn multiply(first: Scalar, second: type_of(first)) -> type_of(first):
return first * second
Usage:
var a: Int16 = 5
var b: Int16 = 10
var result_i: Int16 = multiply(a, b)
print("Result Int:", a, "times", b, "is", result_i) # Output: Result Int: 5 times 10 is 50
var x: Float16 = 3.5
var y: Float16 = 2.0
var result_f: Float16 = multiply(x, y)
print("Result Float", x, "times", y, "is", result_f) # Output: Result Float 3.5 times 2.0 is 7.0
# var z: Float32 = 4.0
# print("Result Mixed: ", x, "times", z, "is", multiply(x, z)) # Uncommenting this will cause a type error due to mismatched types
The type_of function also takes expressions as arguments:
var m: type_of(3*2.5) # Type of m is FloatLiteral[7.5]
m = 7.5
print("Value is: ", m)
#m = 7.1 # Uncommenting this will cause compile-time error due to type mismatch
In the above example, we use type_of on a multiplcation expression that evaluates to a result at compile-time. This result is part of the type of the variable. So when we try to assign a different value other than the result of the previous expression, it results in a type error.
15.2. conforms_to
The conforms_to function in Mojo is a built-in function that checks if a given type conforms to a specified trait or type constraint. This can be useful for ensuring that types meet certain requirements or for implementing generic programming patterns.
trait Animal: ...
trait CanFly: ...
struct Eagle(Animal, CanFly):
fn __init__(out self): ...
struct Monkey(Animal):
fn __init__(out self): ...
fn ability[T: Animal](x: T):
@parameter
if conforms_to(T, CanFly):
print("It can fly!")
else:
print("It cannot fly!")
Usage:
ability(Eagle()) # Prints: It can fly!
ability(Monkey()) # Prints: It cannot fly!
In the above example, we define a trait CanFly and implement it for the Eagle struct. The ability function then checks if the type T conforms to the CanFly trait using the conforms_to function. Depending on whether the type conforms to the trait, it prints whether the animal can fly or not.
conforms_to can be particularly useful when creating libraries and frameworks, as it allows developers to enforce certain type constraints and ensure that their code behaves correctly with different types, while still maintaining flexibility and reusability.
15.3. trait_downcast
The trait_downcast function in Mojo is a built-in function that allows you to downcast a value of a trait type to a concrete type that implements that trait. This can be useful when you have a value of a trait type and you need to access methods or properties specific to the concrete type. It can be used in combination with conforms_to to safely downcast only when the type conforms to the desired trait.
trait Animal: ...
trait CanFly:
fn fly(self):
print("Flying high!")
struct Eagle(Animal, CanFly):
fn __init__(out self): ...
struct Monkey(Animal):
fn __init__(out self): ...
fn ability[T: Animal](x: T):
@parameter
if conforms_to(T, CanFly):
trait_downcast[CanFly](x).fly()
else:
print("It cannot fly!")
Usage:
ability(Eagle()) # Prints: It can fly!
ability(Monkey()) # Prints: It cannot fly!
In the above code listing, we define a trait CanFly and implement it for the Eagle struct. The ability function checks if the type T conforms to the CanFly trait using the conforms_to function. If it does, it uses trait_downcast to downcast the value to the CanFly trait type and calls the fly method. This allows us to access the specific behavior of the Eagle struct when we know it conforms to the CanFly trait.
15.4. size_of
The size_of function in Mojo is a built-in function that returns the size, in bytes, of a given type at compile-time. This is useful for tasks such as memory management, serialization, interfacing with hardware, among others.
Usage:
from sys.info import size_of
fn main():
print("Int16 is ", size_of[Int16](), " bytes") # Prints 2 bytes
print("Float64 is ", size_of[Float64](), " bytes") # Prints 8 bytes
print("Bool is ", size_of[Bool](), " bytes") # Prints 1 byte
15.5. get_type_name
The get_type_name function in Mojo is a built-in function that returns the name of a given type at compile-time. In addition to being useful for debugging and logging purposes, it can also be used in metaprogramming scenarios where type information is needed.
# tag::struct_decl[]
struct Person:
var name: String
var age: Int
# end::struct_decl[]
fn main():
# tag::get_type_name[]
from reflection import get_type_name
fn print_type_name[T: AnyType]():
print("The type of the value is:", get_type_name[T]())
print_type_name[Int]() # Prints: The type of the value is: Int
print_type_name[String]() # Prints: The type of the value is: String
# end::get_type_name[]
# tag::struct_use[]
from reflection import (struct_field_count,
struct_field_names, struct_field_types)
fn print_struct[T: AnyType]():
comptime count = struct_field_count[T]()
comptime names = struct_field_names[T]()
comptime types = struct_field_types[T]()
@parameter
for idx in range(count):
comptime field_name = names[idx]
comptime field_type = types[idx]
print("Field ", idx, ": ", field_name, " of type ", get_type_name[field_type]())
print_struct[Person]()
# end::struct_use[]
15.6. Struct Reflection Functions
Mojo provides several reflection functions that allow you to inspect the fields of a struct at compile-time. These functions include:
-
struct_field_count[T](): Returns the number of fields in the struct typeT. -
struct_field_names[T](): Returns a list of the names of the fields in the struct typeT. -
struct_field_types[T](): Returns a list of the types of the fields in the struct typeT.
# tag::struct_decl[]
struct Person:
var name: String
var age: Int
# end::struct_decl[]
fn main():
# tag::get_type_name[]
from reflection import get_type_name
fn print_type_name[T: AnyType]():
print("The type of the value is:", get_type_name[T]())
print_type_name[Int]() # Prints: The type of the value is: Int
print_type_name[String]() # Prints: The type of the value is: String
# end::get_type_name[]
# tag::struct_use[]
from reflection import (struct_field_count,
struct_field_names, struct_field_types)
fn print_struct[T: AnyType]():
comptime count = struct_field_count[T]()
comptime names = struct_field_names[T]()
comptime types = struct_field_types[T]()
@parameter
for idx in range(count):
comptime field_name = names[idx]
comptime field_type = types[idx]
print("Field ", idx, ": ", field_name, " of type ", get_type_name[field_type]())
print_struct[Person]()
# end::struct_use[]
Usage:
# tag::struct_decl[]
struct Person:
var name: String
var age: Int
# end::struct_decl[]
fn main():
# tag::get_type_name[]
from reflection import get_type_name
fn print_type_name[T: AnyType]():
print("The type of the value is:", get_type_name[T]())
print_type_name[Int]() # Prints: The type of the value is: Int
print_type_name[String]() # Prints: The type of the value is: String
# end::get_type_name[]
# tag::struct_use[]
from reflection import (struct_field_count,
struct_field_names, struct_field_types)
fn print_struct[T: AnyType]():
comptime count = struct_field_count[T]()
comptime names = struct_field_names[T]()
comptime types = struct_field_types[T]()
@parameter
for idx in range(count):
comptime field_name = names[idx]
comptime field_type = types[idx]
print("Field ", idx, ": ", field_name, " of type ", get_type_name[field_type]())
print_struct[Person]()
# end::struct_use[]
In the above example, we define a struct Person with two fields: name and age. The print_struct function uses the reflection functions to get the number of fields, their names, and their types at compile-time. It then prints this information to the console. All these happen at compile-time, unlike many other programming languages where such reflection is typically done at runtime. Compiletime reflection leads to more efficient code as it avoids the overhead of runtime reflection.