My first steps with Mojo: a simple implementation of the delegation pattern

Photo by cottonbro studio: https://www.pexels.com/photo/close-up-shot-of-a-hand-pressing-an-elevator-button-8453040/

Introduction

Some time ago I heard about new nascent pythonic language called Mojo which apart from being faster, according to their website much faster, was also safer. This of course piqued my curiosity, and I decided to have a go implementing a simple design pattern.

Although Mojo is still in its early stages, in this example I am using version 0.7.0, it shows promise. It has the easy syntax of Python combined with a somewhat Rust-like ownership system, and of course type-safety.

The example I am going to present to you is very simple on purpose. First of all I am still learning the language, and yes, the language is not yet finished. Since now windows implementation exists yet, I using mojo on the Windows Subsystem for Linux(WSL)

Introduction to delegation

Delegation is like passing a job to someone else. In Mojo will do this by using composition. It’s almost like asking one object to do a task for another object, just like a subclass asking its parent class for help. Here you can find an article explaining this in more detail.

Implementation in Mojo

Let’s say we want to create a windowing system. We have a basic window that can open and close. We want to add more features to it, like rotating the window.

We will start by defining a Windowable trait:

trait Windowable:
    fn open(self):
        ...
    fn close(self):
        ...
    fn rotate(self,degrees:Int16) raises:
        ...

Some notes:

  1. The three dots are part of the syntax, signifying that there is no implementation of the method
  2. The rotate() method has a raises keyword, which means it might raise an exception, for example when the window can not be rotated.

Next we will implement a simple RotatableWindow:

@value
struct RotatableWindow(Windowable):
    var title:String

    fn __init__(inout self,title:String):
        self.title=title

    fn open(borrowed self):
       print("Opening window with title "+self.title)

    fn close(borrowed self):
       print("Closing window with title "+self.title)
    
    fn rotate(borrowed self,degrees: Int16) raises:
        self.get_delegate().rotate(degrees)
    
    fn get_delegate(borrowed self)->ExtendedWindow:
        return ExtendedWindow(self)

Some notes:

  1. We have to implement every method in the Windowable trait
  2. Rotating the window is delegated to an ExtendedWindow struct which we will define later.
  3. The @value decorator synthesizes some methods for use, like __copyinit__ giving our struct value types semantics.
  4. Also note that in the trait-methods, we use the borrowed keyword, since we are not mutating state. Keywords like inout and borrowed make the code easier to reason about and when used appropiately safer and more stable.

Next we come to the ExtendedWindow struct:

struct ExtendedWindow(Windowable):
    var window:RotatableWindow

    fn __init__(inout self,window:RotatableWindow):
        self.window=window

    fn __copyinit__(inout self,existing:ExtendedWindow):
        self.window=existing.window

    fn rotate(borrowed self,degrees:Int16) raises:
        print("Rotating window by "+str(degrees)+" degrees")
        
    fn open(borrowed self):
        self.window.open()

    fn close(borrowed self):
        self.window.close()

Some remarks here:

  1. This also implements the Windowable trait, however, it delegates many method calles to the underlying RotatableWindow instance.
  2. What it does implement itself is the rotate() method.

As a complement to our RotatableWindow struct, we also implement a NonRotatableWindow struct:

struct NonRotableWindow(Windowable):
    var title: String

    fn __init__(inout self,title:String):
        self.title=title

    fn open(borrowed self):
       print("Opening window with title "+self.title)

    fn close(borrowed self):
       print("Closing window with title "+self.title)
    
    fn rotate(borrowed self,degrees: Int16) raises:
        raise Error("Can not rotate window with title: "+self.title)

This is more or less the same as the RotatableWindow with the main difference being the fact that the rotate() method raises an error.

We also need to define this function, outside of a class:

fn try_to_rotate_window[T:Windowable](w: T,degrees:Int16):
    try:
        w.rotate(degrees)
    except e:
        print("Error: "+str(e))

Some notes:

  1. This method is a generic method, getting an object implementing a Windowable trait as one of its parameters.
  2. In the try-except block we try to rotate the window, and if it does not succeed we print out an error.

Testing time

Now we can test our setup:

fn main():
    let rotatable_window=RotatableWindow("My window")
    rotatable_window.open()
    try_to_rotate_window(rotatable_window,45)
    rotatable_window.close()

    let non_rotatable_window=NonRotableWindow("Non rotatable")
    non_rotatable_window.open()
    try_to_rotate_window(non_rotatable_window,45)
    non_rotatable_window.close()

We create both a RotatableWindow and a NonRotatableWindow, open them, try to rotate them, and close them again. As you will see in the output, we get an error on the NonRotatableWindow

Conclusion

The Mojo language is still in its early stages, and has some rough edges, which I know will be sorted out if I look at the roadmap. However, implementing this pattern was relatively painless, and quick. The type-safety and traits make the code easy to read.

Furthermore I can say, that although Mojo primary purpose is AI and ML, subjects of great interest to me, I think that it can play an important role in general programming: it has almost the productivity of Python, yet it can compete with Rust when it comes to performance.

Leave a Reply

Your email address will not be published. Required fields are marked *