11. Operators and special methods
In Mojo some functions have special naming convention, starting and ending with double underscores: "_". Since they start and end with __double underscores_, they are called dunder methods. These methods are treated specially by Mojo compiler. Mojo supports implementation of arithmetic and relational operators, along with special methods that support lifecycle of values.
In previous chapters, we saw many times the arithmetic and relational operators. Most of these operators can be implemented by user defined structs. Mojo provides quite a bit of flexibility in defining your own low level structs and the ability to implement these operators is part of that flexibility. Implementing an operator is as simple as implementing a function.
Many of Mojo’s built in functionality is implemented as libraries. This means that we are able to implement powerful constructs just using basic language features offered by Mojo.
11.1. Arithmetic operators
The following sections describe the various arithmetic operators in Mojo.
11.1.1. Addition
The following are the addition operators.
__add__
The __add__
stands for arithmetic addition "+" between the struct defining the method and self type or another type.
__radd__
The __radd__
method is known as reverse addition, and is used when we try to add two values, where the first value does not have __add__
implemented. In this case, the Mojo compiler checks if the second value has __radd__
implemented, and it calls that one.
__iadd__
The __iadd__
method is called in-place addition and represents arithmetic addition "=". Even if you implement just `\\__add__` and do not implement `\\__iadd__`, the "=" operation would still work as Mojo will just use __add__
as the fallback. However, typically __add__
returns a new instance of the result. In case of large structs (structs with many fields), it could entail a lot of copy operations. The in-place addition can directly change the struct’s internal data, resulting in an efficient execution of the addition method. The __iadd__
therefore does not have a return value as it updates the struct itself.
Since the struct' internal value is mutated, we need to use one of the inout
or owned
references of self
in the method.
The following code listing shows the different operations. Please note that these examples are not meant for production use, as it is intentionally kept incomplete for simplicity’s sake.
struct MyFloat:
var val: Float64
fn __init__(inout self, value: Float64):
self.val = value
struct MyInt:
var val: Int
fn __init__(inout self, value: Int):
self.val = value
fn __add__(self, other: Self) -> Self:
print("add invoked")
return Self(self.val + other.val)
fn __radd__(self, other: MyFloat) -> Self:
print("radd invoked")
return Self(self.val + int(other.val))
fn __iadd__(inout self, other: Self):
print("iadd invoked")
self.val = self.val + other.val
Usage:
var num: MyInt = MyInt(42)
var add_res = MyInt(1) + MyInt(2)
print(add_res.val)
var radd_res = MyFloat(3.5) + MyInt(2) # Even though MyFloat does not implement __add__ method, we are able to do addition through MyInt's __radd__
print(radd_res.val)
var iadd_res = MyInt(10)
iadd_res += MyInt(20)
print(iadd_res.val)
11.1.2. Subtraction
The following are the subtraction operators.
__sub__
The __sub__
stands for arithmetic subtraction "-" between the struct defining the method and self type or another type.
__rsub__
The __rsub__
method is known as reverse subtraction, and is used when we try to subtract two values, where the first value does not have __sub__
implemented. In this case, the Mojo compiler checks if the second value has __rsub__
implemented, and it calls that one.
As the name implies, the reverse subtraction swaps the operands. Since subtraction is non-commutative, care must be taken to have correct values as the first operand and second operands. For example, x - y
in normal subtraction would be y - x
in reverse subtraction.
__isub__
The __isub__
method is called in-place subtraction and represents arithmetic subtraction "-=". The concept of __isub__
is the same as what we saw in __iadd__
.
struct MyFloat:
var val: Float64
fn __init__(inout self, value: Float64):
self.val = value
struct MyInt:
var val: Int
fn __init__(inout self, value: Int):
self.val = value
fn __sub__(self, other: Self) -> Self:
print("sub invoked")
return Self(self.val - other.val)
fn __rsub__(self, other: MyFloat) -> Self:
print("rsub invoked")
return Self(int(other.val) - self.val) # Order matters for subtraction; it is not commutative.
fn __isub__(inout self, other: Self):
print("isub invoked")
self.val = self.val - other.val
Usage:
var num: MyInt = MyInt(42)
var sub_res = MyInt(1) - MyInt(2)
print(sub_res.val)
var rsub_res = MyFloat(3.5) - MyInt(2) # Even though MyFloat does not implement __sub__ method, we are able to do addition through MyInt's __rsub__
print(rsub_res.val)
var isub_res = MyInt(10)
isub_res -= MyInt(20)
print(isub_res.val)
11.1.3. Multiplication
The following are multiplication operators.
__mul__
The __mul__
stands for multiplication "*" between the struct defining the method and self type or another type.
__rmul__
The __rmul__
method is known as reverse multiplication, and is used when we try to multiply two values, where the first value does not have __mul__
implemented. In this case, the Mojo compiler checks if the second value has __rmul__
implemented, and it calls that one.
__imul__
The __imul__
method is called in-place multiplication and represents multiplication "*=". The concept of __imul__
is the same as what we saw in __iadd__
, except instead of addition, the applied operation is multiplication.
struct MyFloat:
var val: Float64
fn __init__(inout self, value: Float64):
self.val = value
struct MyInt:
var val: Int
fn __init__(inout self, value: Int):
self.val = value
fn __mul__(self, other: Self) -> Self:
print("mul invoked")
return Self(self.val * other.val)
fn __rmul__(self, other: MyFloat) -> Self:
print("rmul invoked")
return Self(int(other.val) * self.val) # Will truncate
fn __imul__(inout self, other: Self):
print("imul invoked")
self.val = self.val * other.val
Usage:
var num: MyInt = MyInt(42)
var mul_res = MyInt(3) * MyInt(2)
print(mul_res.val)
var rmul_res = MyFloat(3.5) * MyInt(2)
print(rmul_res.val)
var imul_res = MyInt(10)
imul_res *= MyInt(20)
print(imul_res.val)
11.1.4. Matrix multiplication
The following are operators for matrix multiplication.
__matmul__
The __matmul__
stands for matrix multiplication represented by the symbol "@" between the struct defining the method and self type or another type.
__rmatmul__
The __rmatmul__
stands for the reverse matrix multiplication represented by the symbol "@" between the struct defining the method and self type or another type. Similar to __rsub__
, take care to use the appropriate order of the operands when doing the implementation, as the operands are swapped.
__imatmul__
The __imatmul__
stands for in-place matrix multiplication represented by the symbol "@" between the struct defining the method and self type or another type. The concept of __imatmul__
is the same as what we saw in __iadd__
, except instead of addition, the applied operation is matrix multiplication.
struct MyFloatMatrix:
var val: List[List[Float16]]
var rows: Int
var cols: Int
fn __init__(inout self, value: List[List[Float16]], rows: Int, cols: Int):
self.val = value
self.rows = rows
self.cols = cols
@staticmethod
fn empty() -> Self:
return MyFloatMatrix(List[List[Float16]](), 0, 0)
struct MyIntMatrix:
var val: List[List[Int]]
var rows: Int
var cols: Int
fn __init__(inout self, value: List[List[Int]], rows: Int, cols: Int):
self.val = value
self.rows = rows
self.cols = cols
@staticmethod
fn empty() -> Self:
return MyIntMatrix(List[List[Int]](), 0, 0)
fn print(self):
print("....")
print("Rows:", self.rows, "Cols:", self.cols)
for row in self.val:
print()
for col in row[]:
print(col[], end=" ")
print()
print("----")
@staticmethod
fn _matmul_internal(first: Self, second: Self) -> Self:
if first.cols != first.rows:
print("Rows and columns do not match. ")
return MyIntMatrix.empty()
var res: List[List[Int]] = List[List[Int]](capacity=first.rows)
for i in range(first.rows):
res.append(List[Int](capacity=second.cols))
for j in range(second.cols):
var s = 0
for k in range(first.cols):
s += first.val[i][k] * second.val[k][j]
res[i].append(s)
return Self(res, first.rows, second.cols)
fn __matmul__(self, other: Self) -> Self: # Naive implementation - not for production use
print("matmul invoked")
return Self._matmul_internal(self, other)
fn __rmatmul__(self, other: MyFloatMatrix) -> Self: # Naive implementation - not for production use
print("rmatmul invoked")
var res: List[List[Int]] = List[List[Int]](capacity=self.rows)
for i in range(other.rows):
res.append(List[Int](capacity=self.cols))
for j in range(self.cols):
var s = 0
for k in range(other.cols):
s += int(other.val[i][k]) * self.val[k][j] # Will truncate
res[i].append(s)
return Self(res, other.rows, self.cols)
fn __imatmul__(inout self, other: Self): # Naive implementation - not for production use
print("imatmul invoked")
var res = Self._matmul_internal(self, other)
self.val = res.val
self.rows = res.rows
self.cols = res.cols
Usage:
var m : List[List[Int]] = List(
List(1, 2, 1),
List(5, 1, 1),
List(2, 3, 1))
var n: List[List[Int]] = List(
List(2, 5),
List(6, 7),
List(1, 1))
var flm : List[List[Float16]] = List(
List[Float16](1.2, 2.3, 1.4),
List[Float16](5.2, 1.2, 1.3),
List[Float16](2.3, 3.4, 1.4))
var matmul_res = MyIntMatrix(m, 3, 3) @ MyIntMatrix(n, 3, 2)
matmul_res.print()
var rmatmul_res = MyFloatMatrix(flm, 3, 3) @ MyIntMatrix(n, 3, 2)
rmatmul_res.print()
var imatmul_res = MyIntMatrix(m, 3, 3)
imatmul_res @= MyIntMatrix(n, 3, 2)
imatmul_res.print()
11.1.5. Division
The division operators.
__truediv__
The __truediv__
stands for division represented by the symbol "/" between the struct defining the method and self type or another type. The result of \\__truediv
is a floating point type with 64 bits.
__rtruediv__
The __rtruediv__
stands for the reverse division represented by the symbol "/" between the struct defining the method and self type or another type. Similar to __rsub__
, take care to use the appropriate order of the operands when doing the implementation, as the operands are swapped.
__itruediv__
The __itruediv__
stands for in-place division represented by the symbol "/" between the struct defining the method and self type or another type. The concept of __itruediv__
is the same as what we saw in __iadd__
, except instead of addition, the applied operation is division.
struct MyFloat:
var val: Float64
fn __init__(inout self, value: Float64):
self.val = value
struct MyInt:
var val: Int
fn __init__(inout self, value: Int):
self.val = value
fn __truediv__(self, other: Self) -> MyFloat:
print("truediv invoked")
return MyFloat(self.val / other.val)
fn __rtruediv__(self, other: MyFloat) -> MyFloat:
print("rtruediv invoked")
return MyFloat(int(other.val) / self.val)
fn __itruediv__(inout self, other: Self):
print("itruediv invoked")
self.val = int(self.val / other.val) # Will truncate
Usage:
var num: MyInt = MyInt(42)
var div_res = MyInt(3) / MyInt(2)
print(div_res.val)
var rdiv_res = MyFloat(3.5) / MyInt(2)
print(rdiv_res.val)
var idiv_res = MyInt(10)
idiv_res /= MyInt(20)
print(idiv_res.val)
Note that __itruediv__
implementation had to convert the result from a Float
to Int
because MyInt
can only store Int
as value within it. Mojo has the ability to have a variable with multiple possible types through Variant
struct. We will cover that in a later chapter.
__floordiv__
The __floordiv__
stands for floor division (also known as integer division) represented by the symbol "//" between the struct defining the method and self type or another type. As the name suggest, the result of the __floordiv__
is an integer instead of float.
Typically implementations truncate towards zero in case of positive values and away from zero for negative values. For example, 7//3
results in 2
, while -7//3
results in -3
and not -2
.
__rfloordiv__
The __rfloordiv__
stands for the reverse division represented by the symbol "//" between the struct defining the method and self type or another type. Similar to __rtruediv__
, take care to use the appropriate order of the operands when doing the implementation, as the operands are swapped.
__ifloordiv__
The __ifloordiv__
stands for in-place division represented by the symbol "//" between the struct defining the method and self type or another type. The concept of __ifloordiv__
is the same as what we saw in __iadd__
, except instead of addition, the applied operation is floor division.
struct MyFloat:
var val: Float64
fn __init__(inout self, value: Float64):
self.val = value
struct MyInt:
var val: Int
fn __init__(inout self, value: Int):
self.val = value
fn __floordiv__(self, other: Self) -> Self:
print("floordiv invoked")
return Self(self.val // other.val)
fn __rfloordiv__(self, other: MyFloat) -> Self:
print("rfloordiv invoked")
return Self(int(other.val) // self.val)
fn __ifloordiv__(inout self, other: Self):
print("ifloordiv invoked")
self.val = self.val // other.val
Usage:
var num: MyInt = MyInt(42)
var floordiv_res = MyInt(3) // MyInt(2)
print(floordiv_res.val)
var rfloordiv_res = MyFloat(3.5) // MyInt(2)
print(rfloordiv_res.val)
var ifloordiv_res = MyInt(10)
ifloordiv_res //= MyInt(20)
print(ifloordiv_res.val)
11.1.6. Modulo
The following are the modulo operators.
__mod__
The __mod__
stands for modulo operation represented by the symbol "%" between the struct defining the method and self type or another type. The approach to implement __mod__
is the same as what we saw in __truediv__
, except instead of division, the applied operation is modulo operation.
The __mod__
derives its name from the mathematical modulo operation. Modulo operation divides two numbers and returns the remainder of the division.
__rmod__
The __rmod__
stands for the reverse modulo operation represented by the symbol "%" between the struct defining the method and self type or another type. Similar to __rtruediv__
, take care to use the appropriate order of the operands when doing the implementation, as the operands are swapped.
__imod__
The __imod__
stands for in-place modulo operation represented by the symbol "%" between the struct defining the method and self type or another type. The concept of __imod__
is the same as what we saw in __itruediv__
, except instead of division, the applied operation is modulo.
struct MyFloat:
var val: Float64
fn __init__(inout self, value: Float64):
self.val = value
struct MyInt:
var val: Int
fn __init__(inout self, value: Int):
self.val = value
fn __mod__(self, other: Self) -> Self:
print("mod invoked")
return Self(self.val % other.val)
fn __rmod__(self, other: MyFloat) -> Self:
print("rmod invoked")
return Self(int(other.val) % self.val)
fn __imod__(inout self, other: Self):
print("imod invoked")
self.val = self.val % other.val
Usage:
var num: MyInt = MyInt(42)
var mod_res = MyInt(7) % MyInt(2)
print(mod_res.val)
var rmod_res = MyFloat(8) % MyInt(2)
print(rmod_res.val)
var imod_res = MyInt(37)
imod_res %= MyInt(20)
print(imod_res.val)
11.1.7. Exponentiation
The following lists the exponentiation operators.
__pow__
The __pow__
stands for exponential operation represented by the symbol "**" between the struct defining the method and self type or another type. The approach to implement __pow__
is the same as what we saw in __mul__
, except instead of multiplication, we apply exponential operation.
__ipow__
The __ipow__
stands for in-place exponential operation represented by the symbol "**" between the struct defining the method and self type or another type. The concept of __ipow__
is the same as what we saw in __imul__
, except instead of multiplication, we apply exponential operation.
struct MyInt:
var val: Int
fn __init__(inout self, value: Int):
self.val = value
fn __pow__(self, other: Self) -> Self:
print("pow invoked")
return Self(self.val ** other.val)
fn __ipow__(inout self, other: Self):
print("ipow invoked")
self.val **= other.val
Usage:
var num: MyInt = MyInt(42)
var pow_res = MyInt(7) ** MyInt(2)
print(pow_res.val)
var ipow_res = MyInt(7)
ipow_res **= MyInt(2)
print(ipow_res.val)
11.1.8. Unary operators
The following are the unary operators.
__neg__
The __neg__
stands for the unary operation represented by the symbol "-" for the struct defining the method. The negative sign appears as prefix to the value and typically results in negation of the value.
struct MyInt:
var val: Int
fn __init__(inout self, value: Int):
self.val = value
fn __neg__(self) -> Self:
print("neg invoked")
return Self(-self.val)
Usage:
var num: MyInt = MyInt(42)
var neg_res = -MyInt(7)
print(neg_res.val)
__pos__
The __pos__
stands for the unary operation represented by the symbol "+" for the struct defining the method. The positive sign appears as prefix to the value.
struct MyInt:
var val: Int
fn __init__(inout self, value: Int):
self.val = value
fn __pos__(self) -> Self:
print("pos invoked")
return Self(+self.val)
Usage:
var num: MyInt = MyInt(42)
var pos_res = +MyInt(7)
print(pos_res.val)
__invert__
The __invert__
stands for the unary operation represented by the symbol "~" for the struct defining the method. The invert sign appears as prefix to the value. Typical implementations return bitwise compliment of the value, switching 1
for 0
and vice versa.
struct MyInt:
var val: Int
fn __init__(inout self, value: Int):
self.val = value
fn __invert__(self) -> Self:
print("invert invoked")
return Self(~self.val)
Usage:
var num: MyInt = MyInt(42)
var invert_res = ~MyInt(2)
print(invert_res.val)
11.2. Bitwise operators
The following are the bitwise operators.
11.2.1. Operators
__lshift__
The __lshift__
stands for left shift operation represented by the symbol "<<" between the struct defining the method and self type or another type. Typical implementation shifts the bits of the first operand to the left. The second operand indicates how many bits are to be shifted to the left.
__rlshift__
The __rlshift__
stands for the reverse left shift operation represented by the symbol "<<" between the struct defining the method and self type or another type. Similar to __rtruediv__
, take care to use the appropriate order of the operands when doing the implementation, as the operands are swapped.
__ilshift__
The __ilshift__
stands for in-place left shift operation represented by the symbol "<⇐" between the struct defining the method and self type or another type. Typical implementation shifts the bits of the first operand to the left. The second operand indicates how many bits are to be shifted to the left. Instead of returning a new instance like in __lshift__
, the __ilshift__
updates its own instance with the result. The concept is similar to __iadd__
mentioned earlier.
struct MyFloat:
var val: Float64
fn __init__(inout self, value: Float64):
self.val = value
struct MyInt:
var val: Int
fn __init__(inout self, value: Int):
self.val = value
fn __lshift__(self, other: Self) -> Self:
print("lshift invoked")
return Self(self.val << other.val)
fn __rlshift__(self, other: MyFloat) -> Self:
print("rlshift invoked")
return Self(int(other.val) << self.val)
fn __ilshift__(inout self, other: Self):
print("ilshift invoked")
self.val <<= other.val
Usage:
var num: MyInt = MyInt(42)
var lshift_res = MyInt(2) << MyInt(3)
print(lshift_res.val)
var rlshift_res = MyFloat(2) << MyInt(3)
print(rlshift_res.val)
var ilshift_res = MyInt(3)
ilshift_res <<= MyInt(2)
print(ilshift_res.val)
__rshift__
The __rshift__
stands for right shift operation represented by the symbol ">>" between the struct defining the method and self type or another type. Typical implementation shifts the bits of the first operand to the right. The second operand indicates how many bits are to be shifted to the right.
__rrshift__
The __rrshift__
stands for the reverse right shift operation represented by the symbol ">>" between the struct defining the method and self type or another type. Similar to __rlshift__
, take care to use the appropriate order of the operands when doing the implementation, as the operands are swapped.
__irshift__
The __irshift__
stands for in-place right shift operation represented by the symbol ">>=" between the struct defining the method and self type or another type. Typical implementation shifts the bits of the first operand to the left. The second operand indicates how many bits are to be shifted to the left. Instead of returning a new instance like in __rshift__
, the __irshift__
updates its own instance with the result. The concept is similar to __iadd__
mentioned earlier.
struct MyFloat:
var val: Float64
fn __init__(inout self, value: Float64):
self.val = value
struct MyInt:
var val: Int
fn __init__(inout self, value: Int):
self.val = value
fn __rshift__(self, other: Self) -> Self:
print("rshift invoked")
return Self(self.val >> other.val)
fn __rrshift__(self, other: MyFloat) -> Self:
print("rrshift invoked")
return Self(int(other.val) >> self.val)
fn __irshift__(inout self, other: Self):
print("irshift invoked")
self.val >>= other.val
Usage:
var num: MyInt = MyInt(42)
var rshift_res = MyInt(20) >> MyInt(3)
print(rshift_res.val)
var rrshift_res = MyFloat(24) >> MyInt(3)
print(rrshift_res.val)
var irshift_res = MyInt(30)
irshift_res >>= MyInt(2)
print(irshift_res.val)
__and__
The __and__
stands for bitwise AND operator represented by the symbol "&" between the struct defining the method and self type or another type.
__rand__
The __rand__
stands for reverse bitwise AND operator represented by the symbol "&" between the struct defining the method and self type or another type. This is invoked when the first value does not have __and__
implemented. In this case, the Mojo compiler checks if the second value has __rand__
implemented, and calls that one.
__iand__
The __iand__
stands for in-place bitwise AND operator represented by the symbol "&=" between the struct defining the method and self type or another type. Instead of returning a new instance like in __and__
, the __iand__
updates its own instance with the result.
struct MyFloat:
var val: Float64
fn __init__(inout self, value: Float64):
self.val = value
struct MyInt:
var val: Int
fn __init__(inout self, value: Int):
self.val = value
fn __and__(self, other: Self) -> Self:
print("and invoked")
return Self(self.val & other.val)
fn __rand__(self, other: MyFloat) -> Self:
print("rand invoked")
return Self(int(other.val) & self.val)
fn __iand__(inout self, other: Self):
print("iand invoked")
self.val &= other.val
Usage:
var num: MyInt = MyInt(42)
var and_res = MyInt(23) & MyInt(6)
print(and_res.val)
var rand_res = MyFloat(20) & MyInt(4)
print(rand_res.val)
var iand_res = MyInt(10)
iand_res &= MyInt(2)
print(iand_res.val)
__or__
The __or__
stands for bitwise OR operator represented by the symbol "|" between the struct defining the method and self type or another type.
__ror__
The __ror__
stands for reverse bitwise OR operator represented by the symbol "|" between the struct defining the method and self type or another type. This is invoked when the first value does not have __or__
implemented. In this case, the Mojo compiler checks if the second value has __ror__
implemented, and calls that one.
__ior__
The __ior__
stands for in-place bitwise OR operator represented by the symbol "|=" between the struct defining the method and self type or another type. Instead of returning a new instance like in __or__
, the __ior__
updates its own instance with the result.
struct MyFloat:
var val: Float64
fn __init__(inout self, value: Float64):
self.val = value
struct MyInt:
var val: Int
fn __init__(inout self, value: Int):
self.val = value
fn __or__(self, other: Self) -> Self:
print("or invoked")
return Self(self.val | other.val)
fn __ror__(self, other: MyFloat) -> Self:
print("ror invoked")
return Self(int(other.val) | self.val)
fn __ior__(inout self, other: Self):
print("ior invoked")
self.val |= other.val
Usage:
var num: MyInt = MyInt(42)
var or_res = MyInt(5) | MyInt(3)
print(or_res.val)
var ror_res = MyFloat(15) | MyInt(17)
print(ror_res.val)
var ior_res = MyInt(5)
ior_res |= MyInt(1)
print(ior_res.val)
__xor__
The __xor__
stands for bitwise XOR operator represented by the symbol "^" between the struct defining the method and self type or another type.
__rxor__
The __rxor__
stands for reverse bitwise XOR operator represented by the symbol "^" between the struct defining the method and self type or another type. This is invoked when the first value does not have __xor__
implemented. In this case, the Mojo compiler checks if the second value has __rxor__
implemented, and calls that one.
__ixor__
The __ixor__
stands for in-place bitwise OR operator represented by the symbol "^=" between the struct defining the method and self type or another type. Instead of returning a new instance like in __xor__
, the __ixor__
updates its own instance with the result.
struct MyFloat:
var val: Float64
fn __init__(inout self, value: Float64):
self.val = value
struct MyInt:
var val: Int
fn __init__(inout self, value: Int):
self.val = value
fn __xor__(self, other: Self) -> Self:
print("xor invoked")
return Self(self.val ^ other.val)
fn __rxor__(self, other: MyFloat) -> Self:
print("rxor invoked")
return Self(int(other.val) ^ self.val)
fn __ixor__(inout self, other: Self):
print("ixor invoked")
self.val ^= other.val
Usage:
var num: MyInt = MyInt(42)
var xor_res = MyInt(5) ^ MyInt(3)
print(xor_res.val)
var rxor_res = MyFloat(15) ^ MyInt(17)
print(rxor_res.val)
var ixor_res = MyInt(5)
ixor_res ^= MyInt(3)
print(ixor_res.val)
11.3. Relational operators
11.3.1. Operators
__eq__
The __eq__
stands for equality operator represented by the symbol "==" between the struct defining the method and self type or another type. The operation returns a Bool
value.
struct MyInt:
var val: Int
fn __init__(inout self, value: Int):
self.val = value
fn __eq__(self, other: Self) -> Bool:
print("eq invoked")
return self.val == other.val
Usage:
var num: MyInt = MyInt(42)
print(MyInt(5) == MyInt(5))
print(MyInt(5) == MyInt(3))
__ne__
The __ne__
stands for inequality operator represented by the symbol "!=" between the struct defining the method and self type or another type. The operation returns a Bool
value. Note that __ne__
is not invoked when you call not x==y
though.
struct MyInt:
var val: Int
fn __init__(inout self, value: Int):
self.val = value
fn __ne__(self, other: Self) -> Bool:
print("ne invoked")
return self.val != other.val
Usage:
var num: MyInt = MyInt(42)
print(MyInt(5) != MyInt(5))
print(MyInt(5) != MyInt(3))
print(not MyInt(5) == MyInt(3))
__lt__
The __lt__
stands for less-than operator represented by the symbol "<" between the struct defining the method and self type or another type. The operation returns a Bool
value.
struct MyInt:
var val: Int
fn __init__(inout self, value: Int):
self.val = value
fn __lt__(self, other: Self) -> Bool:
print("lt invoked")
return self.val < other.val
Usage:
var num: MyInt = MyInt(42)
print(MyInt(5) < MyInt(5))
print(MyInt(3) < MyInt(5))
__gt__
The __gt__
stands for greater-than operator represented by the symbol ">" between the struct defining the method and self type or another type. The operation returns a Bool
value.
struct MyInt:
var val: Int
fn __init__(inout self, value: Int):
self.val = value
fn __gt__(self, other: Self) -> Bool:
print("gt invoked")
return self.val > other.val
Usage:
var num: MyInt = MyInt(42)
print(MyInt(5) > MyInt(5))
print(MyInt(5) > MyInt(3))
__le__
The __le__
stands for less-than-or-equal-to operator represented by the symbol "⇐" between the struct defining the method and self type or another type. The operation returns a Bool
value.
struct MyInt:
var val: Int
fn __init__(inout self, value: Int):
self.val = value
fn __le__(self, other: Self) -> Bool:
print("le invoked")
return self.val <= other.val
Usage:
var num: MyInt = MyInt(42)
print(MyInt(5) <= MyInt(5))
print(MyInt(5) <= MyInt(3))
__ge__
The __ge__
stands for greater-than-or-equal-to operator represented by the symbol ">=" between the struct defining the method and self type or another type. The operation returns a Bool
value.
struct MyInt:
var val: Int
fn __init__(inout self, value: Int):
self.val = value
fn __ge__(self, other: Self) -> Bool:
print("ge invoked")
return self.val >= other.val
Usage:
var num: MyInt = MyInt(42)
print(MyInt(5) >= MyInt(5))
print(MyInt(3) >= MyInt(5))
11.4. Index operators
Any large scale program would use collection data types such as lists, arrays, dictionaries extensively. Some programming languages have built-in syntax to make usage of such types convenient. Mojo has built-in syntactical support for collection types. Mojo allows accessing collection like data types using the syntax []
. For example, list[2]
. It also allows setting of values at a given index (e.g. list[2]=5).
In line with Mojo’s philosophy of moving as much functionality as possible to libraries, instead of having specially treated data types that have exclusive privilege of syntax, Mojo opened up the index operator capability to any type that defines __getitem__
and __setitem__
methods. This means that your custom List
struct will have the same syntactical support like the List
bundled with Mojo.
11.4.1. Operators
__getitem__
The __getitem__
is the method invoked when you try to access values stored within the collection using the my_list[index]
syntax (where my_list
is the collection and index
is the position of the element desired from the list).
__setitem__
The __setitem__
is the method invoked when you try to assign values to a collection using the my_list[index]=value
syntax (where my_list
is the collection and index
is the position at which the value will be assigned).
struct MyCollection:
var list: List[String]
fn __init__(inout self, list: List[String]):
self.list = list
fn __getitem__(self, x: Int) -> String:
return self.list[x]
fn __setitem__(inout self, x: Int, val: String):
self.list[x] = val
Usage:
var x = MyCollection(List(String("A"), String("B")))
print("Before:", x[1]) # Gets
x[1] = String("C") # Sets
print("After:", x[1])
It is also possible to get and set using multiple indices, especially useful for Matrix like data structures. This means that we can use in our code my_list[row, col, and so on…]
.
The following code listing shows such an example for a matrix. Note that this example is not a production quality implementation.
struct MyMatrix:
var val: List[List[String]]
var rows: Int
var cols: Int
fn __init__(inout self, rows: Int, cols: Int):
self.rows = rows
self.cols = cols
self.val = List[List[String]](capacity=rows)
for row in range(rows):
self.val.append(List[String](capacity=cols))
for col in range(cols):
self.val[row].append(String("None"))
fn __getitem__(self, row: Int, col: Int) -> String:
return self.val[row][col]
fn __setitem__(inout self, row: Int, col: Int, s: String):
self.val[row][col] = s
Usage:
var y = MyMatrix(5, 3)
print("Before:", y[1, 2]) # Gets
y[1, 2] = String("D") # Sets
print("After:", y[1, 2])
__getitem__
with Slice
Mojo supports getting a slice of a collection or a container using the slicing operator.
The following diagram shows the structure of the slicing operator.
In order to be able to get a slice from a collection or a container, the collection or the container must implement method __getitem__
that takes a Slice
object. This Slice
object is instantiated by the Mojo compiler when you use the slice operator. In a way you can think of the [start:stop:step]
as a literal form instantiating a Slice
.
The start
argument of Slice
is the start index of the collection from where the slice will be taken. The end
argument is the last index until which the slice will be taken (the element referred by this index will be excluded, only its previous element will be taken as last for the slice). The step
gives the number of increments to be taken to get the next element.
Any of the arguments of the Slice
can be negative. A negative value means reversal of the indexing or stepping. Care must be taken to provide valid combinations though, otherwise it results in error.
struct MySliceableCollection:
var list: List[String]
fn __init__(inout self, list: List[String]):
self.list = list
fn __getitem__(self, slc: Slice) -> List[String]:
return self.list[slc.start:slc.end:slc.step]
Usage:
var z = MySliceableCollection(
List(String("H"),
String("E"),
String("L"),
String("L"),
String("O"),
String("W"),
String("O"),
))
for i in z[0:3:1]:
print(i[], end=" ")
print()
for i in z[1:6:2]:
print(i[], end=" ")
print()
for i in z[1:]:
print(i[], end=" ")
print()
for i in z[:1]:
print(i[], end=" ")
print()
for i in z[-3:]:
print(i[], end=" ")
print()
for i in z[:-3]:
print(i[], end=" ")
print()
for i in z[::-1]:
print(i[], end=" ")
print()
11.5. Context management methods
In large programs, we often need to resources like files and database connections. When we open access to those resources, we typically have a handle, which we use to perform actions. However, once we have done with our actions, we must remember to cleanup or close the resources, otherwise we end up with dangling resources, memory leaks, locked files, etc. Mojo provides with
keyword for managing such context or resources.
The following diagram illustrates the syntax of the with
statement.
11.5.1. Methods
__enter__
The method that handles the allocation of the resource and returns a resource. The resource has scope only within the body of the with
. It is not mandatory to assign the return value to a variable, especially if it is not being used.
__exit__
The method that handles the cleanup of the resource. There are two implementations of the __exit__
method, one without any arguments __exit__(self, Error)
and one with Error
as an argument __exit__(self, Error)
. The __exit__(self, Error)
is invoked when the with
body has an exception and exits the with
block abnormally. The method __exit__(self, Error)
returns a Bool
to indicate whether or not to propagate the error further.
Any resources allocated in the __enter__
must be cleaned up at both the __exit__
methods, otherwise we would end up with dangling resources. For example, if __exit__(self, Error)
is not properly implemented, resource leaks will occur only when there are exceptions raised within the with
body. This will lead to rare but difficult to find defects.
The following code listing shows an example for the context manager.
struct Resource:
var name: String
fn __init__(inout self, name: String):
self.name = name
fn open(self):
print("Opened")
fn close(self):
print("Close")
fn __copyinit__(inout self, other:Resource):
self.name = other.name
struct MyResourceManager:
var resource: Resource
fn __init__(inout self):
self.resource = Resource("a_resource")
fn __enter__(self) -> Resource:
print("Entered context")
self.resource.open()
return self.resource
fn __exit__(self):
self.resource.close()
print("Exited context")
fn __exit__(self, err: Error) -> Bool:
self.resource.close()
print("Exited context")
return False
Usage:
with MyResourceManager() as res:
print("Inside context, resource is:", res.name)
raise Error("An error while processing")
11.6. Other special methods
11.6.1. Methods
__len__
The __len__
is defined within Sized
trait and is used by the built-in len
function. The __len__
method returns the length or size of the struct implementing it.
__int__
The __int__
is defined within Intable
trait and is used by the built-in int
function. The __int__
method returns an integer representation of the struct implementing it.
__bool__
The __bool__
is defined within Boolable
trait and is used by conditional statements such as if
to convert the given value to a boolean value for evaluation. The __bool__
method returns a boolean representation of the struct implementing it.
__str__
The __str__
is defined within Stringable
trait and is used by the built-in str
function. The __str__
method returns an string representation of the struct implementing it.
\\write_to
The built-in print
function expects a value that implements Writable
trait. The Writable
trait expects write_to
to be implemented, which is then used by the print
function. Although it is not a dunder method, the write_to
is useful for those cases where you want to write the content of a value to an output writer, like for example stdout
.
The following code listing provides examples of special methods.
struct MyStruct(Sized, Intable, Boolable, Stringable, Writable):
var ints: List[Int]
fn __init__(inout self, ints: List[Int]):
self.ints = ints
fn __len__(self) -> Int:
print("len called")
return len(self.ints)
fn __int__(self) -> Int:
print("int called")
var sum: Int = 0
for i in range(len(self.ints)):
sum += self.ints[i]
return sum
fn __bool__(self) -> Bool:
print("bool called")
return len(self.ints)>0
fn __str__(self) -> String:
print("str called")
return "MyStruct"
fn write_to[W: Writer](self, inout writer: W):
print("write_to called")
writer.write_bytes("MyStruct".as_bytes())
Usage:
var st = MyStruct(List(1, 2, 3))
print(len(st))
print(int(st))
if st: # Uses __bool__
print("MyStruct is true")
print(str(st))
print(st) # Can directly print st because the write_to method is implemented
__is__
The __is__
method is used by the is
clause to compare the identity between two values, and returns True
if the identities are the same. Note that it is different from the eq
method. The eq
compares if two values are the same content-wise, while is
checks if the two values are having the same identities. This means that two objects may have exactly same content, but different identities. Typical implementations check if the memory location of the two values are the same, in which case it would be considered as being identical.
__isnot__
The __isnot__
method is used by the is not
clause to compare the identity between two values, and returns True
if the identities are not the same. It is the opposite of the is
clause, and in most cases it is sufficient for the implementation to return a negation of the is
method.
The following code listing provides examples of is
and isnot
methods.
from memory import UnsafePointer
struct MyStruct:
var ptr: UnsafePointer[Int]
fn __init__(inout self, ptr: UnsafePointer[Int]):
self.ptr = ptr
fn __is__(self, other: MyStruct) -> Bool:
print("__is__ called")
return self.ptr == other.ptr
fn __isnot__(self, other: MyStruct) -> Bool:
print("__isnot__ called")
return not(self is other)
Usage:
var x: Int = 10
var y: Int = 10
print(MyStruct(UnsafePointer.address_of(x)) is MyStruct(UnsafePointer.address_of(y))) # Results in False
print(MyStruct(UnsafePointer.address_of(x)) is MyStruct(UnsafePointer.address_of(x))) # Results in True
print(MyStruct(UnsafePointer.address_of(x)) is not MyStruct(UnsafePointer.address_of(y))) # Results in True
__getattr__
Many dynamic languages allow us to dynamically define attributes that do not exist in the original struct definition (or class definition in most languages). This ability allows programmers to define an ergonomic API, especially for object relational mapping. Even though Mojo is a statically compiled language, it allows for such a dynamic definition of attributes through a combination of methods, __getattr__
and __setattr__
.
The __getattr__
method takes in the attribute name as an argument. When you call an attribute my_attr
within a struct (my_struct.my_attr
), it is this attribute name my_attr
that gets passed as the argument to the method __getattr__
. Within the __getattr__
, you can as an example return a result based on a database query involving the attribute name. Mojo does not restrict what you do with the attribute name, as long as you return a value conforming to the return type of the function.
__setattr__
The __setattr__
method takes in the attribute name and its value as arguments. When you set an attribute my_attr
within a struct with a value (my_struct.my_attr = 'a value'
), the __setattr__
is called with the given attribute name my_attr
and the value 'a value'
passed as arguments.
The following code listing provides examples of getattr
and setattr
methods.
struct MyStruct:
var fields: Dict[String, String]
fn __init__(inout self, fields: Dict[String, String]):
self.fields = fields
fn __getattr__(self, attr: String) raises -> String:
return self.fields[attr]
fn __setattr__(inout self, attr: String, value: String) raises:
self.fields[attr] = value
Usage:
var d: Dict[String, String] = Dict[String, String]()
d["name"] = "IK"
var st = MyStruct(d)
print(st.name) # __getattr__ is called here
st.name = "PK" # __setattr__ is called here
print(st.name)
__contains__
In Mojo you can check if a value is contained within a struct using the in
operator. For example, "IK" in my_string
, where my_string
is a string and the expression results in value True
if the literal IK
is found within my_string
.
The __contains__
method is used by the in
operator to check if a given value is within the struct defining that method.
The following code listing provides an example of contains
method.
struct MyStruct:
var ints: List[Int]
fn __init__(inout self, ints: List[Int]):
self.ints = ints
fn __contains__(self, value: Int) -> Bool:
for i in self.ints:
if i[] == value:
return True
return False
Usage:
var my_struct = MyStruct(List(1, 2, 3))
print(1 in my_struct) # Returns True
print(5 in my_struct) # Returns False
The __contains__
method also works with custom types as shown in the example below.
struct MyStruct2:
var first_name: String
var last_name: String
fn __init__(inout self, first_name: String, last_name: String):
self.first_name = first_name
self.last_name = last_name
fn __contains__(self, other: MyStruct2) -> Bool:
return (self.first_name == other.first_name) or self.last_name == other.last_name
Usage:
var my_struct2 = MyStruct2("Ram", "C")
print(MyStruct2("Ram", "T") in my_struct2) # Returns True
print(MyStruct2("Kri", "C") in my_struct2) # Returns True
print(MyStruct2("C", "Ram") in my_struct2) # Returns False