February 26, 2024

What are dunder methods? A guide in Mojo🔥

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:

  1. Initialize, copy, move, delete
  2. Getters and Setters: obj[idx], obj[idx]=val
  3. Unary arithmetic operators: -obj, +obj, ~obj
  4. Comparison operators: <, <=, ==, ~=, >, >=
  5. Normal, reversed and in-place arithmetic operators: +, -, *, @, /, //, %, ** , <<, >>, &, |, ^
  6. 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)

Output:

Output
-2.5

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:

  1. When used between a scalar and a vector
  2. 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:

  1. DunderArray is an array data structure that is parameterized by a type (Float64, Float32, Bool, etc.)
  2. DunderArray implements all Mojo dunder methods shown in the figure above.
  3. DunderArray also implements several dunder methods defined by built-in Traits

Using 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()

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 …

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)
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
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]

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)
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)

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()
Output:
Output
error: Expression [21]:6:11: use of uninitialized value 'arr' print(arr) #<-- ERROR: use of uninitialized value 'arr' ^
Implementation:
Mojo
fn __moveinit__(inout self, owned existing: Self): self._ptr = existing._ptr self.numel = existing.numel existing.numel = 0 existing._ptr = DTypePointer[dtype]()

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()

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])
Output:
Output
4.0
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)

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)
Output:
Output
[1.0 2.0 3.0 100.0 5.0] Length:5, DType:float64
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)

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)
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
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)

__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)
Output:
Output
[False True False] Length:3, DType:bool
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

4. Comparison operators

__lt__(), __le__(), __eq__(), __ne__(), __gt__(), __ge__()

DunderArrays 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)
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
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]

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 DunderArrays 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)
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
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

__sub__(), __rsub__(), __isub__()

Similarly, you can subtract DunderArrays 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)
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
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

__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)
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
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

__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 DunderArrays:

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)
Output:
Output
[0.7615] Length:1, DType:float64 [0.7615] Length:1, DType:float64 [0.7615] Length:1, DType:float64
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

__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)
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
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

__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)
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
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)

__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)
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
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)

__pow__(), __ipow__()

DunderArrays 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)
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
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

__lshift__(), __rshift__(), __ilshift__(), __irshift__() 

DunderArrays 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)
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
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)

__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)
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
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)

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):

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)
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
Implementation:
Mojo
fn __len__(self) -> Int: return self.numel fn __int__(self) -> Int: return self.numel

__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)
Output:
Output
[1.0 2.0 3.0 4.0 5.0] Length:5, DType:float64
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

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🔥!

Shashank Prasanna
,
AI Developer Advocate

Shashank Prasanna

AI Developer Advocate

Shashank is an engineer, educator and doodler. He writes and talks about machine learning, specialized machine learning hardware (AI Accelerators) and AI Infrastructure in the cloud. He previously worked at Meta, AWS, NVIDIA, MathWorks (MATLAB) and Oracle in developer relations and marketing, product management, and software development roles and hold an M.S. in electrical engineering.