2012-09-02

What are we doing here?

A few thoughts since last we spoke…

Upon further perusal of Armstrong’s Programming Erlang, I discovered (again?) that there is a process dictionary built into Erlang.

Theoretically, instead of defining a data structure with move functions, the process could just store each function in its dictionary and pull it out when asked. We’ll give that a shot today just for the practice.

However, Armstrong is quite negative about the feature because it violates a basic premise of functional programming; it allows for side-effects.

It also makes it more difficult to re-use this code for other types of board games. Think how easy it would be to use this for checkers1 instead of chess, especially if we put movefuns/02 into a different file. We just pass checkers move functions into piece_loop/4.

Of potentially more interest to me personally would be an implementation of Chinese Chess, but the guards currently prevent such a use (the board is effectively 9x9), so we also need to think about how to allow for varying board sizes. Perhaps we find another mechanism than guards that could handle non-square board layouts.

Stream of consciousness

Process dictionary

So let’s give the built-in dictionary a try. The syntax is simple: put(atom, value) and get(atom).

We’ll define a new init/0 function that stores each currently-defined move function as a list (we’ll see why a list in a moment):

init() ->
    [ PM ] = [ X || {pawnmove, X} <- movefuns() ],
    [ PC ] = [ X || {pawncap, X} <- movefuns() ],
    [ KM ] = [ X || {kingmove, X} <- movefuns() ],
    [ KnM ] = [ X || {knightmove, X} <- movefuns() ],

    put(pawn, [ PM, PC ]),
    put(king, [ KM, KM ]),
    put(knight, [ KnM, KnM ]).

movefuns/0 would presumably go away at some point, but right now this is strictly a proof of concept.

It may be obvious why we stored the functions as a list: we want to start this process with a piece name. Instead of calling spawn/3 with piece_loop/4 as our initial function, we’ll create a new one named start/3:

start(Piece, Location, Team) ->
    init(),
    [Move, Cap] = get(Piece),
    piece_loop(Move, Cap, Location, Team).

And from the shell:

2> Pawn = spawn(piece, start, [pawn, {2, 2}, white ]).
<0.38.0>
3> Pawn ! { self(), location }.
{<0.31.0>,location}
4> flush().
Shell got {2,2}
ok
5> Pawn ! { self(), move, 2, 4 }.
{<0.31.0>,move,2,4}
6> flush().
Shell got {yes,{2,4}}
ok
7> Pawn ! { self(), capture, 3, 5 }.
{<0.31.0>,capture,3,5}
8> flush().
Shell got {yes,{3,5}}
ok
9> Pawn ! { self(), capture, 3, 6 }.
{<0.31.0>,capture,3,6}
10> flush().
Shell got {no,{3,5}}
ok

Looks good. Out of curiosity, let’s see what happens if we try to create a piece which isn’t yet defined:

11> Foo = spawn(piece, start, [foo, {2, 4}, white ]).
<0.48.0>
12> 
=ERROR REPORT==== 2-Sep-2012::14:37:55 ===
Error in process <0.48.0> with exit value: ? My vote is for the latter.

So:

{ _, _, {X, Y} } ->
    throw({invalid_destination, {X, Y}});

Which, when we try to force a piece to move off-board to {0, 3}, give us:

=ERROR REPORT==== 2-Sep-2012::15:20:44 ===
Error in process <0.71.0> with exit value: },[{piece,piece_loop,4,[{file,"/Users/jdaily/github/local/Erlang-Chessboard/piece.erl"},{line,33}]}]}

We’ll do the same for the unknown message throw. Latest version of piece_loop/4:

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);
        { _, _, {X, Y} } ->
            throw({invalid_destination, {X, Y}});
        { From, location } ->
            From ! Location,
            piece_loop(Move, Capture, Location, Team);
        done ->
            done;
        Message ->
            throw({unknown_message, Message})
    end.

Promotion

While mulling over the prospect of implementing checkers with this same code base, it occurred to me that this is a good opportunity to show how (conceptually) easy it is to upgrade running code.

Ready? We’ll just define a new message pattern:

{ promote, NewMove, NewCapture } ->
    piece_loop(NewMove, NewCapture, Location, Team);

Tough gig, this Erlang stuff.

This highlights a gap in our current code that may or may not be important in the future: this process has no idea what type of piece it is, other than knowing what its movement functions are.

That’s it for now, although it’s possible I might do another session later tonight. As a reminder, the code can always be found on GitHub: https://github.com/macintux/Erlang-Chessboard


  1. I’m not entirely sure how easy it would be to take the model as currently defined and apply it to checkers. There’s an implicit assumption that seems embedded: if we’re capturing a piece, that piece is at our destination square. Perhaps a capture at square {4, 5} with a source of {2, 3} implies a piece at {3, 4}.

  2. Up to this point I haven’t used the syntax name/arity for Erlang functions, but since that’s the standard way to describe them, I’ll use that from this point forward.

  3. The code that fails (line 23), if you’re curious, is the invocation of get/1: [Move, Cap] = get(Piece)

  4. Per Wikipedia, ἀνωνυμία is Greek for namelessness, whence our word anonymity.”

erlang chessboard
Previous post
More Erlang Resources Maybe I need a wiki instead of a blog.
Next post
@drkrab on Erlang Of paradigm shifts and finer things.