Proper way to implement (Mnesia-style / wildcard / don't care) tuple matching (Erlang)

247 views Asked by At

I'm implementing a Chess game (Chinese Chess, aka. Xiangqi, to be exact) in Erlang.

A piece is represented by a {Color, Type} tuple, and a point (ie. location) is represented by a {File, Rank} tuple. The board is represented by a point-to-piece map (ie. #{point() => piece()}).

There is a function to query whether a particular point on the board is occupied by a piece or not:

is_point_occupied_simple(Board, Point) ->
    ensure_is_point(Point),
    case maps:find(Point, Board) of
        {ok, _} ->
            true;
        error ->
            false
    end.

However, I would like to add an optional parameter to check the color of the piece - if the point is occupied by a piece of the specified color, the function returns true; otherwise it returns false. If I don't care about the color of the piece, I can just put '_' in the TargetColor parameter (or, equivalently, invoke is_point_occupied/2):

is_point_occupied(Board, Point) ->
    is_point_occupied(Board, Point, '_').

is_point_occupied(Board, Point, '_') ->
    ensure_is_point(Point),
    case maps:find(Point, Board) of
        {ok, _} ->
            true;
        error ->
            false
    end;

is_point_occupied(Board, Point, TargetColor) ->
    ensure_is_point(Point),
    ensure_is_color(TargetColor),
    case maps:find(Point, Board) of
        {ok, {TargetColor, _}} ->
            true;
        {ok, _} ->
            false;
        error ->
            false
    end.

I don't like the above implementation because of the large proportion of copy-and-paste, so I simplified the above function like this:

is_point_occupied_2(Board, Point) ->
    is_point_occupied_2(Board, Point, '_').

is_point_occupied_2(Board, Point, TargetColor) ->
    ensure_is_point(Point),
    ensure_is_color_or_wildcard(TargetColor),
    case maps:find(Point, Board) of
        {ok, {TargetColor, _}} ->
            true;
        {ok, _} ->
            is_wildcard(TargetColor);
        error ->
            false
    end.

The function is_wildcard/1 is simply a one-liner:

is_wildcard(Wildcard) -> Wildcard =:= '_'.

Now, I would like to go further to replace TargetColor by TargetPiece, which is a {TargetColor, TargetType} tuple. None, one or both of the tuple elements may be a wildcard ('_'). I found it difficult to write the case clauses. I also notice that to match an n-tuple allowing "don't care" in this way, there needs 2n clauses. So obviously it is not the proper way to implement this.

Does anyone have better ideas?

PS: I didn't include the source of all functions since those I didn't include are trivial to implement in my opinion. Please leave a comment below if you're interested. Thanks!

2

There are 2 answers

1
Pascal On

I would change the chess board representation, filling the whole board by either a piece {Color,Type} or no piece {none,none} so your code can be more regular:

-module (testmatch).

-compile([export_all]).

is_point_occupied(Board,Point) -> not match(maps:find(Point, Board),{none,none}).
is_point_occupied(Board,Point,Color) -> match(maps:find(Point, Board),{Color,'_'}).
is_point_occupied(Board,Point,Color,Type) -> match(maps:find(Point, Board),{Color,Type}).

match({ok,{C,T}},{C,T}) -> true;
match({ok,{_C,T}},{'_',T}) -> true;
match({ok,{C,_T}},{C,'_'}) -> true;
match(_,_) -> false.

test() ->
    Board = #{1 =>{none,none}, 2 =>{black,king}, 3 => {white,tower} },
    false = is_point_occupied(Board,1),
    true = is_point_occupied(Board,2),
    false = is_point_occupied(Board,2,red),
    true = is_point_occupied(Board,2,black),
    false = is_point_occupied(Board,3,'_',king),
    true = is_point_occupied(Board,3,'_',tower),
    true = is_point_occupied(Board,2,black,king),
    false = is_point_occupied(Board,2,black,tower),
    false = is_point_occupied(Board,2,white,king).
0
Siu Ching Pong -Asuka Kenji- On

My solution to this is to implement an auxiliary function for matching, which makes the 2n case clauses mentioned above into n orelse clauses plus n-1 andalso clauses:

match_piece({Color, Type} = Piece, {TargetColor, TargetType} = TargetPiece) ->
    ensure_is_piece(Piece),
    ensure_is_piece_or_wildcard(TargetPiece),
    (is_wildcard(TargetColor) orelse Color =:= TargetColor)
        andalso (is_wildcard(TargetType) orelse Type =:= TargetType).

The main function doesn't change much:

is_point_occupied_3(Board, Point) ->
    is_point_occupied_3(Board, Point, {'_', '_'}).

is_point_occupied_3(Board, Point, TargetPiece) ->
    ensure_is_point(Point),
    ensure_is_piece_or_wildcard(TargetPiece),
    case maps:find(Point, Board) of
        {ok, Piece} ->
            match_piece(Piece, TargetPiece);
        error ->
            false
    end.

Is there any better / alternative ideas?