Skip to content

Implement user defined types

Open
No due date
Last updated Feb 9, 2026

Classes & interfaces in Compose

Classes are defined with the class keyword.
Classes can contain fields, methods and associated functions. A function is a method if
it takes self as its first parameter.

Functions and fields are private by default and can be made public with the pub keyword.

pub class Point {
    pub x,
    pub y,

    pub fn origin() {
        new Point {
            x: 0, y: 0
        }
    }

    pub fn magnitude(self) {
        math.sqrt(self.x.pow(2) + self.y.pow(2))
    }
}

Instances of classes are created with the new keyword. All fields must be specified.

let p = new Point { x: 3, y: 4 };

Since all fields must be specified, this means classes with private fields can only be created within
their definition.

To allow a class with private fields to be initialised outside the definition, it must expose an associated (factory) function that returns an instance.
Fields can be accessed with field access syntax (.). Private fields can only be accessed within the type definition.

pub class PositiveInt {
    inner,

    /// Create a positive int from the passed in value.
    ///
    /// Panics if the value is a negative int, or of a different type.
    pub fn create(value) {
        match (value) {
            Int v if v > 0 => new PositiveInt { inner: v },
            Int v => panic("Cannot create a PositiveInt from an int <= 0: " + v.to_string()),
            other_ty => panic("Cannot create a PositiveInt from type: ", other_ty.ty().to_string()),
        }
    }

    pub fn get(self) {
        self.inner // accessing inner is allowed here
    }
}

let p = PositiveInt::create(2);
p.inner; // Error: Not allowed to access private field `inner` outside its definition

Associated functions are called with path access (::) on the type.

let origin = Point::origin();

Methods can be called with method calling syntax as well as associated function syntax.

let p = new Point { x: 3, y: 4 };
let mag1 = p.magnitude();
let mag2 = Point::magnitude(p);

Interfaces

Interfaces can be defined to support polymorphic patterns.

pub interface Drawable {
    fn draw(self);
}

pub class Point: Drawable {
    pub x, pub y,

    fn Drawable::draw(self) {
        // ...
    }
}

Interface methods can be called like regular methods:

let p = Point::origin();
p.draw();

Name conflict resolution

Take the following example:

pub class Point: Drawable, ColorDrawable {
    pub fn draw(self) {
        // plain method
    }

    fn Drawable::draw(self) {
        // Drawable interface method
    }

    fn ColorDrawable::draw(self) {
        // ColorDrawable interface method
    }
}

Calling point.draw() is ambiguous in this instance and will result in an error. This is resolved by using fully qualified names:

point.Point::draw();
point.Drawable::draw();
point.ColorDrawable::draw();
// or as associated functions
Point::draw(point);
Drawable::draw(point);
ColorDrawable::draw(point);

Note: the fully qualified names do need to be in scope.

  • fn method(self) defines a plain class method
  • fn Interface::method(self) defines an interface method
  • Plain methods do not automatically satisfy interface requirements
  • Interface methods do not shadow plain methods
  • Conflicts can be resolved by using fully qualified names.

Extension implementations

Sometimes it makes sense to implement methods and associated functions outside the initial definition.
To accomplish this Compose allows extending types.

To extend a type, make sure it is imported and then create an extend block:

pub extend Point as PointTransformationExt {
    fn scale(self, factor) {
        // ...
    }
}

This extension then becomes available on instances of Point. To use these extension methods from other
modules the extension first needs to be imported:

import "point_extensions.cmps": PointTransformationExt;

let point = new Point { x: 1, y: 2 };
point.scale(2);
assert::eq(point.x, 2);
// or as an associated function
PointTransformationExt::scale(point, 2);

Extending types with interface implementations

Sometimes it makes sense to implement an interface for a type outside its definition.
Albeit implementing an interface for a type you don't own, or simply for organisation.

Extension blocks can implement interfaces for other types:

pub extend Point: Drawable as DrawablePointExt {
    fn draw(self) {
        // ...
    }
}

let point = new Point { ... };
point.draw(); // if there are no name conflicts
// or
point.DrawablePointExt::draw(); // fully unambiguous

Interface witnesses

Because Compose supports implementing interfaces multiple times for types, disambiguation for which
implementation to use when a type erased value is passed to a function is needed.

pub fn use_drawable(drawable) {
    drawable.draw();
}

pub class Point: Drawable {
    fn Drawable::draw(self) { "standard" }
}

pub extend Point: Drawable as FancyDrawablePointExt {
    fn draw(self) { "fancy" }
}

pub extend Point: Drawable as BasicDrawablePointExt {
    fn draw(self) { "basic" }
}

let p = new Point;
use_drawable(p); // Which implementation should use_drawable pick?

In this case an interface witness can be used to specify which implementation to use.
If the type itself implements an interface, and no witness is specified the base implementation will
be used.

use_drawable(p as FancyDrawablePointExt); // "fancy"
use_drawable(p as BasicDrawablePointExt); // "basic"
use_drawable(p): // "standard"
0% complete

List view

    There are no open issues in this milestone

    Add issues to milestones to help organize your work for a particular release or project. Find and add issues with no milestones in this repo.