2019-10-05

Enumeration (enum) in Perl6

Perl6 has enum. Now I have acquired a little bit of the Ada mindset, so I expected something similar. But Perl6 isn’t Ada, of course, hence I was disappointed. Should I retune my expectations taking into account this fact?

Yes, but also no. Because Perl6 enumeration doesn’t feel completely right.

My problem was this: I wanted to use enumeration to label squares of a chessboard. Something like this:

enum Square<A8 B8 C8 D8 E8 F8 G8 H8
            A7 B7 C7 D7 E7 F7 G7 H7
            A6 B6 C6 D6 E6 F6 G6 H6
            A5 B5 C5 D5 E5 F5 G5 H5
            A4 B4 C4 D4 E4 F4 G4 H4
            A3 B3 C3 D3 E3 F3 G3 H3
            A2 B2 C2 D2 E2 F2 G2 H2
            A1 B1 C1 D1 E1 F1 G1 H1>;

Now I want to use these labels to address a square on a representation of a board.

enum Piece<EMPTY WPAWN WKING WROOK WBISHOP>; # ...
my @board = EMPTY xx 64;
@board[E1] = WKING;
@board[A2 .. H2] = WPAWN xx 8;
say @board[$_] for A8..H8;

This works (as expected). It isn’t bad.

But now I want a map, for whatever reason. Here come the problems.

my %b = A8 => BROOK, B8 => BKNIGHT;

This is one of the traps of Perl6 and is well explained. A8 here isn’t the enum but a string made from the symbol A8. Whatever on the left of => gets stringified, unless written otherwise.

So we have:

say %b<A8>;  # outputs BROOK

Keys of the map are made of strings, and the <something> stringifies too. So it makes sense.

But the following surprised me:

say %b{A8}; # outputs BROOK

This means that this A8 must be a string in that contest too: I would expect to be whatever A8 is. And what is it?

say A8.WHAT;      # (Square)
say 5.WHAT;       # (Int)
say "hello".WHAT; # (Str)

Since I know we have strings as keys, that (Square) used as a key must be coerced to a string. I don’t know if I like it very much, but it makes sense.

Indeed, however, I wanted the type Square for the keys. How to is explained here. And also we have to keep in mind that:

with objects as keys, you often cannot use the <...> construct for key lookup, as it creates only strings and allomorphs. Use the {...} instead.

Let’s try.

my %b1 := :{ (A8) => BROOK, (B8) => BKNIGHT };

(Without the := it gives a warning, so I added it.)

say %b1<A8>;     # (Mu)   ...wrong
say %b1{A8};     # BROOK  ...ok!

Now I am using Square as keys.

Now I want to use a range.

say %b1{A8..B8};

What have I got here? Before saying the answer, let’s try also this:

say %b1{$_} for A8..B8;

to be compared with the say @board[$_] for A8..H8; I’ve written before.

You obtain (Mu). Why?

Before trying to answer, let’s try this one:

say %b1{[A8, B8]};  # (BROOK BKNIGHT)

It works!

But then, why it doesn’t work with ranges? I’ve found this:

If the symbol is used, it is treated as a constant expression and the symbol is replaced with the value of the enum-pair.

Then shouldn’t the [A8, B8] really be [0, 1]? It is when used to index an array, it is not in the case you build %b1 the way I did (it is something of type Square), and it is again when you use ranges, even if apparently the range is made of typed values:

say A8..B8;  # Square::A8..Square::B8

but

my @c = A8..B8;
say @c;          # [0 1]

Just ints.

So if I want to iterate over a rank, I can’t do An..Hn, unless I am using the @board (array) representation, or doing something as cumbersome as:

my %b2 = (+A8) => BROOK, (+B8) => BBISHOP;
say %b2{$_} for A8..B8;

This is cumbersome because I have to force the coercion of the keys into integers, i.e., the value. Which is then stringified anyway:

say %b2.perl;
   # {"0" => Piece::BROOK, "1" => Piece::BBISHOP}
say %b2<A8>;  # (Any)
say %b2{A8};  # (Any)
say %b2{+A8}; # BROOK

If I want to be able to iterate using ranges, I must use integers (the values of the symbols, since there isn’t anything different specified) as keys. But this would look like this:

my %b3 := :{ (+A8) => BROOK, (+B8) => BBISHOP };
say %b3{$_} for A8..B8;   # ok!
say %b3.perl;
   # :{0 => Piece::BROOK, 1 => Piece::BBISHOP}

But when addressing a single square I still have to write this:

say %b3{+A8}; # BROOK

And by the way, the symbols BROOK and BBISHOP aren’t replaced with the value of the enum-pair.

What am I missing?

The case of @board looks good, all other usages can be confusing and force the use of extra code, explicit coercion, or whatever. Like this:

say %b1{Square($_)} for A8..B8;

I need this explicit Square(x) to get it the way I want, and this even if, as we saw above, A8..B8 seems to be “correctly typed”.

I keep saying this is confusing:

my Square @b-back-rank = A8..H8;  # Type check failed
   # A8..H8 => Square::A8..Square::H8

But the following is ok — of course (?):

my Square @b-back-rank = [A8, B8, C8, D8, E8, F8, G8, H8];

I.e., those symbols aren’t replaced with the value of the enum-pair or we would have another type check failed:

my Square @b-back-rank = [0, 1, 2, 3, 4, 5, 6, 7];

Instead this will be ok:

my @b-back-rank = A8..H8;
say @board[@b-back-rank];
  # EMPTY EMPTY EMPTY EMPTY BKING EMPTY EMPTY EMPTY)

And this too:

my @some-squares = [A8, B8]; # [Square::A8, Square::B8]
say @board[@some-squares];

As long as ranges aren’t involved, it seems I can stick with my hashes:

say %b1{@some-squares}; # (BROOK BKNIGHT)

Then if I need a whole rank and I don’t want to write each square one by one:

my @b-back-rank = (A8..H8).map({Square($_)});
say @b-back-rank.perl;

Or maybe better — but still unnecessarily lengthy and confusing (if A8 is a Square::A8, … why do I need to use Square()? Because when you iterate over the range, it becomes its value…? Hence you can’t have ranges of discrete “type”?):

my @b-back-rank = [Square($_) for A8..H8];

Some other details

Of course

say A6 < H1;

is true, but it isn’t clear if it is true because A6 comes before H1, or because 16 < 63. (I hope the subtle difference is clear.)

That’s it.

Let’s take a look at Ada now.

Ada enumeration

Ada hasn’t the keyword enumeration (or enum or alike) but it has enumerations: you define an enumeration as a type, of course.

   type Square is 
     (A8, B8, C8, D8, E8, F8, G8, H8,
      A7, B7, C7, D7, E7, F7, G7, H7,
      A6, B6, C6, D6, E6, F6, G6, H6,
      A5, B5, C5, D5, E5, F5, G5, H5,
      A4, B4, C4, D4, E4, F4, G4, H4,
      A3, B3, C3, D3, E3, F3, G3, H3,
      A2, B2, C2, D2, E2, F2, G2, H2,
      A1, B1, C1, D1, E1, F1, G1, H1);

No surprises of sort, because Ada isn’t gradually typed and it takes types very “rigidly” and seriously (and by this I don’t want to imply that Perl6 doesn’t take types seriously —but of course here we have two languagues with a totally different ideology). This means “don’t you dare to think H1 is 63”. Kind of. Because if you want:

   Put (Square'Pos (H1)); New_Line;

this outputs 63 (formatted according to a default, and provided you have withed and used Ada.Integer_Text_IO and also Ada.Text_IO).

A board indexed by a Square would be of type:

   type Board is array (Square range A8..H1) of Piece;

An empty board is initiazed as

   A_Board : Board := (others => Empty);

And some piece can be put like this:

   A_Board (E1) := White_King;
   A_Board (A8) := Black_Rook;
   A_Board (B8) := Black_Knight;
   A_Board (E8) := Black_King;

A rank of pawns is placed like this:

   for Sq in A2 .. H2 loop
      A_Board (Sq) := White_Pawn;
   end loop;

Ada is a verbose language, but the point isn’t verbosity: here the range is really a range of elements of type Square, and it stays that way. Of course languages like Perl6 are more versatile and Ada isn’t quite right for quick prototyping and experimenting. But it’s a coherent language, as far as I know, and well design, as long as I can say.

Perl6 is really great and interesting in all the thing I’ve seen so far, except the enum: here it really felt like there is something wrong to me.

With a hash wouldn’t be different. For example with an Ordered_Map:

   package Ordered is new Ordered_Maps
     (Key_Type => Square,
      Element_Type => Piece);

  O_Board : Ordered.Map;

  -- ...

   O_Board.Insert (E1, White_King);
   O_Board.Insert (A8, Black_Rook);
   O_Board.Insert (E8, Black_King);
   
   for Sq in A2 .. H2 loop
      O_Board.Insert (Sq, White_Pawn);
   end loop;

   -- ...

   for Sq in A8 .. H1 loop
      if O_Board.Contains (Sq) then
         Put_Line (Piece'Image (A_Board (Sq)));
      end if;
   end loop;

We have said the type of the key and the element, and that’s what they must be. The enumeration being an actual type, gives no surprise.

With a little help of the buzz and redditors

It seems like the perl6 tag triggers some interest, and that’s good because it means there’s a live community. Glad to give my little contribution, for what is worth. Perl6 Raku1 is a very nice language to me!

Now, about the topic of this article: beyond other reasoning, on reddit2 I’ve received an easy solution to my “problem”, thanks to Kaiepi: using ... (Seq) instead of .. (Range).

E.g.,

say %b1{A8...B8}

  1. It could happen that I mispell it as roku, because in Japanese roku (六) means six, like in Perl6. By the way, I am not Japanese but Italian, but I had an interest in Japanese language decades ago, when I was younger. It kind of stuck with me even nowadays, because of the nickname, and because I still would like to learn Japanese, among other (too many) human languages. But alas, I know just Italian and a little bit of English.↩︎

  2. Shintakezou it’s just a nickname, name handle, username, whatever, and shouldn’t be written like Shin Takezou (otherwise it looks like surname-name); Takezou is a real thing, Shintakezou isn’t, to my knowledge. Decades ago I was reading Musashi (the book by Yoshikawa), hence got used to pick musashi as a nickname online, until the day I found a site where it was already taken, and decided to form shintakezou, from Shinmen Takezou, but also considering that shin (新) can mean new, and Musashi is, in a certain way, a new Takezou. (In the book, it was Takuan to rename him Musashi, just a different reading of the same kanji of Takezou.)↩︎

No comments:

Post a Comment