Double underscore (dunder) methods, also known as special methods in Mojo🔥 are used to define how objects behave in specific situations. Mojo🔥 is a member of the Python family and it embraces Python's dunder method syntax and extends it in new ways. You can use dunder methods to customize the behavior of common operations such as + , - , * , ** , == , % , @ and others on custom data structures, initialize and delete objects, get string representation, enable indexing on objects and more. For example, you can use dunder methods to define __add__() to define element-wise addition operation using the + operator, on custom objects that represent vectors or tensors.
Mojo🔥 supports several dunder methods and in this blog post we'll dive deep into every single dunder method listed below with code examples. They are broadly categorized into the following types:
Initialize, copy, move, delete Getters and Setters: obj[idx] , obj[idx]=val Unary arithmetic operators: -obj , +obj , ~obj Comparison operators: < , <= , == , ~= , > , >= Normal, reversed and in-place arithmetic operators: + , - , * , @ , / , // , % , ** , << , >> , & , | , ^ Defined by built-in Traits: len(obj) , int(obj) , print(obj) Note: 1-5 are compiler dunder methods and 6 are dunder methods defined by built-in Traits that enable functions like len() , int() and print() to work on custom objects.
Dunder methods in Mojo🔥 Let’s say you want to implement the following equation in Mojo:
$z = x^3 + 3x - \frac{x}{5} + 0.3 + y^2$
It's easy to do that in Mojo if you’re working with scalar floating point variables:
Mojo
x = -1.0
y = 1.0
z = x**3 + 3*x - x/5. + 0.3 + y**2
print(z)
Copy
Output:
What if you wanted to extend this capability to custom objects that implement vectors? We’ll have to explicitly define the behavior of these three operations ** , * , / for two cases:
When used between a scalar and a vector When used between a vector and a vector You can define these behaviors in the dunder methods __pow__() , __mul__() and __truediv__() with overloaded versions for scalar-vector and vector-vector operations. Through the rest of this blog post I'll walk you through creating an example Mojo struct (similar to a class in Python) that implements most of the dunder method supported by Mojo.
Note: This example was built with Mojo SDK v0.7. Future releases may add, change or remove supported dunder methods and/or Traits.
DunderArray: An example Mojo struct that demonstrates how to use Mojo dunder methods The hero of this blog post is the DunderArray . We’ll implement a DunderArray struct which has the following qualities:
DunderArray is an array data structure that is parameterized by a type (Float64 , Float32 , Bool , etc.)DunderArray implements all Mojo dunder methods shown in the figure above.DunderArray also implements several dunder methods defined by built-in TraitsUsing DunderArray you can do scalar-vector and vector-vector math operations like the example below, making it easy to implement mathematical equations. The complete example is on GitHub .
Mojo
x = DunderArray(np.sin(np.arange(0,6,0.1)))
y = DunderArray.rand(x.numel)
z = x**3 + 3*x - x/5. + 0.3 + y**2
plt.plot(z.to_numpy(),'r')
plt.show()
Copy
DunderArray struct has the following skeleton structure:
Mojo
struct DunderArray[dtype: DType = DType.float64](Stringable, Intable, CollectionElement, Sized):
var _ptr: DTypePointer[dtype]
var numel: Int
# Initialize, copy, move, delete
…
# Get and Set
…
# Unary operators
…
# Comparison operators.
…
# Normal, reversed and in-place operators
…
# Defined by built-in Traits
…
Copy
We’ll work our way through every dunder method defined in DunderArray struct, and for each dunder method in DunderArray we’ll discuss its usage and implementation.
1. Initialize, copy, move, delete __init__() The __init__() method is used to initialize a new object when it is created, set its initial attributes or perform additional setup. You can also overload it to allow different types of initializations.
Usage:
Mojo
arr1 = DunderArray(5)
arr2 = DunderArray(1,2,3,4,5)
print(arr1)
print(arr2)
Copy
Output:
Output
[0.0 0.0 0.0 0.0 0.0]
Length:5, DType:float64
[1.0 2.0 3.0 4.0 5.0]
Length:5, DType:float64
Copy
Implementation: The two different initializations seen above DunderArray(5) and DunderArray(1,2,3,4,5) call two different __init__ functions:
Mojo
fn __init__(inout self, numel: Int):
self._ptr = DTypePointer[dtype].alloc(numel)
self.numel = numel
memset_zero[dtype](self._ptr, numel)
fn __init__(inout self, *data: Scalar[dtype]):
self.numel = len(data)
self._ptr = DTypePointer[dtype].alloc(len(data))
for i in range(len(data)):
self._ptr[i] = data[i]
Copy
You can have any number of overloads for __init__() to customize how you want to initialize your struct. The complete example on GitHub also includes an __init__() function that can load NumPy arrays into DunderArray .
Since my goal is to introduce you to dunder methods in Mojo, I’ve implemented these methods in DunderArray .
__copyinit__() The __copyinit__() dunder method is invoked when the assignment operator =
is used to make a copy of an object.
Usage: You can assign a DunderArray variable a to a new variable b:
Mojo
arr = DunderArray.rand(5)
print("Original:",arr)
arr_copy = arr
print()
print("Copy:",arr_copy)
Copy
Output: Implementation: When a is assigned to b , __copyinit__() is invoked, which creates a copy of the underlying data in DunderArray and assigns it to other which in this case is variable b
Mojo
fn __copyinit__(inout self, other: Self):
self.numel = other.numel
self._ptr = DTypePointer[dtype].alloc(other.numel)
memcpy[dtype](self._ptr, other._ptr, self.numel)
Copy
You can learn more about copy constructors in the Mojo programming manual.
__moveinit__() Similarly, when we want to move the value of a variable, its ownership is transferred from one variable to another using __moveinit__()
Usage:
Mojo
fn test_move():
var arr = DunderArray(2,4,6,8)
print(arr)
var arr_moved = arr^
print(arr_moved)
print(arr) #<-- ERROR: use of uninitialized value 'arr'
test_move()
Copy
Output:
Output
error: Expression [21]:6:11: use of uninitialized value 'arr'
print(arr) #<-- ERROR: use of uninitialized value 'arr'
^
Copy
Implementation:
Mojo
fn __moveinit__(inout self, owned existing: Self):
self._ptr = existing._ptr
self.numel = existing.numel
existing.numel = 0
existing._ptr = DTypePointer[dtype]()
Copy
You can learn more about move constructors in the Mojo programming manual.
Note: __copyinit__ and __moveinit__ are unique to Mojo🔥 and are not supported in Python. Mojo is Pythonic in syntax but also lets you incrementally adopt systems-programming features like strong type checking and memory management. You can also avoid implementing __init__() , __copyinit__() and __moveinit__() completely by using the @value decorator, as discussed in this doc page .
__del__() The __del__ dunder method is used by Mojo to end a value’s lifetime.
Usage: Mojo uses static compiler analysis to find when a value is last used and calls __del__ to perform the clean up.
Implementation: In DunderArray this function simply frees the pointer. You can add any additional steps necessary to cleanup if needed.
Mojo
fn __del__(owned self):
self._ptr.free()
Copy
You can learn more about destruction behavior in the programming manual .
2. Getters and Setters In Mojo and in Python, the __getitem__() method is used to define how an object should behave when accessed with square brackets arr[index] . The __setitem__ method is used to specify the behavior when assigning a value to an item using square brackets arr[index] = value .
__getitem__() Usage: In DunderArray we can get an element from the array by using square bracket indexing:
Mojo
arr = DunderArray(1,2,3,4,5)
print(arr[3])
Copy
Output: Implementation: We implement this logic in the __getitem__() method to load and return a value at a specific index.
Mojo
fn __getitem__(self, idx: Int) -> Scalar[dtype]:
return self._ptr.simd_load[1](idx)
Copy
You can also overload __getitem__() to implement slicing using the syntax arr[a:b] to retrieve values between a and b. I wrote an earlier blog post on how to implement NumPy style matrix slicing in Mojo🔥 and you can find the code and explanation on how to do that in that blog post.
__setitem__() DunderArray , lets you access specific elements in the array using square bracket indexing:
Usage:
Mojo
arr = DunderArray(1,2,3,4,5)
arr[3]=100
print(arr)
Copy
Output:
Output
[1.0 2.0 3.0 100.0 5.0]
Length:5, DType:float64
Copy
Implementation: The __setitem__() dunder method lets you define the behavior of the square bracket assignment arr[idx] = value
operation.
Mojo
fn __setitem__(inout self, elem: Int, val: Scalar[dtype]):
return self._ptr.simd_store[1](elem, val)
Copy
3. Unary operators Unary arithmetic operators in Mojo are used to perform operations on a single operand, and they include + for positive, - for negation, and ~ for boolean inversion.
__pos__() and __neg__() Usage:
Mojo
arr = DunderArray(1,2,3,4,5)
print(+arr)
print(arr)
print(-arr)
Copy
Output:
Output
[1.0 2.0 3.0 4.0 5.0]
Length:5, DType:float64
[1.0 2.0 3.0 4.0 5.0]
Length:5, DType:float64
[-1.0 -2.0 -3.0 -4.0 -5.0]
Length:5, DType:float64
Copy
Implementation: In the __pos__() and __neg__() dunder methods, we multiply each element of the DunderArray with +1.0 or -1.0. These functions assume the multiplication operation * has already been defined on DunderArray . We’ll discuss that in the arithmetic operations section.
Mojo
fn __neg__(self)->Self:
return self*(-1.0)
fn __pos__(self)->Self:
return self*(1.0)
Copy
__invert__() Usage: Since DunderArray is parameterized on DType , we can also initialize a bool array. The __invert__() dunder method allows us to define the behavior of boolean inversion
Mojo
arr_bool = DunderArray[DType.bool](True,False,True)
print(~arr_bool)
Copy
Output:
Output
[False True False]
Length:3, DType:bool
Copy
Implementation: To invert a DunderArray we have to first check that the array is of type bool. We do that using the compile time conditional _mlirtype_is_eq[Scalar[dtype], Bool]() which returns True if the array is boolean. If it is a boolean array, we use the elementwise function to invert the array in a fast vectorized way.
Mojo
fn __invert__(self) -> Self:
if _mlirtype_is_eq[Scalar[dtype], Bool]():
let new_array = Self(self.numel)
@parameter
fn wrapper[simd_width:Int,rank:Int=1](idx: StaticIntTuple[rank]):
new_array._ptr.simd_store[simd_width](idx[0], ~self._ptr.simd_load[simd_width](idx[0]))
elementwise[1, simdwidthof[dtype](), wrapper](self.numel)
return new_array
else:
print('Error: You can only invert Bool arrays')
return self
Copy
4. Comparison operators __lt__() , __le__() , __eq__() , __ne__() , __gt__() , __ge__() DunderArray s can be compared with each other using these comparison dunder methods. When comparing vectors, you have to define a metric on the vector that you wish to compare between vectors. In DunderArray struct we compute the euclidean norm and compare the norm values of two arrays.
Usage:
Mojo
arr1 = DunderArray.rand(100)
arr2 = DunderArray.rand(100)
print("Norm of arr1:",(arr1**2)._reduce_sum())
print("Norm of arr2:",(arr2**2)._reduce_sum())
print("Comparing the squared euclidean norm of arr1 and arr2")
print("__lt__: arr1 < arr2:", arr1 < arr2)
print("__le__: arr1 <= arr2:", arr1 <= arr2)
print("__eq__: arr1 == arr2:", arr1 == arr2)
print("__ne__: arr1 != arr2:", arr1 != arr2)
print("__gt__: arr1 > arr2:", arr1 > arr2)
print("__ge__: arr1 >= arr2:", arr1 >= arr2)
Copy
Output:
Output
Norm of arr1: [33.0329]
Length:1, DType:float64
Norm of arr2: [33.5523]
Length:1, DType:float64
Comparing the squared euclidean norm of arr1 and arr2
__lt__: arr1 < arr2: True
__le__: arr1 <= arr2: True
__eq__: arr1 == arr2: False
__ne__: arr1 != arr2: True
__gt__: arr1 > arr2: False
__ge__: arr1 >= arr2: False
Copy
Implementation: DunderArray struct implements the following comparision dunder methods:
__lt__
: Less than comparison.__le__
: Less than or equal to comparison.__eq__
: Equality comparison.__ne__
: Inequality comparison.__gt__
: Greater than comparison.__ge__
: Greater than or equal to comparison.
Mojo
fn __lt__(self, other: Self) -> Bool:
return (self**2)._reduce_sum()[0] < (other**2)._reduce_sum()[0]
fn __le__(self, other: Self) -> Bool:
return (self**2)._reduce_sum()[0] <= (other**2)._reduce_sum()[0]
fn __eq__(self, other: Self) -> Bool:
return self._ptr == other._ptr
fn __ne__(self, other: Self) -> Bool:
return self._ptr != other._ptr
fn __gt__(self, other: Self) -> Bool:
return (self**2)._reduce_sum()[0] > (other**2)._reduce_sum()[0]
fn __ge__(self, other: Self) -> Bool:
return (self**2)._reduce_sum()[0] >= (other**2)._reduce_sum()[0]
Copy
5. Normal, reflected and in-place arithmetic operators In Mojo, normal arithmetic operators perform the standard arithmetic operation between two operands, such as a + b for addition, a - b for subtraction, a * b for multiplication, and a / b for division. These are implemented with __add__() , __sub__() , __mul__() and __div__() dunder methods.
Reflected arithmetic operators are used when the left operand does not support the corresponding normal operator. For example, a float Scalar does not support addition with DunderArray , because it doesn’t know what a DunderArray is. The DunderArray struct can implement a reflected addition operator specified by the __radd__() that can perform the operation.
Finally, in-place arithmetic operators combine the current value of the left operand with the right operand and assign the result to the left operand. For instance, a += b is an in-place addition, and it is equivalent to a = a + b . These operators are specified by methods such as __iadd__() , __isub__() , __imul__() , and __itruediv__() .
__add__() , __radd__() , __iadd__() You can add a scalar to a DunderArray or add two DunderArray s together.
Usage:
Mojo
arr1 = DunderArray.rand(5)
arr2 = DunderArray(100,100,100,100,100)
# Normal and in-place
print(arr1+100.0)
print(arr1+arr2)
# Reflected
print(100.0+arr1)
#In-place
arr1+=100.0
print(arr1)
Copy
Output:
Output
[100.6571 100.2221 100.3893 100.7008 100.0546]
Length:5, DType:float64
[100.6571 100.2221 100.3893 100.7008 100.0546]
Length:5, DType:float64
[100.6571 100.2221 100.3893 100.7008 100.0546]
Length:5, DType:float64
[100.6571 100.2221 100.3893 100.7008 100.0546]
Length:5, DType:float64
Copy
Implementation: In DunderArray we use two helper functions to calculate elementwise operations between floating point scalars and DunderArray (_elemwise_scalar_math ) and DunderArray and DunderArray _elemwise_array_math
Mojo
fn __add__(self, s: Scalar[dtype])->Self:
return self._elemwise_scalar_math[math.add](s)
fn __add__(self, other: Self)->Self:
return self._elemwise_array_math[math.add](other)
fn __radd__(self, s: Scalar[dtype])->Self:
return self+s
fn __iadd__(inout self, s: Scalar[dtype]):
self = self+s
fn _r_elemwise_scalar_math[func: fn[dtype: DType, width: Int](SIMD[dtype, width],SIMD[dtype, width])->SIMD[dtype, width]](self, s: Scalar[dtype]) -> Self:
alias simd_width: Int = simdwidthof[dtype]()
let new_array = Self(self.numel)
@parameter
fn elemwise_vectorize[simd_width: Int](idx: Int) -> None:
new_array._ptr.simd_store[simd_width](idx, func[dtype, simd_width](SIMD[dtype, simd_width](s), self._ptr.simd_load[simd_width](idx)))
vectorize[simd_width, elemwise_vectorize](self.numel)
return new_array
fn _elemwise_array_math[func: fn[dtype: DType, width: Int](SIMD[dtype, width],SIMD[dtype, width])->SIMD[dtype, width]](self, other: Self) -> Self:
alias simd_width: Int = simdwidthof[dtype]()
let new_array = Self(self.numel)
@parameter
fn elemwise_vectorize[simd_width: Int](idx: Int) -> None:
new_array._ptr.simd_store[simd_width](idx, func[dtype, simd_width](self._ptr.simd_load[simd_width](idx), other._ptr.simd_load[simd_width](idx)))
vectorize[simd_width, elemwise_vectorize](self.numel)
return new_array
Copy
__sub__() , __rsub__() , __isub__() Similarly, you can subtract DunderArray s and scalars
Usage:
Mojo
arr1 = DunderArray.rand(5)
arr2 = DunderArray(100,100,100,100,100)
# Normal and in-place
print(arr1-100.0)
print(arr1-arr2)
# Reflected
print(100.0-arr1)
#In-place
arr1-=100.0
print(arr1)
Copy
Output:
Output
[-99.1395 -99.3242 -99.3291 -99.9894 -99.9540]
Length:5, DType:float64
[-99.1395 -99.3242 -99.3291 -99.9894 -99.9540]
Length:5, DType:float64
[99.1395 99.3242 99.3291 99.9894 99.9540]
Length:5, DType:float64
[-99.1395 -99.3242 -99.3291 -99.9894 -99.9540]
Length:5, DType:float64
Copy
Implementation:
Mojo
fn __sub__(self, s: Scalar[dtype])->Self:
return self._elemwise_scalar_math[math.sub](s)
fn __sub__(self, other: Self)->Self:
return self._elemwise_array_math[math.sub](other)
fn __rsub__(self, s: Scalar[dtype])->Self:
return -(self-s)
fn __isub__(inout self, s: Scalar[dtype]):
self = self-s
Copy
__mul__() , __rmul__() , __imul__() Usage:
Mojo
arr1 = DunderArray.rand(5)
arr2 = DunderArray(100,100,100,100,100)
# Normal and in-place
print(arr1*100.0)
print(arr1*arr2)
# Reflected
print(100.0*arr1)
#In-place
arr1*=100.0
print(arr1)
Copy
Output:
Output
[66.3367 94.0818 30.6635 95.1850 10.8657]
Length:5, DType:float64
[66.3367 94.0818 30.6635 95.1850 10.8657]
Length:5, DType:float64
[66.3367 94.0818 30.6635 95.1850 10.8657]
Length:5, DType:float64
[66.3367 94.0818 30.6635 95.1850 10.8657]
Length:5, DType:float64
Copy
Implementation:
Mojo
fn __mul__(self, s: Scalar[dtype])->Self:
return self._elemwise_scalar_math[math.mul](s)
fn __mul__(self, other: Self)->Self:
return self._elemwise_array_math[math.mul](other)
fn __rmul__(self, s: Scalar[dtype])->Self:
return self*s
fn __imul__(inout self, s: Scalar[dtype]):
self = self*s
Copy
__matmul__() , __imatmul__() You can also define a special behavior for the @ operators, which is typically reserved for matrix multiplication. Since we're working with vectors only, and * already defines elementwise vector multiplications, we'll use the @ operators to calculate dot products between DunderArray s:
Usage:
Mojo
arr1 = DunderArray.rand(5)
arr2 = DunderArray.rand(5)
# Normal and in-place
print(arr1@arr2)
#Verify
print((arr1*arr2)._reduce_sum())
#In-place
arr1@=arr2
print(arr1)
Copy
Output:
Output
[0.7615]
Length:1, DType:float64
[0.7615]
Length:1, DType:float64
[0.7615]
Length:1, DType:float64
Copy
Implementation: The implementation is similar to * but we also reduce the result using the helper function _reduce_sum
Mojo
fn __matmul__(self, other: Self)->Self:
return self._elemwise_array_math[math.mul](other)._reduce_sum()
fn __imatmul__(inout self, other: Self):
self = self.__matmul__(other)
fn _reduce_sum(self) -> Self:
var reduced = Self(1)
alias simd_width: Int = simdwidthof[dtype]()
@parameter
fn vectorize_reduce[simd_width: Int](idx: Int) -> None:
reduced[0] += self._ptr.simd_load[simd_width](idx).reduce_add()
vectorize[simd_width,vectorize_reduce](self.numel)
return reduced
Copy
__truediv__() , __rtruediv__() , __itruediv__() Usage:
Mojo
arr1 = DunderArray.rand(5)
arr2 = DunderArray(10,10,10,10,10)
val = 10.0
print(arr1)
print(arr1/val)
print(arr1/arr2)
print(val/arr1)
arr2/=val
print(arr2)
Copy
Output:
Output
[0.1848 0.4721 0.8434 0.3443 0.6652]
Length:5, DType:float64
[0.0184 0.0472 0.0843 0.0344 0.0665]
Length:5, DType:float64
[0.0184 0.0472 0.0843 0.0344 0.0665]
Length:5, DType:float64
[54.1012 21.1811 11.8554 29.0419 15.0318]
Length:5, DType:float64
[1.0 1.0 1.0 1.0 1.0]
Length:5, DType:float64
Copy
Implementation:
Mojo
fn __truediv__(self, s: Scalar[dtype])->Self:
return self._elemwise_scalar_math[math.div](s)
fn __truediv__(self, other: Self)->Self:
return self._elemwise_array_math[math.div](other)
fn __rtruediv__(self, s: Scalar[dtype])->Self:
return self._r_elemwise_scalar_math[math.div](s)
fn __itruediv__(inout self, s: Scalar[dtype]):
self = self/s
Copy
__floordiv__() , __rfloordiv__() , __ifloordiv__() Floor division is similar to true division operation, bit it rounds down the result to the nearest integer, yielding the largest integer that is less than or equal to the exact result. DunderArray implements the //
Usage:
Mojo
arr1 = DunderArray.rand(5)*100.0
arr2 = DunderArray(10,10,10,10,10)
val = 10.0
print(arr1)
print(arr1//val)
print(arr1//arr2)
print(val//arr1)
arr2//=val
print(arr2)
Copy
Output:
Output
[43.4406 39.3320 76.2639 79.3802 94.1105]
Length:5, DType:float64
[4.0 3.0 7.0 7.0 9.0]
Length:5, DType:float64
[4.0 3.0 7.0 7.0 9.0]
Length:5, DType:float64
[0.0 0.0 0.0 0.0 0.0]
Length:5, DType:float64
[1.0 1.0 1.0 1.0 1.0]
Length:5, DType:float64
Copy
Implementation:
Mojo
fn __floordiv__(self, s: Scalar[dtype])->Self:
return (self/s)._elemwise_transform[math.floor]()
fn __floordiv__(self, other: Self)->Self:
return (self/other)._elemwise_transform[math.floor]()
fn __rfloordiv__(self, s: Scalar[dtype])->Self:
return (s/self)._elemwise_transform[math.floor]()
fn __ifloordiv__(inout self, s: Scalar[dtype]):
self = self.__rfloordiv__(s)
Copy
__mod__() , __rmod__() , __imod__() DunderArray implements the modulus operation using the %
operator, which returns the remainder of the elementwise division of two arrays or between one array and a scalar.
Usage:
Mojo
arr1 = DunderArray.rand(5)*100.0
arr2 = DunderArray(10,10,10,10,10)
val = 10.0
print(arr1)
print(arr1%val)
print(arr1%arr2)
print(val%arr1)
arr2%=val
print(arr2)
Copy
Output:
Output
[51.6576 96.3232 19.3779 32.9542 54.0671]
Length:5, DType:float64
[1.6576 6.3232 9.3779 2.9542 4.0671]
Length:5, DType:float64
[1.6576 6.3232 9.3779 2.9542 4.0671]
Length:5, DType:float64
[10.0 10.0 10.0 10.0 10.0]
Length:5, DType:float64
[0.0 0.0 0.0 0.0 0.0]
Length:5, DType:float64
Copy
Implementation:
Mojo
fn __mod__(self, s: Scalar[dtype])->Self:
return self._elemwise_scalar_math[math.mod](s)
fn __mod__(self, other: Self)->Self:
return self._elemwise_array_math[math.mod](other)
fn __rmod__(self, s: Scalar[dtype])->Self:
return self._r_elemwise_scalar_math[math.mod](s)
fn __imod__(inout self, s: Scalar[dtype]):
self = self.__mod__(s)
Copy
__pow__() , __ipow__() DunderArray s can be raised to an integer power:
Usage:
Mojo
arr_pow = DunderArray(1,2,3,4,5)
let val: Int = 2
print(arr_pow)
print(arr_pow**val)
arr_pow**=val
print(arr_pow)
Copy
Output:
Output
[1.0 2.0 3.0 4.0 5.0]
Length:5, DType:float64
[1.0 4.0 9.0 16.0 25.0]
Length:5, DType:float64
[1.0 4.0 9.0 16.0 25.0]
Length:5, DType:float64
Copy
Implementation: To calculate the power of a floating point variable we'll implement a helper function called _elemwise_pow() shown below.
Mojo
fn __pow__(self, p: Int)->Self:
return self._elemwise_pow(p)
fn __ipow__(inout self, p: Int):
self = self.__pow__(p)
fn _elemwise_pow(self, p: Int) -> Self:
alias simd_width: Int = simdwidthof[dtype]()
let new_array = Self(self.numel)
@parameter
fn tensor_scalar_vectorize[simd_width: Int](idx: Int) -> None:
new_array._ptr.simd_store[simd_width](idx, math.pow(self._ptr.simd_load[simd_width](idx), p))
vectorize[simd_width, tensor_scalar_vectorize](self.numel)
return new_array
Copy
__lshift__() , __rshift__() , __ilshift__() , __irshift__() DunderArray s can be left shifted (<< n
) which performs elementwise shifts the bits which is effectively multiplying it by $2^n$. Right shift (>> n
) performs elementwise bits to the right, akin to dividing the number by $2^n$
Usage:
Mojo
arr_shift = DunderArray[DType.int32](1,2,3,4,5)
print(arr_shift)
print(arr_shift << 2)
arr_shift<<=4
print(arr_shift)
print(arr_shift >> 2)
arr_shift>>=4
print(arr_shift)
Copy
Output:
Output
[1 2 3 4 5]
Length:5, DType:int32
[4 8 12 16 20]
Length:5, DType:int32
[16 32 48 64 80]
Length:5, DType:int32
[4 8 12 16 20]
Length:5, DType:int32
[1 2 3 4 5]
Length:5, DType:int32
Copy
Implementation: DunderArray must be of the type Int32
Mojo
fn __lshift__(self, p: Int) -> Self:
if _mlirtype_is_eq[Scalar[dtype], Int32]():
let new_array = Self(self.numel)
@parameter
fn wrapper[simd_width:Int,rank:Int=1](idx: StaticIntTuple[rank]):
new_array._ptr.simd_store[simd_width](idx[0], self._ptr.simd_load[simd_width](idx[0])<
Self:
if _mlirtype_is_eq[Scalar[dtype], Int32]():
let new_array = Self(self.numel)
@parameter
fn wrapper[simd_width:Int,rank:Int=1](idx: StaticIntTuple[rank]):
new_array._ptr.simd_store[simd_width](idx[0], self._ptr.simd_load[simd_width](idx[0])>>p)
elementwise[1, simdwidthof[dtype](), wrapper](self.numel)
return new_array
else:
print('Error: You can only shift int arrays')
return self
fn __ilshift__(inout self, p: Int):
self = self.__lshift__(p)
fn __irshift__(inout self, p: Int):
self = self.__rshift__(p)
Copy
__and__() , __iand__() ,__or__() , __ior__() ,__xor__() , __ixor__() DunderArray can also be initialized as a boolean array by using the dtype=DType.bool . With a boolean DunderArray , you can perform standard boolean operations such as and , or , and xor described below.
Usage:
Mojo
arr_bool1 = DunderArray[DType.bool](1,1,1,1)
arr_bool2 = DunderArray[DType.bool](1,0,1,0)
print("AND:")
print(arr_bool1 & arr_bool2)
arr_bool1&=arr_bool2
print(arr_bool1)
print("OR:")
print(arr_bool1 | arr_bool2)
arr_bool1|=arr_bool2
print(arr_bool1)
print("XOR:")
print(arr_bool1 ^ arr_bool2)
arr_bool1^=arr_bool2
print(arr_bool1)
Copy
Output:
Output
AND:
[True False True False]
Length:4, DType:bool
[True False True False]
Length:4, DType:bool
OR:
[True False True False]
Length:4, DType:bool
[True False True False]
Length:4, DType:bool
XOR:
[False False False False]
Length:4, DType:bool
[False False False False]
Length:4, DType:bool
Copy
Implementation: __and__
: Bitwise AND operation between DunderArray objects.__iand__
: In-place bitwise AND operation.__or__
: Bitwise OR operation between objects.__ior__
: In-place bitwise OR operation.__xor__
: Bitwise XOR operation between objects.__ixor__
: In-place bitwise XOR operation.
Mojo
fn __and__(self, other: Self) -> Self:
if ~_mlirtype_is_eq[Scalar[dtype], Float64]():
let new_array = Self(self.numel)
@parameter
fn wrapper[simd_width:Int,rank:Int=1](idx: StaticIntTuple[rank]):
new_array._ptr.simd_store[simd_width](idx[0],
self._ptr.simd_load[simd_width](idx[0])&other._ptr.simd_load[simd_width](idx[0]))
elementwise[1, simdwidthof[dtype](), wrapper](self.numel)
return new_array
else:
print('Error: You can only AND int or bool arrays')
return self
fn __iand__(inout self, other: Self):
self = self.__and__(other)
fn __or__(self, other: Self) -> Self:
if ~_mlirtype_is_eq[Scalar[dtype], Float64]():
let new_array = Self(self.numel)
@parameter
fn wrapper[simd_width:Int,rank:Int=1](idx: StaticIntTuple[rank]):
new_array._ptr.simd_store[simd_width](idx[0],
self._ptr.simd_load[simd_width](idx[0])|other._ptr.simd_load[simd_width](idx[0]))
elementwise[1, simdwidthof[dtype](), wrapper](self.numel)
return new_array
else:
print('Error: You can only AND int or bool arrays')
return self
fn __ior__(inout self, other: Self):
self = self.__or__(other)
fn __xor__(self, other: Self) -> Self:
if ~_mlirtype_is_eq[Scalar[dtype], Float64]():
let new_array = Self(self.numel)
@parameter
fn wrapper[simd_width:Int,rank:Int=1](idx: StaticIntTuple[rank]):
new_array._ptr.simd_store[simd_width](idx[0],
self._ptr.simd_load[simd_width](idx[0])^other._ptr.simd_load[simd_width](idx[0]))
elementwise[1, simdwidthof[dtype](), wrapper](self.numel)
return new_array
else:
print('Error: You can only AND int or bool arrays')
return self
fn __ixor__(inout self, other: Self):
self = self.__xor__(other)
Copy
6. Defined by built-in traits __len__() , __int__() DunderArray conforms to the Sized and Intable traits as shown here:
Mojo
struct DunderArray[dtype: DType = DType.float64](Stringable, Intable, CollectionElement, Sized):
Copy
This means DunderArray must implement __len__() and __int__() which lets you can call the len() and int() functions on DunderArray objects. For the purpose of illustration, we return the same values for both functions.
Usage:
Mojo
arr = DunderArray.rand(100)
print(len(arr))
print(int(arr))
print(arr)
Copy
Output:
Output
100
100
[0.8998 0.2015 0.4789 0.8032 0.8235 0.2892 0.9468 0.1380 0.7171 0.7669 0.2198 0.1612 0.1117 0.8888 0.6517 0.9204 0.9292 0.4855 0.5689 0.1464 0.7388 0.8918 0.3839 0.0385 0.2523 0.8308 0.7654 0.4705 0.9235 0.4045 0.8920 0.6080 0.2996 0.4123 0.7128 0.6077 0.4797 0.3032 0.1610 0.4581 0.2791 0.8645 0.4394 0.7279 0.8277 0.0262 0.2052 0.5736 0.1632 0.4053 0.2356 0.4852 0.6374 0.7196 0.5340 0.0912 0.6327 0.6552 0.0813 0.1537 0.6173 0.5354 0.2336 0.4306 0.2936 0.9323 0.6296 0.5276 0.7022 0.4943 0.9199 0.7379 0.9631 0.8317 0.1177 0.5522 0.6578 0.9034 0.7658 0.9795 0.5007 0.4396 0.8772 0.0373 0.0582 0.5582 0.1998 0.5465 0.6568 0.1957 0.3009 0.5186 0.7176 0.3793 0.6154 0.2856 0.7474 0.5119 0.5767 0.9709]
Length:100, DType:float64
Copy
Implementation:
Mojo
fn __len__(self) -> Int:
return self.numel
fn __int__(self) -> Int:
return self.numel
Copy
__str__() DunderArray also conforms to the Stringable trait, so we must implement the __str__() that returns a String that can be printed using the print() function. You've seen this in action throughout this blog post, and here you'll can see how it's implemented.
Usage:
Mojo
arr = DunderArray(1,2,3,4,5)
print(arr)
Copy
Output:
Output
[1.0 2.0 3.0 4.0 5.0]
Length:5, DType:float64
Copy
Implementation:
Mojo
fn __str__(self) -> String:
var printStr:String = "["
var prec:Int=4
for i in range(self.numel):
var val = self[i]
@parameter
if _mlirtype_is_eq[Scalar[dtype], Float64]():
var s: String = ""
let int_str: String
int_str = String(math.trunc(val).cast[DType.int32]())
if val < 0.0:
val = -val
let float_str: String
if math.mod(val,1)==0:
float_str = "0"
else:
float_str = String(math.mod(val,1))[2:prec+2]
s = int_str+"."+float_str
if i==0:
printStr+=s
else:
printStr+=" "+s
else:
if i==0:
printStr+=str(val)
else:
printStr+=" "+str(val)
printStr+="]\n"
printStr+="Length:"+str(self.numel)+","+" DType:"+str(dtype)
return printStr
Copy
Conclusion That's it folks, I hope you enjoyed this deep dive into all the dunder methods. To sum up, using dunder methods in Mojo🔥 provides a way to customize struct object behaviors. On GitHub you'll find both a Jupyter notebook that shows you how to use DunderArray and a Mojo file which implements the DunderArray .
If you're new to Mojo🔥 here are some additional getting started resources:
Until next time🔥!