The Life of a Programmer

Working with types and named constructors in Leaf

As types are a keystone in Leaf, working with them must be fluid and simple. I recently improved this area by adding named constructors and bare type names. The results are interesting.

1
2
3
4
5
6
7
8
var s = vector()

var t = typevector
var p = t.with_ends(3,5)

var rt = vector
var x = rt.with_angle
var q = x(0.5, 3)

Named constructors

Named constructors are what set me down this path. I noticed a lot of my non-Leaf code is littered with static functions that construct an object. I wanted to get this behavior into the core syntax.

This vector class demonstrates one use for named constructors:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class vector {
    var x : float
    var y : float

    defn with_ends = ( x : float, y : float ) -> construct {
        this.x = x
        this.y = y
    }

    defn with_angle = ( r : float, len : float ) -> construct {
        this.x = math.cos(r) * len
        this.y = math.sin(y) * len
    }

    defn default = -> construct {
    }
}

Previously only a default constructor was available. I’ve left it in here so we can still create default initialized vector types. I’ll revisit later whether there should be a special syntax to keeping or excluding the default constructor.

I needed a way to call these constructors so I introduced type:

1
2
3
var p = typevector.with_ends( 3, 8 )
var q = typevector.with_angle( 0.5, 2 )
var r = typevector」()

I wrote the syntax before the implementation, leaving the question of what type actually is. I’ll get back to the redundancy a bit later.

type「」

Defining what type is requires us to go a bit into the confusing side of compilers and type systems. At first I implemented type as a special syntax to construct a class. It worked with default constructors, but didn’t expand well to named constructors. It was also quite inflexible.

I changed type to have an actual value. This would allow something like type「vector」 to exist on its own, without having to call it. It also meant I could add a .name property to describe the type. Being a value also meant it could be assigned.

1
2
3
4
var t = typevector
var p = t.with_ends( 3, 8 )

std.print([t.name,"\n"])

This may look like dynamic type construction, but it’s not. The trick is in what the inferred type of t is. It’s where the naming gets circular, t is of type type「vector」. The type token here is distinct from the type token used in the expression: one is a type name, the other is an expression keyword. Understanding this distinction, or what is actually happening here, won’t be necessary for most users of Leaf.

Since t is of type type「vector」 it’s easy for the compiler to understand t.with_ends. It’s resolved statically; there is no runtime resolution on the value of t. Objects of this type don’t even have a real value: they are zero-length.

Constructors are still functions

Look again at the syntax for a constructor:

1
2
3
4
defn with_ends = ( x : float, y : float ) -> () construct {
    this.x = x
    this.y = y
}

Other than the construct flag these are normal functions in the class. This means the following is also possible:

1
2
3
var t = typevector
var x = t.with_angle
var q = x( 0.5, 3 )

It again works without any kind of runtime resolution. t and x are typed strongly enough that the compiler knows exactly what to do at x( 0.5, 3 ).

As there is still no runtime value involved here, dynamic construction is not possible. The type type also has no common base class, so it’s not possible to convert to a more fundamental type. It can still be used as a parametric argument though:

1
2
3
4
defn tuple_init = ( x:float, y:float, t ) -> {
    return t(x, y)
}
var n = tuple_init( 1, 2, typevector.with_ends )

It’s more of just a curiosity here, as this tuple_init function is kind of pointless. The feature can be used to provide advanced parametric functions, container class support, as well as things like object pool. This aspect is not a special feature, it just naturally arises from how I implemented the rest of the type feature.

Removing the type「」 part

I don’t think anybody will be too exited about coding type「vector」. It’s a bit bulky.

Why do I need the type part at all? It’s because vector is not a name in the expression scope. It exists solely as a type name. The type「」 syntax switches into the type namespace, allowing us to find the vector name.

It goes beyond just the name: it’s the full type syntax at this point. You could also create a shared vector with this syntax:

1
var q = typevector shared.with_ends(1,5)

Sure, that’s great, but what if I just want a vector, which is a very common case:

1
var q = vector.with_ends(1,5)

To get this working I merged the type names into the normal scoped namespace. Only the intrinsic type name is a symbol though, so vector works, but vector shared doesn’t — which also couldn’t work due to parsing issues on the type syntax.

This helped clean up a big question mark in the compiler code. The type parser previously had a special lookup to add the core types, like integer. These are now proper scope symbols.

Please join me on Discord to discuss, or ping me on Mastadon.

Working with types and named constructors in Leaf

A Harmony of People. Code That Runs the World. And the Individual Behind the Keyboard.

Mailing List

Signup to my mailing list to get notified of each article I publish.

Recent Posts