Day 18: Typed Raku, Part 1: Taming State

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

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.