Code Review 5: Bowling Kata, Erlang edition
So for this cycle of Erlang coding, I did a combination of coding and reading.
Code-wise, one of the most pertinent things I tried was extracting constants. My initial pass at this was to create a module with functions that returned the constants, but, as I thought, it lead me to an illegal guard expression compile error:
roll(on_frame, #frame{first_roll=First, second_roll=Second}, that_knocked_down, Pins_Knocked_Down) when (First =:= constants:all_pins_down()) and (Second =:= undefined) ->
{frame, #frame{first_roll = Pins_Knocked_Down, value = Pins_Knocked_Down}};
I quickly wised up and, through a combination of the Armstrong book and a Google search, I found that macros are the standard way to achieve constants in this language. Thusly:
-define(TRACE, 1). -define(INFO, 2). -define(ERROR, 3). -define(ALL_PINS_DOWN, 10).
-include("constants.hrl").
roll(on_frame, #frame{first_roll=First, second_roll=Second}, that_knocked_down, Pins_Knocked_Down) when (First =:= ?ALL_PINS_DOWN) and (Second =:= undefined) ->
{frame, #frame{first_roll = Pins_Knocked_Down, value = Pins_Knocked_Down}};
I also buttoned-up my hand-rolled logger. Or more accurately, I refined my usage of said logger. As I added more and more informative log messages, I found myself sorely needing some kind of printf analog, and its name is io_lib:format():
roller(
frame_roll_function, Roll,
frame_is_full_function, IsFull,
frame_rescore_function, Rescore,
frame_constructor_function, NewFrame,
logger_function, Log) when
is_function(Roll) and
is_function(IsFull) and
is_function(Rescore) and
is_function(NewFrame) and
is_function(Log) ->
{roller, fun(on_game, #game{frames=Frames} = G, knocked_down, PinsKnockedDown) when Frames =:= undefined ->
Log(info, io_lib:format("Rolling a ~w", [PinsKnockedDown]), with_code, ?INFO),
{game, roll_with_no_frames(G, PinsKnockedDown, Roll, NewFrame, Log)};
(on_game, #game{frames=Frames} = G, knocked_down, PinsKnockedDown) when Frames =/= undefined ->
Log(info, io_lib:format("Rolling a ~w", [PinsKnockedDown]), with_code, ?INFO),
{game, roll_with_frames(G, PinsKnockedDown, Roll, IsFull, Rescore, NewFrame, Log)}
end}.
I had to look up the ~w switch in favor of the ~p in order to keep the log message all on a single line. ~p includes “sensible” indentation.
Next on the agenda was to replace anonymous underscore variables with “named” anonymous variables, in the name of readability a la Armstrong:
roll(on_frame, #frame{first_roll=First} = Frame, that_knocked_down, Pins_Knocked_Down) when First =:= undefined ->
{frame, Frame#frame{first_roll = Pins_Knocked_Down, value = Pins_Knocked_Down}};
roll(on_frame, #frame{first_roll=First, second_roll=Second}, that_knocked_down, Pins_Knocked_Down) when (First =:= ?ALL_PINS_DOWN) and (Second =:= undefined) ->
{frame, #frame{first_roll = Pins_Knocked_Down, value = Pins_Knocked_Down}};
roll(on_frame, #frame{second_roll=Second, value=V} = Frame, that_knocked_down, Pins_Knocked_Down) when Second =:= undefined ->
{frame, Frame#frame{second_roll = Pins_Knocked_Down, value = V+Pins_Knocked_Down}};
roll(_on_frame, _Frame, _that_knocked_down, _Pins_Knocked_Down) ->
{error, frame_already_has_two_rolls}.
There actually weren’t very many other occurrences of this, but they are now gone.
I did do some reading in the book about Erlang concurrency. Eventually, I will move on to the generic server, but I want to understand the basics first. I have a tendency for that sort of thing.
When I felt I had read too much, I put the book down and started on the scoring server:
-module(score_server).
-export([
start/2
]).
-include("constants.hrl").
-include("game.hrl").
start(logger_function, Log) when is_function(Log) ->
spawn(fun() -> main_loop(Log) end).
main_loop(Log) ->
receive
{From, {score_of, Game}} when is_record(Game, game) ->
Log(info, io_lib:format("Server received score_of message: ~w", [Game]), with_code, ?INFO),
ScoreOf = get_scorer(),
From ! {self(), ScoreOf(Game)},
main_loop(Log)
end.
get_scorer() ->
{scorer, Scorer} = game:scorer(value_of_a_frame()),
Scorer.
value_of_a_frame() -> fun frame:value_of/1.
So that is my first pass. And I interacted with it as follows:
Erlang (BEAM) emulator version 5.6.5 [smp:2] [async-threads:0]
Eshell V5.6.5 (abort with ^G)
1> {logger, Log} = logger:new("testlog.txt").
{logger,#Fun<logger.0.60591536>}
2> rr(game).
[frame,game]
3> MyGame = #game{frames=[]}.
#game{frames = [],score = undefined}
4> Server = score_server:start(logger_function, Log).
<0.36.0>
5> Server ! {self(), {score_of, MyGame}}, receive {Server, Response} -> Response end.
{score,0}
For next time:
- I still have native coverage analysis on my back burner.
- I need to find a way to log process ID.
- I want to write a client of the scoring server so that I can have many clients and servers talking at the same time.
- Add more functionality to the server.
- Eventually get to reading about the generic server.
Now if you’ll excuse me, my scrum team needs me to be the villain at Sprint Zero.
![]() |
Announcer: You’re reading the EIP web-ring. |
