I started learning Erlang by way of Bruce Tate’s book from Pragmatic Programmers, Seven Languages in Seven Weeks. I’m sure it’s not my original inspiration, because I skipped a few languages to reach it, but at this point I’m not sure what was.
I set the book aside once I reached the end of Day 2, because I couldn’t figure out how to do the bonus problem in a clean manner, and I’m easily frustrated. (Not a good characteristic for a programmer, I’ll readily admit.)
After reading Solving Embarrassingly Obvious Problems In Erlang, a blog post from Garrett Smith on refactoring/simplifying Erlang, I decided I should take another look at it.
Take a list or tuple representing a tic-tac-toe board and determine who, if anyone, has won the game. Straightforward enough.
The atom cat
is used as a return value indicate that the board is fully occupied but no one has won; the atom no_winner
means no one has won but open spots remain.
I chose to use undef
as the atom to indicate an open spot on the board, and x
and o
are used as pieces.
Pattern matching makes this a fairly straightforward task, but it’s very repetitive code. Again, though, after reading Garrett’s post, I’m no longer afraid of making long code, so long as the functions themselves are very short.
And, in fact, these functions are remarkably short. Not a single function is longer than one line of code.
I think the code speaks for itself.
-module(ttt).
-include_lib("eunit/include/eunit.hrl").
-export([ttt/1]).
%% Throughout this I use S for side
ttt(List) ->
check_open_unless(List,
check_diag_unless(List,
check_vertical_unless(List,
check_horiz(List)))).
check_horiz([]) ->
{ no };
check_horiz([S, S, S | _Tail]) when S =/= undef ->
{ yes, S };
check_horiz([_, _, _| Tail]) ->
check_horiz(Tail).
check_vertical_unless(_List, {yes, S}) ->
{ yes, S };
check_vertical_unless(List, {no}) ->
check_vert(List).
check_vert([ S, _, _, S, _, _, S, _, _]) when S =/= undef ->
{ yes, S };
check_vert([ _, S, _, _, S, _, _, S, _]) when S =/= undef ->
{ yes, S };
check_vert([ _, _, S, _, _, S, _, _, S]) when S =/= undef ->
{ yes, S };
check_vert(_List) ->
{ no }.
check_diag_unless(_List, {yes, S}) ->
{ yes, S };
check_diag_unless(List, {no}) ->
check_diag(List).
check_diag([ S, _, _, _, S, _, _, _, S ]) when S =/= undef ->
{ yes, S };
check_diag([ _, _, S, _, S, _, S, _, _]) when S =/= undef ->
{ yes, S };
check_diag(_List) ->
{ no }.
check_open_unless(_List, {yes, S}) ->
{ yes, S };
check_open_unless(List, {no}) ->
check_open(List).
%%
%% Note that this function will return cat if there are no open
%% squares, regardless of whether there's a winner.
check_open([]) ->
cat;
check_open([undef | _Tail]) ->
no_winner;
check_open([_H | Tail]) ->
check_open(Tail).
%%
%% Test functions
no_horiz_test() ->
{ no } = check_horiz([x, o, o, o, x, x, o, x, o]).
horiz_1_test() ->
{ yes, o } = check_horiz([x, o, o, x, o, x, o, o, o]).
horiz_2_test() ->
{ yes, x } = check_horiz([x, o, o, x, x, x, o, x, o]).
no_diag_test() ->
{ no } = check_diag([x, o, o, x, o, o, x, o, o]).
diag_1_test() ->
{ yes, o } = check_diag([x, o, o, x, o, o, o, x, o]).
diag_2_test() ->
{ yes, x } = check_diag([x, o, x, o, x, o, x, x, o]).
no_vert_test() ->
{ no } = check_vert([x, o, o, x, o, o, o, x, x]).
vert_1_test() ->
{ yes, o } = check_vert([x, o, o, x, o, o, o, x, o]).
vert_2_test() ->
{ yes, x } = check_vert([x, o, x, o, x, x, o, o, x]).
undef_1_test() ->
{ no } = check_vert([x, o, undef, o, x, undef, o, o, undef]).
done_test() ->
cat = check_open([x, o, x, o, x, x, o, o, x]).
open_test() ->
no_winner = check_open([x, o, x, o, undef, x, o, o, x]).
full_open_test() ->
no_winner = ttt([undef, o, x, o, undef, x, o, undef, o]).
full_done_test() ->
cat = check_open([x, o, x, o, x, x, o, x, o]).
full_vert_test() ->
{ yes, o } = ttt([x, o, o, x, o, o, o, x, o]).
erlang
7langs