Day 19: Typed Raku, Part 2: Taming Behaviour

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 whens, not ifs. 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:

bastille% raku chess.raku
[♜][][♝][][♛][][♞][]
[♟︎][♟︎][♟︎][♟︎][♟︎][♟︎][♟︎][♟︎]
[ ][ ][ ][ ][ ][ ][ ][ ]
[ ][ ][ ][ ][ ][ ][ ][ ]
[ ][ ][ ][ ][ ][ ][ ][ ]
[ ][ ][ ][ ][ ][ ][ ][ ]
[♙][][♙][][♙][][♙][]
[][♘][][♕][][♗][][♖]

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:

bastille% raku chess.raku
(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

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

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

%d bloggers like this: