29 March 2016

Roll for Initiative, part 2

blogdown: text2.md

More Perl 6 learnings must be had! I'm behind on my posts and I want to quickly catch up to where the module is at, so that I can then make the module even better! But what is a Module, in Perl 6? Perl 5 programmers will find it's a pretty similar process, but let's look over it and also see how classes work. Yes, there's a class keyword in Perl 6 and it's built in, no Moose required!

Love the unit

Perl 6 divides code into compilation units. These are independent bits of code that be compiled separately, and (very) roughly correspond to the source files you might use to set up a Module. While you can absolutely declare modules and classes and roles inline in your code, as in:-

module Stuff {
   module More {
   # This code gets put into Stuff::More
      sub frob() is export {
         ...
      }
   }
}

... you might prefer to declare the rest of the file is part of your module. Much like the old package keyword in Perl 6, you can do exactly that:-

unit module Stuff::More;

sub frob() is export {
   ...
}

Much nicer if you're making a large module or class which would require a lot of mostly superfluous indentation.

I keep saying module or class. In Perl 5, a "class" is just a special kind of package; in Perl 6, it is much more distinct and gets its own keyword built-in from day one. Just as with modules, you can declare them inline or decide to use the rest of the file as the class definition.

Lick the unit

I want to make a module for dice-rolling. I suspect the best interface for such a module would be an object-oriented one, so I'll make the main point of entry a class rather than a module that just exports some subroutines. Note that even if I'm not using the module keyword, I'm still going to talk about the project as a whole as being a "Module", i.e. something you could install with panda install Dice::Roller. So let's start sketching something out. Here's what my file structure looks like so far:-

.
├── lib
│   └── Dice
│       └── Roller.pm6
├── LICENSE
├── META6.json
├── README.md
├── roll.p6
└── t
    └── 00-basics.t

Don't worry too much about the META6.json file or the t/ directory just yet; they're only needed later when we turn our pet module into a Proper Perl 6 Module With Bells And Whistles. We just need a 'lib' directory to put code in, containing a structure that corresponds to our chosen Dice::Roller name. The basic roll.p6 code using the module might look like this:-

#!/usr/bin/env perl6

use v6;
use lib 'lib';
use Dice::Roller;

my $dice = Dice::Roller.new('1d20');
say $dice.perl;

We use v6; to ensure we're on the right perl, use lib 'lib'; indicating where we want to look for additional libraries (because we're running this from right here in our source directory; if we install the module properly later, it'll already be on the appropriate search paths), use the module itself, and just check we can create an object of the Dice::Roller class.

How are we defining that class? Slowly, incrementally, one step at a time. Let's define a very basic grammar, two attributes, and a new method.

unit class Dice::Roller;

# Grammar defining a dice string:-
# ------------------------------

grammar DiceGrammar {
   token         TOP { ^ <roll> $ }
   token        roll { <quantity> <die> }
   token    quantity { \d+ }
   token         die { d(\d+) }
}

# Attributes of a Dice::Roller:-
# ----------------------------

# Attributes are all private by default, and defined with the '!' twigil. But using '.' instead instructs
# Perl 6 to define the $!string attribute and automagically generate a .string *accessor* that can be
# used publically. Note that this accessor will be read-only by default.

has Str $.string is required;
has Match $.parsed is required;

# We define a custom .new method to allow for positional (non-named) parameters:-
method new(Str $string) {
   my Match $match = DiceGrammar.parse($string);
   return self.bless(string => $string, parsed => $match);
}

The grammar defines a series of tokens that will match a very simple dice expression like "1d20". Perl 6 grammars are fancy new ways to organise the new regular expressions - the new syntax is a lot cleaner than Perl 5's regexes, and lets us write a more formal-looking parser.

Attributes of a class are defined using the has keyword. They also have twigils - in this case a . after the $ sigil - signifying that we want a $!string attribute and we want it to be accessible to users of the class using an accessor - .string().

Note that overriding method new is generally not considered the best way to define your constructor; it means you have to do more things by hand. If you're happy using named parameters, implementing a submethod BUILD lets you do any last-minute tinkering with your object's attributes and any other initialisation you might need.

Become the unit

Here's what the output looked like before I added the DiceGrammar.parse bit and the $!match attribute:-

$ ./roll.p6 
Dice::Roller.new(string => "1d20")

The .perl method will give you a representation of the object as if it were constructed using Perl code, just like good ol' Data::Dumper from Perl 5. It's pretty handy for debugging, but it can get quite verbose sometimes. Here's what it looked like after I also added the Match object as an attribute:-

$ ./roll.p6 
Dice::Roller.new(string => "1d20", parsed => Match.new(ast => Any, list => (), hash => Map.new((:roll(Match.new(ast => Any, list => (), hash => Map.new((:die(Match.new(ast => Any, list => (Match.new(ast => Any, list => (), hash => Map.new(()), orig => "1d20", to => 4, from => 2),), hash => Map.new(()), orig => "1d20", to => 4, from => 1)),:quantity(Match.new(ast => Any, list => (), hash => Map.new(()), orig => "1d20", to => 1, from => 0)))), orig => "1d20", to => 4, from => 0)))), orig => "1d20", to => 4, from => 0))

Not an ideal representation. What else can we do with this? Well, we can use .gist instead:-

   my Match $match = DiceGrammar.parse($string);
   say "Parsed: ", $match.gist;

This gets us a much nicer representation provided by the Match object designed for humans to read:-

Parsed: 「1d20」
 roll => 「1d20」
  quantity => 「1」
  die => 「d20」
   0 => 「20」

Observe that all the named calls to other regexps in the grammar like <die> became captured and accessible by their name, while the one "anonymous" capture defined with parentheses in token die { d(\d+) } is accessible via the positional capture 0. We could iterate through the Match object, using it like a hash or a list, and discover our captured tokens that way. For simple regexps, that's exactly what you'd do, with the special $/ match variable. But for complex grammars, we'd want to use a series of actions to build up our own "Abstract Syntax Tree" of custom objects. Then we can define precisely what our internal representation should look like, and keep the parsing code separate from the grammar definition.

So let's do that.

We are the unit

We will need a Grammar (and to make this interesting, let's use an updated one from later on in development when I was experimenting with adding flat 'modifiers' to rolls):-

grammar DiceGrammar {
   token         TOP { ^ <roll> [ ';' \s* <roll> ]* ';'? $ }
   token        roll { <quantity> <die> \s* <modifier>* \s* }
   token    quantity { \d+ }
   token         die { d(\d+) }
   token    modifier { ('+' | '-') \s* (\d+) \s* }
}

Note this also allows for multiple independent rolls from the same string, separated with ';'. The square brackets are grouping ';' and the second <roll> together while not capturing like parentheses do.

We need some classes to build our abstract syntax tree with:-

# A single polyhedron.
class Die {
   has Int $.faces;      # All around me different faces I see
   has @.distribution;   # We will use this when rolling; this allows for non-linear dice to be added later.
   submethod BUILD(:$!faces) {
      # Initialise the distribution of values with a range of numbers from 1 to the number of faces the die has.
      @!distribution = 1..$!faces;
   }
}

# Some fixed value adjusting a roll's total outcome.
class Modifier {
   has $.value;
}

# A roll of one or more polyhedra, with some rule about how we combine them.
class Roll {
   has Int $.quantity;
   has Die $.die;
   has Modifier @.modifiers;
}

Then comes the magic - an actions class with methods that correspond to our grammar's rules, determining how to make each step of the tree as we go through it:-

class DiceActions {
   method TOP($/) {
      make $<roll>».made;
   }
   method roll($/) {
      make Roll.new( quantity => $<quantity>.made, die => $<die>.made, modifiers => $<modifier>».made );
   }
   method quantity($/) {
      make $/.Int;
   }
   method die($/) {
      make Die.new( faces => $0.Int );
   }
   method modifier($/) {
      make Modifier.new( value => "$0$1".Int );
   }
}

I am... the unit

All that's required is to then pass this into the parsing method:-

   my $match = DiceGrammar.parse($string, :actions(DiceActions));

Now we can get a Match object that contains a member $match.made, and in this case will be an array of Dice::Roller::Roll objects! All neatly packaged up and ready to roll, if we'd implemented that method yet.

$ ./roll.p6 
Parsed: 「1d20」
 roll => 「1d20」
  quantity => 「1」
  die => 「d20」
   0 => 「20」
Dice::Roller.new(string => "1d20", parsed => $[Dice::Roller::Roll.new(quantity => 1, die => Dice::Roller::Die.new(faces => 20, distribution => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]), modifiers => Array[Dice::Roller::Modifier].new())])

But don't worry, I am speaking to you from the future! and we have implemented that method and many more. Perhaps we can leave that for part 3, though.

1 comment:

  1. This comment has been removed by a blog administrator.

    ReplyDelete