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 = type「vector」 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 = type「vector」.with_ends( 3, 8 ) var q = type「vector」.with_angle( 0.5, 2 ) var r = type「vector」() |
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 = type「vector」 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 = type「vector」 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, type「vector」.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 = type「vector 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.





