Skip to content

Instantly share code, notes, and snippets.

@dwarring
Last active December 30, 2015 15:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dwarring/7848868 to your computer and use it in GitHub Desktop.
Save dwarring/7848868 to your computer and use it in GitHub Desktop.
An (almost) Simple Grammar

An (almost) Simple Grammar

Today's example constructs a grammar for tracking playing cards in a single deal. We'll say it's poker with one or more players and that each player is being dealt a hand that contains exactly five cards.

The "almost" part is the need to track cards and detect duplicates. We want to check for repeated cards both within each card-hand and between hands.

To start with, here's the basic grammar (no duplicate checks yet):

grammar CardGame {

    rule TOP { ^ <deal> $ }

    rule deal {
        <hand>+ % ';'
    }

    rule hand { [ <card> ]**5 }

    token card {<face><suit>}

    proto token suit {*}
    token suit:sym<♥>  {<sym>}
    token suit:sym<♦>  {<sym>}
    token suit:sym<♠>  {<sym>}
    token suit:sym<♣>  {<sym>}

    token face {:i <[2..9]> | 10 | j | q | k | a }
}

say CardGame.parse("2♥ 5♥ 7♦ 8♣ 9♠");
say CardGame.parse("2♥ a♥ 7♦ 8♣ j♥");

Our top-level rule is a deal. The deal consists of one or more hands separated by ';'. Each hand consists of 5 playing cards.

Each card is represented by a face, one of: a (ace), j (jack), q (queen) or k (king), or 2 to 10 followed by a suite: ♥ (hearts) ♦ (diamonds) ♣ (clubs) or ♠ (spades). [We could have used the playing cards characters, newly introduced in Unicode 6.0, but these aren't widely supported yet].

As expected, the first cut of the grammar cheerily parses hands and games with duplicate cards.

say CardGame.parse("a♥ a♥ 7♦ 8♣ j♥");                  # one hand, duplicate a♥
say CardGame.parse("a♥ 7♥ 7♦ 8♣ j♥; 10♥ j♥ q♥ k♥ a♥"); # two hands, duplicate j♥

Now to add the duplicate check. We start by adding a Perl 6 variable declaration to the grammar:

rule deal {
    :my %*PLAYED = ();
    <hand>+ % ';'
}

This declares %*PLAYED [1]. It's %* twigil indicates declared it as a hash (%) that's dynamically scoped (*).

Dynamic scoping is not only for subroutine and method calls [1]. It also works seamlessly with grammar rules and tokens . %*PLAYED is available to the hand rule called from the game and therefore to the card token, which it calls.

This includes any actions being called. So we can track and report on duplicates by adding an action method on card:

class CardGame::Actions {
    method card($/) {
       my $card = $/.lc;
       say "Hey, there's an extra $card"
           if %*PLAYED{$card}++;                                              
    }
}

my $a = CardGame::Actions.new;
say CardGame.parse("a♥ a♥ 7♦ 8♣ j♥", :actions($a));                  # one hand, duplicate a♥
# 'Hey there's an extra a♥'
say CardGame.parse("a♥ 7♥ 7♦ 8♣ j♥; 10♥ j♥ q♥ k♥ a♦", :actions($a)); # two hands, duplicate j♥
# 'Hey there's an extra j♥'

And that might be all that's needed most of the time for tracking and reporting on duplicates. There's a pretty good separation between the declarative grammar and procedural actions, with just one dynamic variable for tracking duplicates.

I had a situation where I wanted duplicate checking to be a parsing constraint. Parsing needed to fail when duplicates were encountered.

This was achieved by moving the duplicate check grammar side:

token card {<face><suit>
    <?{
        # only allow each card to appear once
        my $card = $/.lc;
        say "Hey, there's an extra $card"
            if %*PLAYED{$card};
            
        ! %*PLAYED{$card}++;
     }>
}

We've introduced a code assertion delimited by <?{ and }> [2]. The rule succeeds when the code evaluates to a True value. The card token thus fails when the same card is detected more than once in a single deal.

say CardGame.parse("2♥ 7♥ 2♦ 3♣ 3♦");                  # legitimate
# parses

say CardGame.parse("a♥ a♥ 7♦ 8♣ j♥");                  # one hand, duplicate a♥
# fails with message: Hey, there's an extra a♥

say CardGame.parse("a♥ 7♥ 7♦ 8♣ j♥; 10♥ j♥ q♥ k♥ a♦"); # two hands, duplicate j♥
# fails with message: Hey, there's an extra j♥

This last technique works fully on rakudo/parrot today and will hopefully be available on the jvm in the near future.

There is one thing to be careful of with this type of technique, back-tracking (trying of alternatives). Ideally grammars should be as simple as possible and minimise back-tracking.

If in any doubt, I recommend using on or more of the Grammar::Debugger, Grammar::Tracer [3] or the Rakudo::Debugger [4] modules [5] to track what's happening.

That concludes the exercise for today; a simple Perl 6 Grammar to parse playing-cards in a card-game, but with duplicate checks using either actions or code assertions.

@raiph
Copy link

raiph commented Dec 13, 2013

Well say ->
We'll say ?

The deal consists of one or hands ->
The deal consists of one or more hands

♥ (hearts) ♦ (diamonds) ♣ (clubs) and ♠ (spades). ->
♥ (hearts) ♦ (diamonds) ♣ (clubs) or ♠ (spades).

token thus fails when their are any ->
token thus fails when there are any

If in any doubt, I recommed ->
If in any doubt, I recommend


using the Grammar::Debugger or Grammar::Tracer module [3] [4] to track what's happening.

Or the debugger?

@dwarring
Copy link
Author

thanks for the feedback raiph, have updated gist

@dwarring
Copy link
Author

updated draft 16/12

  • added CardGame::Actions
  • mentioned availability on jvm

@raiph
Copy link

raiph commented Dec 16, 2013

There is one thing to be careful of with this type of technique, back-tracking (trying of alternatives). One ideal of a well written grammar is to minimise back-tracking anyway.

The issue is presumably side-effects and back-tracking. Maybe discuss that a little bit?

@dwarring
Copy link
Author

raiph,
Haven't come up with anything succinct (yet). just noted that it's a big topic. Backtracking/efficient grammars could be one for next year!

  • David

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment