One of the prominent features of the Raku language is its extensive type system. This doesn’t mean the vast number of the standard classes and roles but it is about the system where these two are not the only kind of types allowed. Is it still unclear what we’re talking about? Then let’s have a look at a fundamental concept of the metamodel.
On The Surface
The first things that pop up in minds of OO programmers would be classes and, next to them, roles1. I’m not even sure if a role is even considered being a type in every language where the concept exists. It would be interesting to hear from the readers, who are better familiar with the area.
A newbie could be confused at first to find out that in Raku Foo:D()
isn’t a class Foo
with some syntax bells and
whistles attached, but a type object on its own. And that trying something like Foo:D().new
would result in an
exception thrown because this particular type cannot produce concrete objects.
Let’s list some type kinds we have, just of the top of my head:
- class
- role
- enum
- subset
- type smiley
- coercion
What is common among them? The fact that any object can be typematched against them. And this is basically it. Can we
instantiate a type? I.e. can we create a concrete object out of it? The apparent “yes” goes for
classes2. One could argue that role Foo {}.new
would create an instance of Foo
, but
in reality this line hides auto-punning behind it, meaning
that we end up having a class Foo
which we then instantiate.
For the remaining type kinds instantiation would simply fail3.
It’s hard to tell what other publicly visible properties we can use to show the differences between the kinds. All one needs to know is what behaviors they provide and care about nothing else otherwise.
Categorizing Independently
The one who does care is the compiler. Because it needs to know exactly what does it deal with, can a type be used here or there, which “buttons” are to be pushed to get what a user is asking for. This where a question pops up: how to let the compiler do its job in a best possible way?
To complicate the situation a bit let’s not forget that Raku’s metamodel allows a developer to create their own type
object by introducing a new metamodel class4. Therefore the most straightforward approach where we just
take the metamodel object and check what type is it would only work for the limited set provided by the CORE itself.
Forget about an independent type – inherit from the existing Metamodel::ClassHOW
, Metamodel::SubsetHOW
, etc.
There is another solution which comes to mind and which is looking very promising at the first glance: we introduce roles that would be responsible for marking or even implementing certain properties of the metaobjects. Unfortunately, there is at least one case where this solution would complicate the code required to handle a type object. This isn’t a big problem if we consider the compiler alone, but the run-time is in the risk zone too because on many occasions it has to solve similar tasks. It’s too early to get into the details now, but we will return to this matter later when it comes to nominalizables.
Raku’s answer to these challenges is the archetypes concept. It could be wound down to the following definition: archetypes is a set of flag-style properties where raising a flag on a metamodel object signals about it’s ability to implement corresponding functionality.
Technically archetyping is implemented by
Metamodel::Archetypes
class, which
is a container for a few boolean attributes – one per each archetype. Corresponding methods provide read only access for
the attributes:
say class Foo {}.^archetypes.nominal; # 1
say (subset Bar of Any).^archetypes.nominal; # 0
By using them the compiler (or run-time code) doesn’t need to know if a type in a certain context has this or that metaclass behind it as often it’s OK to know if the class has appropriate archetype.
Available Archetypes
Let’s get to the point and enumerate the archetypes we have. As a matter of fact, this is what this text is created for in the first place!
nominal
The most simple and straightforward archetype which barely needs much explanation.
Part of being a nominal means that a value can be produced with this type. At the first glance it is not applicable to
roles, but since one can always do Role.new
things start making sense again5.
generic
A generic type is the one which serves as a placeholder where the concrete type is not known at the compile time. Generics are useless as such and any attempt to directly use them as method invocators most often ends up with some kind of error.
The form of generic that’d be immediately familiar is a type capture:
sub foo(::T) {
say T.^name;
}
foo(Bool); # Bool
In this example when foo
is called our runtime instantiates the generic type T
with Bool
. Many entities of the
Raku language are capable of generic instantiation by implementing instantiate_generic
method6. The
process itself is sometimes tricky and has a number of pitfalls. A couple of them still remain unresolved,
unfortunately.
In a day to day activities it should be nearly to impossible to cross ways with a generic type wandering free in the
wild. But we can entrap one by modifying the sub foo
a little:
sub foo(::T, T $v) {}
say &foo.signature.params[1].type.^name; # T
say &foo.signature.params[1].type.HOW.^name; # Perl6::Metamodel::GenericHOW
There is only one specific requirement to a generic: its metaobject must have instantiate_generic
method.
coercive
Type objects of this archetype must be capable of coercing values into instances of other types. The primary requirement
to the coercive metaclasses is to implement method coerce
.
definite
Type objects of this archetype are representing definedness of a value. In rakue these are type smiley, created with
either :D
or :U
.
nominalizable
Types of this kind cannot produce values but they know how to produce a nominal type. The nominalizables Raku provides
out of the box are the smileys, subsets, and coercions. By simplifying things down, we can say that all three are kind
of wrappers around other types. Say, Str:D
is a definite wrapper over Str
.
The other property they have in common is implicitly described in the previous paragraph. Have you noticed that “other types” locution has been used instead of “nominal types”? This is because Raku allows to wrap one nominalizable into another like in the following case:
subset Foo of Int();
say Foo:D.^base_type; # (Foo)
This complicates compiler’s and runtime code quite noticeably. In many cases it is insufficient to know that a definite represents a subset, but very important not to miss the coercion deeper inside it! Finding this out would require a number of checks to be performed:
- is a type nominalizable?
- what kind of nominalizable it is?
- since we now know how to get its wrapee then repeat from step 1.
Also take into account that these checks are to be done not once somewhere and by the compiler alone, but in many places and often times by the run-time code too.
The solution for this is nominalizable transparency where properties of underlying types are available on their wrappers. Surely, it applies to the archetypes too:
say Foo:D.^archetypes.coercive; # 1
Remember though that not every archetype is subject for nominalizable transparency. It only works with coercive
,
definite
, and generic
because these affect analysis of nominalizables.
Let’s tweak the example from the generic
section above and observe these mechanics in action:
sub foo(::T, T:D() $v) {}
my $type = &foo.signature.params[1].type;
say "NAME : ", $type.^name; # NAME : T:D(Any)
say "Coercive: ", $type.^archetypes.coercive; # Coercive: 1
say "Definite: ", $type.^archetypes.definite; # Definite: 1
say "Generic : ", $type.^archetypes.generic; # Generic : 1
At this point it should be more clear as to why the archetypes concept works better than the idea with roles, mentioned above. But I’ll leave it for the concluding section of this article.
inheritable
A type object possesses this archetype if it can be inherited from. In the Raku core we mean classes and native types here.
inheritablizable
Whenever a type cannot be inherited from but it knows how to produce something inheritable
. Think of roles in first
place: a role can produce a pun of itself which can be subclassed.
composable
Let me simply quote the comment to this archetype:
# Can this be composed (either with flattening composition, or used
# as a mixin)?
First of all, don’t associate the term “composed” with the method compose
7, that can be found on each
HOW
-class. Being composable
doesn’t mean “to have the method compose
”. There is a relation, but it is not that
direct.
Bearers of this archetypes are expected to be capable of serving as a source of methods, and attributes available for adoption by other types. Think of roles, in first place: any composable must behave well anywhere where otherwise a role would be used.
The word “flattening” in the above citation could be confusing at first. It means that when roles are composed into
another type the lists of methods and attributes for adoption are getting flattened down. No exception made for
indirectly consumed roles, like in role R1 does R2 does R3 {}
case where R2
and R3
are the indirect ones.
composalizable
This archetype cannot be composed in directly. But it knows how to produce something that can. We currently have only one such type: enumeration.
enum Maybe <No Yes Dunno>;
class Foo does Maybe { }
say Foo.^roles; # ((Maybe))
my $foo = Foo.new: Maybe => Dunno;
say $foo.No; # False
say $foo.Yes; # False
say $foo.Dunno; # True
Unless I’m missing something, for this archetype the only requirement is to have method composalize
on the metaobject:
say Maybe.^composalize.HOW.^name; # Perl6::Metamodel::ParametricRoleHOW
parametric
This is another kind of ‘incomplete type’, in “addition” to the generic
above. But contrary to the latter, parametrics
are types on their own. The big difference is that sometimes parametrics may require additional bits before it can be
used. Though sometimes they may not.
The most familiar kind of incomplete parametric is, once again, a role:
role R[::T] {}
But even non-parameterized version of it role R {}
is still a parametric type.
augmentable
This marks types that can be augmented. There is barely a lot to be added to this definition.
Let’s Wrap It Up
To be honest, in the past it took me some time before I got to understanding the concept of archetyping. Though no wonder because it was my first year with Raku in general. But the memories of how baffling it used to be are still alive. Hopefully, the information here would help somebody spend less time roaming over the source code while deciphering the intentions of its creators.
Just one topic I’d like to get back to before we call it a day.
It is possible that the example of nominalizables is not fully convincing and the idea of dynamic mixing in of corresponding archetype-representing roles into the metaobject would still look appealing. Here is how I see it: perhaps it would work, but doesn’t necessarily makes sense.
I never spent enough time to mind-model this case. If there is sufficient bravery in someone to give this approach a try – I’d be interested in seeing the experiment outcomes. And if they are great – don’t blame the inventors of the current approach. Look at the historical context: back then Perl6 was under-optimized. A lot of operations that we consider rather cheap nowadays, including type matching which got significant gains with the introduction of new-disp, were major performance killers back then. It’s hard to recall now where I read about it, but mixins were among those costly ops.
-
Or interfaces, or whatever other terms languages can use. ↩
-
One could point at
Nil
which can’t be instantiated becauseNil.new =:= Nil
. But this is just a trickNil
’snew
method does. It is currently possible to create an instance ofNil
withbless
orCREATE
methods. Though I’ve no idea what use could it has. ↩ -
For enums it is also possible to do
enum FOO <...>; say FOO.bless.WHICH;
. But this would be about punning too and is likely to produce something rather unexpected at first. Yet, we have instances of enums in a form of pre-existing objects, but there is no legal way to create one manually. ↩ -
And, perhaps, some new syntax to use it. But this subject goes far beyond this article’s scope. ↩
-
Even though it’s done via punning. ↩
-
Please, tell no one I shared this method name with you! It’s undocumented, and unspecced, and not guaranteed to stick around forever. ↩
-
Will be discussed in the next article of the series. ↩
Comments