2012-08-31

How we doin’, boss?

Obviously we haven’t gotten very far yet. Were I not writing this journal as I create the code, I’d probably be further along, but it’s helpful for me to capture this to really drill home what and why I’m doing what I’m doing.

I’m concerned that I may start straying from common Erlang conventions, but that’s nothing time and experience can’t solve. So let’s dive back in and see where this joyride takes us.

To give this project a little more structure and to learn a valuable skill (Git + GitHub) I’ve created a public repository. Long-time subversion user, so I’m cautiously optimistic that for such a simple project Git won’t cause much heartburn.

Stream of consciousness

Messaging

As I implied yesterday, messages in Erlang are not like traditional RPC calls. All messages are asynchronous: these are not function calls with return values.1

First incremental change today: let’s send a reply to whoever sent us the move request so that process doesn’t have to ask us for the results.

As with the location request (which will stay in place), we need to know our caller’s PID in order to send a response, so we’ll change the match from:

    { X, Y } when X > 0, X < 9, Y > 0, Y < 9 ->
        piece_loop(Maneuver, Maneuver(Location, {X, Y}), Team);

to:

    { From, X, Y } when X > 0, X < 9, Y > 0, Y < 9 ->
        piece_loop(Maneuver, Maneuver(Location, {X, Y}), Team);

Interesting point here. In most languages, when changing a method signature, one would have to chase down every place where the method is called to change the clients as well.

Here, we could leave the original match in place and simply not send a response if the caller doesn’t pass along its PID.2 However, my only client so far is the shell, so I don’t have backwards compatibility to worry about.

Now to add the messaging logic:

    { From, X, Y } when X > 0, X < 9, Y > 0, Y < 9 ->
        piece_loop(Maneuver, From ! Maneuver(Location, {X, Y}), Team);

Is it really that easy?

2> PawnMove = fun({ OldX, OldY }, { NewX, NewY }) when NewX =:= OldX, NewY - OldY =:= 1 -> { NewX, NewY };
2>               ({X, Y}, _) -> { X, Y } end.
#Fun<erl_eval.12.82930912>
3> Pawn = spawn(piece, piece_loop, [ PawnMove, {1, 2}, white ]).
<0.39.0>
4> flush().
ok
5> Pawn ! { self(), 1, 3 }.
{<0.31.0>,1,3}
6> flush().
Shell got {1,3}
ok
7> Pawn ! { self(), location }.
{<0.31.0>,location}
8> flush().
Shell got {1,3}
ok

Yeah, it’s really that easy. The return value” from a message is the value sent, so we don’t have to introduce a short-lived variable to store the return value from Maneuver() to use it twice.

This would be a viable long-winded version if we were so inclined:

    { From, X, Y } when X > 0, X < 9, Y > 0, Y < 9 ->
        NewLoc = Maneuver(Location, {X, Y}),
        From ! NewLoc,
        piece_loop(Maneuver, NewLoc, Team);

One disadvantage to our approach (short- or long-winded) is that we’re sending back a location with no context. Did the piece move or not?

Pattern Matching

Let’s use this as an opportunity to experiment with pattern matching3 a bit further. We’ll send to the client not only the piece’s location, but also a flag indicating whether it moved.

piece_loop(Maneuver, Location, Team) ->
    receive
        { From, X, Y } when X > 0, X < 9, Y > 0, Y < 9 ->
            piece_loop(Maneuver,
                       move_response(From, Location, Maneuver(Location, {X, Y})),
                       Team);
        { From, location } ->
            From ! Location,
            piece_loop(Maneuver, Location, Team);
        done ->
            done
    end.

move_response(PID, Loc, Loc) ->
    PID ! { no, Loc };
move_response(PID, _, Loc) ->
    PID ! { yes, Loc }.

Pattern matching is a wonderful, wonderful tool. What did I just do?

In plain English:

If move_response is called with two location tuples that are identical, send a failure message. Otherwise, send a success message.”

_ is a dummy variable that will match anything, but since we already covered the matching location case in the first match, anything that doesn’t match that will be two different locations.

In either case, the full message back to the client is a tuple with 2 values: an atom (yes/no) and another tuple, the current location.

Let’s see it in action.

18> f().
ok
19> PawnMove = fun({ OldX, OldY }, { NewX, NewY }) when NewX =:= OldX, NewY - OldY =:= 1 -> { NewX, NewY };
19>               ({X, Y}, _) -> { X, Y } end.
#Fun<erl_eval.12.82930912>
20> Pawn = spawn(piece, piece_loop, [ PawnMove, {1, 2}, white ]).
<0.66.0>
21> Pawn ! { self(), 1, 1 }.
{<0.31.0>,1,1}
22> flush().
Shell got {no,{1,2}}
ok

So far, so good. Notice the error tuple that the shell found in its mailbox: {no,{1,2}}

Continuing, then, this should work:

23> Pawn ! { self(), 1, 3 }.
{<0.31.0>,1,3}
24> flush().
Shell got {no,{no,{1,2}}}
ok

Heh. Oops.

Recall that the return value from a message is the message sent. I introduced into the recursive call the return value from move_response(); the return value is the last value seen in that particular thread of execution, which is the message, which is not the location, but the full error tuple (or success tuple had that been the case).

So, when the second message was received from the shell, Location was no longer a {X, Y} tuple, but a {no, {X, Y}} tuple.

Easy fix:

move_response(PID, Loc, Loc) ->
    PID ! { no, Loc },
    Loc;
move_response(PID, _, Loc) ->
    PID ! { yes, Loc },
    Loc.

Now we’re making sure to return the current location from move_response, so it’ll be passed back into the recursive loop.

27> PawnMove = fun({ OldX, OldY }, { NewX, NewY }) when NewX =:= OldX, NewY - OldY =:= 1 -> { NewX, NewY };
27>               ({X, Y}, _) -> { X, Y } end.
#Fun<erl_eval.12.82930912>
28> Pawn = spawn(piece, piece_loop, [ PawnMove, {1, 2}, white ]).
<0.79.0>
29> Pawn ! { self(), 1, 3 }.
{<0.31.0>,1,3}
30> flush().
Shell got {yes,{1,3}}
ok
31> Pawn ! { self(), 1, 5 }.
{<0.31.0>,1,5}
32> flush().
Shell got {no,{1,3}}
ok
33> Pawn ! { self(), 1, 4 }.
{<0.31.0>,1,4}
34> flush().
Shell got {yes,{1,4}}
ok

Pawns, why did I start with pawns?

After flailing a bit for my next step, I decided to start capturing the various move functions in code. As soon as I started, I realized one glaring flaw with my current logic: pawns can only move in one direction, but that direction is different depending on the color.

So now the Maneuver anonymous functions are going to have to take 3 arguments: start, target, and team color.

Pawns have other odd properties, like the option to moving two spaces on the first move. That can be handled easily by making sure the Y value is 2 (for white) or 7 (for black) before accepting a two space move.

The real twist is the pawn’s diagonal capture. I have no concept whatsoever in my current code to allow for capturing.

We’ll need to make two changes:

  1. Define two different movement functions for pawns and change the initialization (the signature of piece_loop) to include two functions, one for movement, one for capture. For every piece except the pawn those would be the same.
  2. Include another atom to each move message (move vs. capture).

Digging in deeper

I’m tired of reconstructing an anonymous pawn movement function team time I want to run a test, and the function is about to get much more complicated, so I experiment a bit with how best to do it. I tried defining an anonymous function as a macro, but that doesn’t seem to work.

There are built-in data stores, ETS and DETS, but they seem like overkill.

The best approach I can come up with at the moment is to define a new function in my module, movefuns.

movefuns() ->
[ 
  { pawnmove, fun({ X, OldY }, { X, NewY }, white) when NewY - OldY =:= 1 -> { X, NewY };
                 ({ X, OldY }, { X, NewY }, black) when NewY - OldY =:= -1 -> { X, NewY };
                 ({ X, 2 }, { X, 4 }, white) -> { X, 4 };
                 ({ X, 7 }, { X, 5 }, black) -> { X, 5 };
                 (Loc, _, _) -> Loc end
  }
].

(The above is quite different from the pawn movement function I was defining from the shell. I think you’ll find it self-explanatory with a bit of persistence.)

Then, when I want to create a new Pawn process from the shell:

87> [ F ] = [ X || {pawnmove, X} <- piece:movefuns() ].
[#Fun<piece.0.83500532>]
88> Pawn = spawn(piece, piece_loop, [ F, {1, 7}, black ]).
<0.238.0>

How’s that first line for cryptic syntax? In Perl that’d be $F = $somehash{pawnmove}. We’re definitely in a brave new world.

See List Comprehensions from Learn You Some Erlang” for more.

Now we can test with our new function. Let’s move the black pawn two spaces forward, then try moving to the same spot it’s already in, then move two spaces forward again, then one space forward.

90> Pawn ! { self(), 1, 5 }.
{<0.189.0>,1,5}
91> flush().
Shell got {yes,{1,5}}
ok
92> Pawn ! { self(), 1, 5 }.
{<0.189.0>,1,5}
93> flush().
Shell got {no,{1,5}}
ok
94> Pawn ! { self(), 1, 3 }.
{<0.189.0>,1,3}
95> flush().
Shell got {no,{1,5}}
ok
96> Pawn ! { self(), 1, 4 }.
{<0.189.0>,1,4}
97> flush().
Shell got {yes,{1,4}}
ok

And the new code in piece_loop is trivial: we just pass in the team as a third argument to Maneuver.

{ From, X, Y } when X > 0, X < 9, Y > 0, Y < 9 ->
        piece_loop(Maneuver,
                   move_response(From, Location, Maneuver(Location, {X, Y}, Team)),
                   Team);

Capture my heart

Now we’re ready to tackle the pawn capture problem.

As we discussed above, we’re going to pass two movement functions when initializing a piece process.

Current code:

piece_loop(Maneuver, Location, Team) ->
    receive
        { From, X, Y } when X > 0, X < 9, Y > 0, Y < 9 ->
            piece_loop(Maneuver,
                       move_response(From, Location, Maneuver(Location, {X, Y}, Team)),
                       Team);
        { From, location } ->
            From ! Location,
            piece_loop(Maneuver, Location, Team);
        done ->
            done
    end.

New code:

piece_loop(Move, Capture, Location, Team) ->
    receive
    { From, move, X, Y } when X > 0, X < 9, Y > 0, Y < 9 ->
            piece_loop(Move, Capture,
                       move_response(From, Location, Move(Location, {X, Y}, Team)),
                       Team);
    { From, capture, X, Y } when X > 0, X < 9, Y > 0, Y < 9 ->
            piece_loop(Move, Capture,
                       move_response(From, Location, Capture(Location, {X, Y}, Team)),
                       Team);
    { From, location } ->
            From ! Location,
            piece_loop(Move, Capture, Location, Team);
    done ->
            done
    end.

Being explicit about the type of move makes sense for future needs; there’s no longer ambiguity about whether an enemy piece at the destination square is preventing a move.

A quick note on style here. Because the message sent back to the calling object is a nested tuple (e.g., {no, {1, 5}}, it makes me wonder whether my inbound messages should also have the destination square as a nested tuple, like {From, capture, {X, Y}}.4 I’ll probably make that change soon.

Back to my latest change, I still need to test the move vs. capture logic. To be thorough, let’s go ahead and define our pawncap anonymous function.

movefuns() ->
    [ 
      { pawnmove, fun({ X, OldY }, { X, NewY }, white) when NewY - OldY =:= 1 -> { X, NewY };
                     ({ X, OldY }, { X, NewY }, black) when NewY - OldY =:= -1 -> { X, NewY };
                     ({ X, 2 }, { X, 4 }, white) -> { X, 4 };
                     ({ X, 7 }, { X, 5 }, black) -> { X, 5 };
                     (Loc, _, _) -> Loc end
      },

      { pawncap, fun({ OldX, OldY }, { NewX, NewY }, white)
                       when abs(NewX - OldX) =:= 1, NewY - OldY =:= 1 -> { NewX, NewY };
                    ({ OldX, OldY }, { NewX, NewY }, black)
                       when abs(NewX - OldX) =:= 1, NewY - OldY =:= -1 -> { NewX, NewY };
                     (Loc, _, _) -> Loc end
      }
    ].

And now to the shell:

5> [ G ] = [ X || {pawncap, X} <- piece:movefuns() ].
[#Fun<piece.1.108329003>]
6> [ H ] = [ X || {pawnmove, X} <- piece:movefuns() ].
[#Fun<piece.0.108329003>]
7> Pawn = spawn(piece, piece_loop, [ H, G, {1, 7}, black ]).
<0.297.0>
8> Pawn ! { self(), 1, 5 }.
{<0.275.0>,1,5}
9> flush().
ok
10> Pawn ! { self(), move, 1, 5 }.
{<0.275.0>,move,1,5}
11> flush().
Shell got {yes,{1,5}}
ok
12> Pawn ! { self(), capture, 2, 4 }.
{<0.275.0>,capture,2,4}
13> flush().
Shell got {yes,{2,4}}
ok
14> Pawn ! { self(), capture, 2, 3 }.
{<0.275.0>,capture,2,3}
15> flush().
Shell got {no,{2,4}}
ok

Note that command 8 (sending a message the old style, without the move atom as an argument) did no good; no messages were sent back to the shell, because that message did not match any patterns. That message is still sitting in the mailbox for the Pawn process, although I don’t know how to examine it from the shell to verify that fact.


  1. See this Lambda the Ultimate thread for more discussion of why Erlang uses asynchronous messages.

  2. In all fairness, many OO languages allow for method overloading, and we could always introduce a differently-named method that takes more arguments, so this isn’t unique to Erlang.

  3. I’ve referred to matching/pattern matching several times without any explanation. In short, pattern matching makes pretty much everything in Erlang possible. See Learn You Some Erlang since I’m too lazy to explain it myself.

  4. In case it’s unclear, the only difference between {From, capture, Target} and {From, capture, {X, Y}} in my pattern match logic is that by breaking out the tuple explicitly, I can use the guards to compare X and Y against their bounds.

erlang chessboard
Previous post
Chessboard: Day 1 Time to take Erlang out for a spin.
Next post
Chessboard: Day 3 Short day.