In the previous part, I claimed that types can allow for more fluid, robust code, then wrote a bunch of restrictive types for chess that won’t allow for this to occur:
subset Chess::Index of Int:D where ^8;
class Chess::Position {
has Chess::Index $.file is required;
has Chess::Index $.rank is required;
}
enum Chess::Colour <White Black>;
enum Chess::Type <Pawn Bishop Rook 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;
}
class Chess::Board {
has Chess::Square:D @.squares[8;8];
submethod BUILD(::?CLASS:D: --> Nil) { ... }
}
Branching with Multiple Dispatch
There’s a key concept we need to understand before we can fix the types we wrote, but we can’t use chess as an example without fixing the types first. Instead, we’ll use a small helper routine from my Trait::Traced
module as an example.
When rendering prettified trace output, objects that can appear in a trace can be rendered in a few different ways: an exception will be rendered as a red exception name, a failure will be rendered as a yellow exception name, and anything else will be rendered as its gist. We can write a &prettify
routine with conditions based around the type of a value parameter, which we’ll assume is Any:_
:
sub prettify(Any:_ $value --> Str:D) {
if $value ~~ Exception:D {
"\e[31m$value.^name()\e[m"
} elsif $value ~~ Failure:D {
"\e[33m$value.exception.^name()\e[m"
} else {
$value.gist
}
}
We have conditions based on smartmatching semantics that affect the value we wind up with, so maybe we have when
s, not if
s. We’ll rewrite this with the given
/when
pattern:
sub prettify(Any:_ $value --> Str:D) {
given $value {
when Exception:D { "\e[31m$value.^name()\e[m" }
when Failure:D { "\e[33m$value.exception.^name()\e[m" }
default { $value.gist }
}
}
We’re writing a routine that can potentially get called for every single traced event that occurs in the duration of a program; that given
block introduces an extra scope that we don’t need, which comes with overhead that is unacceptable in this case. Because we already have a block to work with (&prettify
itself), if we rename $value
to $_
, we can get rid of that:
sub prettify(Any:_ $_ --> Str:D) {
when Exception:D { "\e[31m$_.^name()\e[m" }
when Failure:D { "\e[33m$_.exception.^name()\e[m" }
default { $_.gist }
}
But this doesn’t read very well. When we have conditions based around the type of a routine’s parameters, representing the routine as not one routine, but multiple with differing signatures becomes a possibility:
multi sub prettify(Any:_ $value --> Str:D) { $value.gist }
multi sub prettify(Exception:D $exception --> Str:D) { "\e[31m$exception.^name()\e[m" }
multi sub prettify(Failure:D $failure --> Str:D) { "\e[33m$failure.exception.^name()\e[m" }
Each multi
here is a dispatchee of the &prettify
routine. When this is called, the dispatchee with the most specific signature that typechecks given its argument will be selected and invoked, or if none match, a typechecking exception will be thrown. Because we didn’t define one ourselves, the proto
routine that handles this will be generated for us. This comes with :(|)
as a signature when we want something a little more specific here:
proto sub prettify(Any:_ --> Str:D) {*}
This constrains the type of &prettify
‘s first parameter to Any:_
for all of its dispatchees. Where the {*}
is written lies the multi
invocation that will get made when this is called; because this is all we want to do in this case, we can write this in place of the routine body.
With the help of subsets, multiple dispatch can represent simpler if
, when
, or with
branches in a program in a more extendable and testable way. There are ways we can avoid having these branches occur during runtime in certain cases, however.
Sharing with Roles
Getting back to chess, our Chess::Piece
and Chess::Square
types aren’t as optimal as they could be. Because we define their $.colour
and $.type
as attributes, we would wind up evaluating conditions related to these with each move made. This is unnecessary when we know exactly how a specific colour and type of piece will ever be able to move, and what colour each square of a chess board will ever be ahead of time.
Starting with Chess::Piece
, we could maybe eliminate the $.colour
and $.type
attributes that define behaviour related to these, but in doing so, we lose direct access to Chess::Piece
‘s attributes. Creating a hierarchical type system with classes here is overkill. When we want to share rather than extend behaviour, a role would be a more apt kind to give a type, or in this case, several:
role Chess::Piece { }
role Chess::Piece[White, Pawn] {
method colour(::?CLASS:_: --> White) { }
method type(::?CLASS:_: --> Pawn) { }
}
role Chess::Piece[Black, Pawn] {
method colour(::?CLASS:_: --> Black) { }
method type(::?CLASS:_: --> Pawn) { }
}
role Chess::Piece[Chess::Colour:D $colour, Bishop] {
method colour(::?CLASS:_: --> Chess::Colour:D) { $colour }
method type(::?CLASS:_: --> Bishop) { }
}
role Chess::Piece[Chess::Colour:D $colour, Rook] {
method colour(::?CLASS:_: --> Chess::Colour:D) { $colour }
method type(::?CLASS:_: --> Rook) { }
}
role Chess::Piece[Chess::Colour:D $colour, Knight] {
method colour(::?CLASS:_: --> Chess::Colour:D) { $colour }
method type(::?CLASS:_: --> Knight) { }
}
role Chess::Piece[Chess::Colour:D $colour, Queen] {
method colour(::?CLASS:_: --> Chess::Colour:D) { $colour }
method type(::?CLASS:_: --> Queen) { }
}
role Chess::Piece[Chess::Colour:D $colour, King] {
method colour(::?CLASS:_: --> Chess::Colour:D) { $colour }
method type(::?CLASS:_: --> King) { }
}
Roles are mixin types. We can declare an arbitrary number of these with the same name to form a role group, so long as they have differing and non-overlapping type parameters. With two type parameters, we can establish a relationship between colours and types of chess pieces when it comes to behaviour. That first role is an exception, which will contain behaviour pertaining to the absence of a chess piece in Chess::Square
. The types we wind up with here resemble a multiple dispatch routine. In fact, we do have multiple dispatch, just it’s happening at the type level.
Similar to class’ $?CLASS
and ::?CLASS
symbols, we have $?ROLE
and ::?ROLE
symbols we can use to reference an outer role type. Roles can’t do much besides parameterize without the help of a class somewhere along the way, so we still get the $?CLASS
and ::?CLASS
symbols in the end. In the context of a role, these are generic types to be filled by any class it ever winds up getting mixed into.
As mixin types, methods and attributes of roles don’t really belong to the role itself, but the class it eventually gets mixed into. However, Chess::Piece
is one of the rarer cases where we don’t actually have any class to mix the type into. That’s OK because roles are punnable. By default, when we attempt to call a method like new
on a role, this is not called on the role itself, but a pun produced by mixing in the role to an empty class. This behaviour allows roles to be usable like classes for the most part.
As with Chess::Piece
, the behaviour of Chess::Square
will depend on its colour in a predictable way, so we have a type parameter there too:
role Chess::Square[White] {
has Chess::Piece:_ $.piece is rw = Chess::Piece.^pun;
method colour(::?CLASS:_: --> White) { }
}
role Chess::Square[Black] {
has Chess::Piece:_ $.piece is rw = Chess::Piece.^pun;
method colour(::?CLASS:_: --> Black) { }
}
We change the default chess piece for a square to a pun of Chess::Piece
because Chess::Piece
refers to the entire role group, not the individual pun we mean here.
Wrapping Up Once More
With our types sorted out, we can now set up a chess board for Chess::Board.BUILD
now:
class Chess::Board {
# ...
submethod BUILD(::?CLASS:D: --> Nil) {
@!squares = |(
(flat (Chess::Square[Black].new, Chess::Square[White].new) xx 4),
(flat (Chess::Square[White].new, Chess::Square[Black].new) xx 4),
) xx 4;
@!squares[0;0].piece = Chess::Piece[White, Rook].new;
@!squares[0;1].piece = Chess::Piece[White, Knight].new;
@!squares[0;2].piece = Chess::Piece[White, Bishop].new;
@!squares[0;3].piece = Chess::Piece[White, Queen].new;
@!squares[0;4].piece = Chess::Piece[White, King].new;
@!squares[0;5].piece = Chess::Piece[White, Bishop].new;
@!squares[0;6].piece = Chess::Piece[White, Knight].new;
@!squares[0;7].piece = Chess::Piece[White, Rook].new;
@!squares[1;$_].piece = Chess::Piece[White, Pawn].new for ^8;
@!squares[6;$_].piece = Chess::Piece[Black, Pawn].new for ^8;
@!squares[7;0].piece = Chess::Piece[Black, Rook].new;
@!squares[7;1].piece = Chess::Piece[Black, Knight].new;
@!squares[7;2].piece = Chess::Piece[Black, Bishop].new;
@!squares[7;3].piece = Chess::Piece[Black, King].new;
@!squares[7;4].piece = Chess::Piece[Black, Queen].new;
@!squares[7;5].piece = Chess::Piece[Black, Bishop].new;
@!squares[7;6].piece = Chess::Piece[Black, Knight].new;
@!squares[7;7].piece = Chess::Piece[Black, Rook].new;
}
}
At this point, it’d be nice if we could see what we’re doing. We’ll define dispatchees for the gist
method in relevant types, starting with Chess::Piece
:
role Chess::Piece {
multi method gist(::?CLASS:U: --> ' ') { }
}
role Chess::Piece[White, Pawn] {
# ...
multi method gist(::?CLASS:D: --> '♙') { }
}
role Chess::Piece[Black, Pawn] {
# ...
multi method gist(::?CLASS:D: --> '♟︎') { }
}
role Chess::Piece[Chess::Colour:D $colour, Bishop] {
# ...
multi method gist(::?CLASS:D: --> Str:D) {
my constant %BISHOPS = :{ (White) => '♗', (Black) => '♝' };
%BISHOPS{$colour}
}
}
role Chess::Piece[Chess::Colour:D $colour, Rook] {
# ...
multi method gist(::?CLASS:D: --> Str:D) {
my constant %ROOKS = :{ (White) => '♖', (Black) => '♜' };
%ROOKS{$colour}
}
}
role Chess::Piece[Chess::Colour:D $colour, Knight] {
# ...
multi method gist(::?CLASS:D: --> Str:D) {
my constant %KNIGHTS = :{ (White) => '♘', (Black) => '♞' };
%KNIGHTS{$colour}
}
}
role Chess::Piece[Chess::Colour:D $colour, Queen] {
# ...
multi method gist(::?CLASS:D: --> Str:D) {
my constant %QUEENS = :{ (White) => '♕', (Black) => '♛' };
%QUEENS{$colour}
}
}
role Chess::Piece[Chess::Colour:D $colour, King] {
# ...
multi method gist(::?CLASS:D: --> Str:D) {
my constant %KINGS = :{ (White) => '♔', (Black) => '♚' };
%KINGS{$colour}
}
}
Chess::Square
can wrap its piece’s gist with coloured brackets:
role Chess::Square[White] {
# ...
multi method gist(::?CLASS:D: --> Str:D) {
"\e[37m[\e[m$!piece.gist()\e[37m]\e[m"
}
}
role Chess::Square[Black] {
# ...
multi method gist(::?CLASS:D: --> Str:D) {
"\e[30m[\e[m$!piece.gist()\e[30m]\e[m"
}
}
And Chess::Board
can glue these together:
class Chess::Board {
# ...
multi method gist(::?CLASS:D: --> Str:D) {
@!squares.rotor(8).reverse.map(*».gist.join).join($?NL)
}
}
my Chess::Board:D $board .= new;
say $board;
Now we can see some results:
[♜][♞][♝][♚][♛][♝][♞][♜]
[♟︎][♟︎][♟︎][♟︎][♟︎][♟︎][♟︎][♟︎]
[ ][ ][ ][ ][ ][ ][ ][ ]
[ ][ ][ ][ ][ ][ ][ ][ ]
[ ][ ][ ][ ][ ][ ][ ][ ]
[ ][ ][ ][ ][ ][ ][ ][ ]
[♙][♙][♙][♙][♙][♙][♙][♙]
[♖][♘][♗][♕][♔][♗][♘][♖]
Now let’s put Chess::Position
to use. We’ll give it an explicit new
method to make it a little easier to work with and a gist
candidate to express its output in a way a chess player can read:
class Chess::Position {
# ...
method new(::?CLASS:_: Int:D $rank, Int:D $file --> ::?CLASS:D) {
self.bless: :$rank, :$file
}
multi method gist(::?CLASS:D: --> Str:D) {
my constant @RANKS = <a b c d e f g h>;
@RANKS[$!rank] ~ $!file + 1
}
}
If we return a list of offsets for all possible moves a knight can make:
role Chess::Piece[Chess::Colour:D $colour, Knight] {
# ...
method moves(::?CLASS:D: --> Seq:D) {
gather {
take slip (-2, 2) X (-1, 1);
take slip (-1, 1) X (-2, 2);
}
}
}
Then we can grep for the valid moves to make from these in Chess::Board
with the help of feed operators and v6.e’s ||
prefix operator:
use v6.e.PREVIEW;
class Chess::Board {
# ...
method moves(::?CLASS:D: Chess::Index $rank, Chess::Index $file --> Seq:D) {
gather with @!squares[$rank;$file].piece -> Chess::Piece:D $piece {
$piece.moves
==> map({ $rank + .[0], $file + .[1] })
==> grep((Chess::Index, Chess::Index))
==> grep({ not @!squares[||$_].piece.?colour ~~ $piece.colour })
==> map({ Chess::Position.new: |$_ })
==> slip()
==> take()
}
}
}
my Chess::Board:D $board .= new;
say $board.moves: 0, 1;
Now we can see what moves the white knight at a2 can make on the first turn:
(c1 c3)
Combined with multiple dispatch, Raku’s type system allows us take a problem like a game of chess and split it up into smaller, more manageable components that can be tested more easily. Runtime exceptions can often be expressed as typechecking exceptions and if denoted explicitly, types can make it easier to catch bugs ahead of time. Though they don’t come into play with chess, at this point you should know enough about how types in Raku work to be able to take advantage of generics and coercions.
One thought on “Day 19: Typed Raku, Part 2: Taming Behaviour”