When I started learning Raku a couple years back, one of the first features that stuck out to me was its type system. This is one I feel gets overlooked at times. I found this to be rather difficult to wrap my head around at first, but I found that relying on strict typing can lead to simpler, more robust code that can better cope with changes as time goes on. I’ll be using chess to demonstrate this, but there are some fundamentals to cover first.
Introspecting Types and Kinds
The type of any object in Raku can be introspected with WHAT
:
say 42.WHAT; # OUTPUT: (Int)
say WHAT 42 | 24; # OUTPUT: (Junction)
Though we’re often more interested in its type’s name than the type object itself when we want to do this:
say 42.^name; # OUTPUT: Int
Typechecking is among the behaviours defined by an object’s HOW
:
say 42.HOW.^name; # OUTPUT: Perl6::Metamodel::ClassHOW
In type theory jargon, this would be a kind, or a type of type.
Runtime Typechecking
Occasionally, there comes a time when you need to typecheck objects manually, such as when debugging. Smartmatching against a type object will perform a typecheck by default:
say 42 | 24 ~~ Junction; # OUTPUT: True
say 42 | 24 ~~ Int; # OUTPUT: True
However, we’re smartmatching; ~~
can have any behaviour depending on what how the RHS’ ACCEPTS
method behaves. While this can allow for smartmatching of junctions of objects against type objects for instance, sometimes a more literal typecheck is needed. Metamodel::Primitives.is_type
will do in those cases:
say Metamodel::Primitives.is_type: 42 | 24, Junction; # OUTPUT: True
say Metamodel::Primitives.is_type: 42 | 24, Int; # OUTPUT: False
Typing Variables
Types may optionally be provided for any variable, parameter, or attribute, generally as a prefix to the variable name. For $
-sigilled variables, this denotes its value’s type; for @
-sigilled variables, this denotes the type of the list’s values; for %
-sigilled variables, this denotes the type of the hash’s values; for &
-sigilled variables, this denotes the type of the routine’s return value. With %
-sigilled variables in particular, an additional key type may be given in curly braces after the variable name:
my Int $x = 0;
my Str @ss = <sup lmao>;
my Num %ns{Str} = :pi(π);
my True &is-cool = sub is-cool(Cool $x --> True) { };
Alternatively, value types may be specified using the of
trait:
my $x of Int = 0;
my @ss of Str = <sup lmao>;
my %ns{Str} of Num = :pi(π);
my &is-cool of True = sub is-cool(Cool $x --> True) { };
Wait a minute, how is True
a valid type? True
being a Bool
enum value, it’s a Cool
constant that can be used like a type with ACCEPTS
semantics. String and numeric literals also fall under this category. We give one as a return value here, so we don’t need to return anything from the routine explicitly, as it will always return True
.
Definitely Typing Variables
Raku offers a way to restrict values of a type based on their definiteness using type smileys, which are placed after the type of a variable:
my Int:U $type; # Contains an Int type object by default.
my Int:D $value = 42;
my Int:_ $nullish;
$nullish = $value;
The :U
smiley denotes an undefined type object; the :D
smiley denotes defined values (or instances); the :_
smiley denotes either/or.
When no type smiley is given for a type, they will use :_
semantics by default. This can be customized using the variables
and attributes
pragmas (parameters
is NYI). For instance, types without smileys can be made definite to give variable typings behaviour more akin to Haskell’s like so:
use variables :D;
use attributes :D;
Nil
is an exception when it comes to typechecking of :U
and :_
variables. This cannot be bound to a variable typed with these smileys, but when assigned or returned from a routine with them, you’ll wind up with its type object instead of Nil
itself. Failure
also being a nullish type, failures can be returned from routines of other :U
/:_
typings.
Definite types can help prevent common, annoying runtime errors related to treating type objects like instances or vice versa. For example, you’ve probably seen this sort of warning before:
put my $warning; # OUTPUT:
# Use of uninitialized value $message of type Any in string context.
# Methods .^name, .raku, .gist, or .say can be used to stringify it to something meaningful.
# in block <unit> at -e line 1
Emitting this warning is Mu
‘s default behaviour for Str
coercions of type objects. Giving variables a :D
typing can help prevent it from appearing.
Grouping Data with Classes
If we were to represent a chess piece as data, we might have a colour and a type of piece. The question is how do we type this? We might have an Array:D
of Str:D
to start:
my Str:D @piece = 'white', 'pawn';
my Str:D $colour = @piece[0];
my Str:D $type = @piece[1];
0
and 1
don’t make good names for colours and types though. If we were to group data of mixed types, we would be stuck typing the entire thing with a less specific typing than we intend for them to have. We want types! We can bundle our data together in a class to type all of this:
class Chess::Piece {
has Str:D $.colour is required;
has Str:D $.type is required;
}
$.colour
and $.type
are public attributes of the Chess::Piece
class. Attributes are declared in the has
scope, with public attributes having the .
twigil and private attributes having the !
twigil. Because we have :D
typings for these, they either must be required or have a default value. We can’t assume anything about their values at this point, so we mark these as required.
As in a traditional object-oriented language, a class can be constructed with data to produce a value. The new
method is the default method we can use to do this, which accepts named parameters corresponding to the class’ public attributes:
my Chess::Piece:D $pawn = Chess::Piece.new: :colour<white>, :type<pawn>;
My pawn chess piece is a chess piece… because we have Chess::Piece
as a type for $pawn
, there is sugar for assignments based on method calls we can use to make this less redundant:
my Chess::Piece:D $pawn .= new: :colour<white>, :type<pawn>;
my Str:D $colour = $pawn.colour;
my Str:D $type = $pawn.type;
We can access the public attributes of $pawn
with syntax similar to method calls. In fact, these are method calls. Raku doesn’t differentiate between public, private, or protected data when it comes to classes; it’s always private. What we call public attributes are really private attributes with an automatically generated getter method.
Chess pieces can exist within a square on a chess board, which comes with a colour. That might be a class too:
class Chess::Square {
has Str:D $.colour is required;
has Chess::Piece:_ $.piece is rw;
}
my Chess::Square:D $a1 .= new: :colour(Black);
$a1.piece .= new: :colour(White), :type(Rook);
We give Chess::Square
an rw
$.piece
attribute. is rw
is a trait that makes the getter for a public attribute more of a combination of a getter and a setter, allowing for assignments to the attribute to be made from outside of Chess::Square
.
We will need another class for the chess board itself. This will keep track of a grid of squares:
class Chess::Board {
has Chess::Square:D @.squares[8;8];
submethod BUILD(::?CLASS:D: --> Nil) { ... }
}
This wraps a @.squares
multidimensional array with 8 ranks (rows) of 8 files (columns). We stub a BUILD
submethod that will initialize @!squares
once the board has been constructed. As a submethod, this will not get inherited by any potential subclasses. As a ...
stub, this will fail when called (we’re not quite ready to implement this yet).
Typically we want methods, not submethods. Public methods can be declared by using the method
routine declarator instead of submethod
; private methods are declared the same way as public methods, but have a name prefixed with !
; there are no protected methods in Raku.
Within the scope of a class, we will always have $?CLASS
and ::?CLASS
symbols that act as aliases for it. ::?CLASS
can be used as a typing, while $?CLASS
would be preferable in other contexts. The type that comes before the :
in BUILD
‘s signature is an invocant typing. This is a parameter like any other, but we don’t give it a name because there already is a default symbol for it that’s usually good enough: self
.
Closing Sets with Enums
We represent chess piece colours and types as strings at the moment. Strings are for text. In representing them this way, any text can be given as a colour or type, but we can can only have black or white as a valid colour for a chess piece, and only pawns, rooks, bishops, knights, queens, and kings as types. In other words, we get values a human can understand, but a computer doesn’t interpret them how we’d like it to here. We can better represent these with enums:
enum Chess::Colour <White Black>;
enum Chess::Type <Pawn Rook Bishop Knight Queen King>;
class Chess::Piece {
has Chess::Colour:D $.colour is required;
has Chess::Type:D $.type is required;
}
class Chess::Square {
has Chess::Colour:D $.colour is required;
has Chess::Piece:_ $.piece is rw;
}
my Chess::Square:D $a1 .= new: :colour(Black);
$ai.piece .= new: :colour(White), :type(Bishop);
Now only the colours and types of chess pieces we intend to have can be used.
By default, an enum will have its index in the enum’s list of values as its value, alongside a key with the enum value’s name:
say White; # OUTPUT: White
say White.key; # OUTPUT: White
say White.value; # OUTPUT: 0
As for enum values themselves, they will be instances of their enum type, and will always be equivalent to their value, yet they will come with an Enumeration
typing as well:
say White == 0; # OUTPUT: True
say White ~~ Enumeration:D; # OUTPUT: True
The Enumeration
type is what causes White
to get output as White
by &say
instead of 0
, for instance.
The default index values works fine in the case of chess piece colours and types, but not all cases. Enums can have any type of value, though its values can only be instances as of writing. This is inferred from the enum’s values by default, but may be denoted explicitly when a scope declarator (my
, our
, unit
, etc.) is given:
my Int enum Chess::Colour <White Black>;
Constraining Types and Values with Subsets
When moving a chess piece, we will need to take tuples of its file and rank (indices in Chess::Board
‘s @.squares
) as parameters for a routine somewhere along the way. We could represent these with Int:D
in a Chess::Position
class:
class Chess::Position {
has Int:D $.file;
has Int:D $.rank;
}
But as with colours and types, this is overly broad; we only want to allow integers from 0-7. We could maybe represent files with an Int
enum of a-h, but we don’t have any symbols to give ranks. We can use a subset to constrain valid values for either to the range we want instead:
subset Chess::Index of Int:D where ^8;
class Chess::Position {
has Chess::Index $.file is required;
has Chess::Index $.rank is required;
}
This constrains Int
to a range of 0-7 via a runtime typecheck. Here, Int:D
is our subset’s refinee, and ^8
is its refinement. Smartmatching against Chess::Index
will first typecheck the LHS against its refinee, then smartmatch the LHS against the refinement should that succeed. While I’d normally include a type smiley with a type, writing Chess::Index:D
is redundant when the refinee already includes :D
.
We can write this subset in a more ad-hoc way using the where
clause. This variable declaration is roughly equivalent to a Chess::Index
-typed variable:
my Int:D $file where ^8 = 0;
However, when typed with a bare where
clause in lieu of an explicit subset like this, we will wind up with a less readable exception should a typecheck fail.
Wrapping Up
So far, we have a handful of types for chess. While we have type-safe representations of state associated with them, we have little in the ways of behaviour implemented. We don’t implement any at this point because our types are flawed; our dependence on classes will complicate the code we wind up with in the end. We focus on the more restrictive aspects of Raku’s type system first so we can fully take advantage of the more liberating aspects, which will be the focus of the next part.
2 thoughts on “Day 18: Typed Raku, Part 1: Taming State”