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.