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 mut or var 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__(out self, value: Float64):
self.val = value
struct MyInt:
var val: Int
fn __init__(out 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__(mut 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__(out self, value: Float64):
self.val = value
struct MyInt:
var val: Int
fn __init__(out 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__(mut 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__(out self, value: Float64):
self.val = value
struct MyInt:
var val: Int
fn __init__(out 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__(mut 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__(out self, value: List[List[Float16]], rows: Int, cols: Int):
self.val = value.copy()
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__(out self, value: List[List[Int]], rows: Int, cols: Int):
self.val = value.copy()
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__(mut self, other: Self): # Naive implementation - not for production use
print("imatmul invoked")
var res = Self._matmul_internal(self, other)
self.val = res.val.copy()
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__(out self, value: Float64):
self.val = value
struct MyInt:
var val: Int
fn __init__(out 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__(mut 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__(out self, value: Float64):
self.val = value
struct MyInt:
var val: Int
fn __init__(out 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__(mut 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__(out self, value: Float64):
self.val = value
struct MyInt:
var val: Int
fn __init__(out 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__(mut 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__(out self, value: Int):
self.val = value
fn __pow__(self, other: Self) -> Self:
print("pow invoked")
return Self(self.val ** other.val)
fn __ipow__(mut 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__(out 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__(out 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__(out 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__(out self, value: Float64):
self.val = value
struct MyInt:
var val: Int
fn __init__(out 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__(mut 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__(out self, value: Float64):
self.val = value
struct MyInt:
var val: Int
fn __init__(out 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__(mut 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__(out self, value: Float64):
self.val = value
struct MyInt:
var val: Int
fn __init__(out 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__(mut 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__(out self, value: Float64):
self.val = value
struct MyInt:
var val: Int
fn __init__(out 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__(mut 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__(out self, value: Float64):
self.val = value
struct MyInt:
var val: Int
fn __init__(out 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__(mut 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__(out 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__(out 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__(out 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__(out 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__(out 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__(out 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__(out self, list: List[String]):
self.list = list.copy()
fn __getitem__(self, x: Int) -> String:
return self.list[x]
fn __setitem__(mut 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__(out 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__(mut 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__(out self, list: List[String]):
self.list = list.copy()
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(ImplicitlyCopyable):
var name: String
fn __init__(out self, name: String):
self.name = name
fn open(self):
print("Opened")
fn close(self):
print("Close")
fn __copyinit__(out self, other:Resource):
self.name = other.name
struct MyResourceManager:
var resource: Resource
fn __init__(out 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__(out self, ints: List[Int]):
self.ints = ints.copy()
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 __copyinit__(out self, other: MyStruct):
# Ignore this method for now
self.ints = other.ints.copy()
fn __moveinit__(out self, deinit other: MyStruct):
# Ignore this method for now
self.ints = other.ints^
fn write_to[W: Writer](self, mut 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(String(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 Pointer
struct MyStruct[o:ImmutableOrigin]:
var ptr: Pointer[Int, o]
fn __init__(out self, ptr: Pointer[Int, o]):
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(Pointer(to=x)) is MyStruct(Pointer(to=y))) # Results in False
print(MyStruct(Pointer(to=x)) is MyStruct(Pointer(to=x))) # Results in True
print(MyStruct(Pointer(to=x)) is not MyStruct(Pointer(to=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__(out self, fields: Dict[String, String]):
self.fields = fields.copy()
fn __getattr__(self, attr: String) raises -> String:
return self.fields[attr]
fn __setattr__(mut 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__(out self, ints: List[Int]):
self.ints = ints.copy()
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__(out 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
__merge_with__
We saw that to convert a type from one type to another, we can use @implicit decorator to perform an implicit conversion without any in-built language support. However, thre are limitations to implicit conversions. For example, if the type we define cannot not have a single value constructor, then we cannot use implicit conversion. In such cases, we can use the __merge_with__ method to convert from one type to another. Please note that it is not a full replacement for implicit conversion, but it is useful in some scenarios.
struct MyStruct:
var num: Int
var denom: Int
fn __init__(out self, num: Int, denom: Int):
self.num = num
self.denom = denom
fn __merge_with__[other_type: type_of(Int)](self) -> Int:
return Int(self.num/self.denom)
Usage:
var val = 10
var my_struct = MyStruct(10,5)
var result = val if val < 5 else my_struct # Uses __merge_with__ to convert my_struct to Int
print(result) # Prints 2
In the above example, the variable result is inferred to be of type Int because the MyStruct has a merge_with method that converts it to Int.