Crafty Erlang

An elegant language for small projects

Colin MacDonald

Synergistic Weirdness

Recursion

Recursion


 %% beginning
func(Input) ->
    Output = [],
    func(Input, Output).

Recursion

 %% beginning
func(Input) ->
    Output = [],
    func(Input, Output).

 %% end
func([], Output) -> lists:reverse(Output).

Recursion

 %% beginning
func(Input) ->
    Output = [],
    func(Input, Output).

 %% end
func([], Output) -> lists:reverse(Output).

 %% middle
func([First | Rest], Output) ->
    NewFirst = munge(First),
    func(Rest, [NewFirst | Output]);

Digression: Backwards Lists

Digression: Backwards Lists

Foo = [cat, dog].
Foo
|
cat - dog

Digression: Backwards Lists

Foo = [cat, dog].
Bar = [monkey | Foo].
Bar      Foo
|        |
monkey - cat - dog

Digression: Backwards Lists

Foo = [cat, dog].
Bar = [monkey | Foo].
Baz = [elephant, tiger | Foo].
Baz       Bar      Foo
|         |        |
|         monkey - cat - dog
|                 /
elephant - tiger /

Back to recursion: Bowling Game

Bowling Game

 %% Beginning: score/1 -> score/3
score(Rolls) ->
    Frame = 1,
    Score = 0,
    score(Rolls, Frame, Score).

Bowling Game

 %% Beginning
score(Rolls) ->
    score(Rolls, 1, 0).

 %% End
score(_Rolls, 11, Score) -> Score.

Bowling Game

 %% Beginning
score(Rolls) ->
    score(Rolls, 1, 0).

 %% End
score(_Rolls, 11, Score) -> Score;

 %% Middle
score([Roll1, Roll2 | Rest], Frame, Score) ->
    NewScore = Score + Roll1 + Roll2,
    score(Rest, Frame + 1, NewScore).

Bowling Game

 %% Beginning
score(Rolls) ->
    score(Rolls, 1, 0).

 %% End
score(_Rolls, 11, Score) -> Score;

 %% Strike
score([10 | Rest], Frame, Score) ->
    ...

 %% Spare
score([Roll1, Roll2 | Rest], Frame, Score) when Roll1 + Roll2 == 10 ->
    ...

 %% Normal
score([Roll1, Roll2 | Rest], Frame, Score) ->
    NewScore = Score + Roll1 + Roll2,
    score(Rest, Frame + 1, NewScore).

Bowling Game

 %% Beginning
score(Rolls) ->
    score(Rolls, 1, 0).

 %% End
score(_Rolls, 11, Score) -> Score;

 %% Strike
score([10 | Rest], Frame, Score) ->
    [Bonus1, Bonus2 | _] = Rest,
    NewScore = Score + 10 + Bonus1 + Bonus2,
    score(Rest, Frame + 1, NewScore);

 %% Spare
score([Roll1, Roll2 | Rest], Frame, Score) when Roll1 + Roll2 == 10 ->
    [Bonus1 | _] = Rest,
    NewScore = Score + 10 + Bonus1,
    score(Rest, Frame + 1, NewScore);

 %% Normal
score([Roll1, Roll2 | Rest], Frame, Score) ->
    NewScore = Score + Roll1 + Roll2,
    score(Rest, Frame + 1, NewScore).

Bowling Game

What about incomplete games?

Bowling Game

score(Rolls) -> score(Rolls, 1, 0).

score(_Rolls, 11, Score) -> Score;

score([10 | Rest], Frame, Score) ->
    score(Rest, Frame + 1, Score + 10 + strike_bonus(Rest));

score([Roll1, Roll2 | Rest], Frame, Score) when (Roll1 + Roll2 == 10) ->
    score(Rest, Frame + 1, Score + 10 + spare_bonus(Rest));

score([Roll1, Roll2 | Rest], Frame, Score) ->
    score(Rest, Frame + 1, Score + Roll1 + Roll2);

score([Roll1], _Frame, Score) -> Score + Roll1;
score([], _Frame, Score) -> Score.


spare_bonus([]) -> 0;
spare_bonus([Bonus1 | _Rest]) -> Bonus1.

strike_bonus([]) -> 0;
strike_bonus([Only]) -> Only;
strike_bonus([Bonus1, Bonus2 | _Rest]) -> Bonus1 + Bonus2.

Ok, that’s an algorithm

Sketching the CLI

Eshell V5.8.4  (abort with ^G)
1> Line = io:get_line("Next> ").
Next> 

Sketching the CLI

Eshell V5.8.4  (abort with ^G)
1> Line = io:get_line("Next> ").
Next> colin 4
"colin 4\n"

Sketching the CLI

Eshell V5.8.4  (abort with ^G)
1> Line = io:get_line("Next> ").
Next> colin 4
"colin 4\n"
2> [Player, RollText] = string:tokens(Line, " \t\n").
["colin","4"]

Sketching the CLI

Eshell V5.8.4  (abort with ^G)
1> Line = io:get_line("Next> ").
Next> colin 4
"colin 4\n"
2> [Player, RollText] = string:tokens(Line, " \t\n").
["colin","4"]
3> {Roll, _} = string:to_integer(RollText).
{4,[]}

Sketching the CLI

Eshell V5.8.4  (abort with ^G)
1> Line = io:get_line("Next> ").
Next> colin 4
"colin 4\n"
2> [Player, RollText] = string:tokens(Line, " \t\n").
["colin","4"]
3> {Roll, _} = string:to_integer(RollText).
{4,[]}
4> GameData = dict:new().
{dict,0,...

Sketching the CLI

Eshell V5.8.4  (abort with ^G)
1> Line = io:get_line("Next> ").
Next> colin 4
"colin 4\n"
2> [Player, RollText] = string:tokens(Line, " \t\n").
["colin","4"]
3> {Roll, _} = string:to_integer(RollText).
{4,[]}
4> GameData = dict:new().
{dict,0,...
5> NewGameData = dict:append(Player, Roll, GameData).
{dict,1,...

Sketching the CLI

Eshell V5.8.4  (abort with ^G)
1> Line = io:get_line("Next> ").
Next> colin 4
"colin 4\n"
2> [Player, RollText] = string:tokens(Line, " \t\n").
["colin","4"]
3> {Roll, _} = string:to_integer(RollText).
{4,[]}
4> GameData = dict:new().
{dict,0,...
5> NewGameData = dict:append(Player, Roll, GameData).
{dict,1,...
6> {ok, Rolls} = dict:find(Player, NewGameData).
{ok,[4]}

Sketching the CLI

Eshell V5.8.4  (abort with ^G)
1> Line = io:get_line("Next> ").
Next> colin 4
"colin 4\n"
2> [Player, RollText] = string:tokens(Line, " \t\n").
["colin","4"]
3> {Roll, _} = string:to_integer(RollText).
{4,[]}
4> GameData = dict:new().
{dict,0,...
5> NewGameData = dict:append(Player, Roll, GameData).
{dict,1,...
6> {ok, Rolls} = dict:find(Player, NewGameData).
{ok,[4]}
7> Score = bowling_game:score(Rolls).
4

Recurse!

loop(GameData) ->
    Line = io:get_line("Next> "),
    [Player, RollText] = string:tokens(Line, " \t\n"),
    {Roll, _} = string:to_integer(RollText),
    NewGameData = dict:append(Player, Roll, GameData),
    {ok, Rolls} = dict:find(Player, NewGameData).
    Score = bowling_game:score(Rolls).
    io:format("New score for ~s: ~p~n", [Player, Score]),
    loop(NewGameData).

Begin…

main([]) -> loop(dict:new()).

loop(GameData) ->
    ...

…with Escript!

#!/usr/local/bin/escript
#
# scorekeeper.erl 

-import(bowling_game).

main([]) -> loop(dict:new()).

loop(GameData) ->
    ...

Run it!

$ ./scorekeeper.erl 
Next>

Run it!

$ ./scorekeeper.erl 
Next> colin 3
New score for colin: 3
Next>

Run it!

$ ./scorekeeper.erl 
Next> colin 3
New score for colin: 3
Next> colin 4
New score for colin: 7
Next>

Run it!

$ ./scorekeeper.erl 
Next> colin 3
New score for colin: 3
Next> colin 4
New score for colin: 7
Next> colin 10
New score for colin: 17
Next>

Run it!

$ ./scorekeeper.erl 
Next> colin 3
New score for colin: 3
Next> colin 4
New score for colin: 7
Next> colin 10
New score for colin: 17
Next> colin 3
New score for colin: 23
Next> 

Webify!

Bowling Service

Remember the command-line loop?

loop(GameData) ->
    Line = io:get_line("Next> "),
    [Player, RollText] = string:tokens(Line, " \t\n"),

    {Roll, _} = string:to_integer(RollText),
    NewGameData = dict:append(Player, Roll, GameData),
    {ok, Rolls} = dict:find(Player, NewGameData).
    Score = bowling_game:score(Rolls).

    io:format("New score for ~s: ~p~n", [Player, Score]),

    loop(NewGameData).

Bowling Service

Here’s the message-handling loop.

loop(GameData) ->
    receive {From, {append, Player, RollText}} ->

        %% This is the same
        {Roll, _} = string:to_integer(RollText),
        NewGameData = dict:append(Player, Roll, GameData),
        {ok, Rolls} = dict:find(Player, NewGameData),
        Score = bowling_game:score(Rolls),

        From ! Score,

        loop(NewGameData)
    end.

Bowling Service

loop(GameData) ->
    receive {From, {append, Player, RollText}} ->
        {Roll, _} = string:to_integer(RollText),
        NewGameData = dict:append(Player, Roll, GameData),
        {ok, Rolls} = dict:find(Player, NewGameData),
        Score = bowling_game:score(Rolls),
        From ! Score,
        loop(NewGameData)
    end.

init() ->
    Data = dict:new(),
    Start = fun() -> loop(Data) end,
    spawn(Start).  % returns pid

Bowling Service

loop(GameData) ->
    receive {From, {append, Player, RollText}} ->
        {Roll, _} = string:to_integer(RollText),
        NewGameData = dict:append(Player, Roll, GameData),
        {ok, Rolls} = dict:find(Player, NewGameData),
        Score = bowling_game:score(Rolls),
        From ! Score,
        loop(NewGameData)
    end.

init() ->
    Data = dict:new(),
    Start = fun() -> loop(Data) end,
    spawn(Start).  % returns pid

append(Player, RollText, Pid) ->
    Pid ! {self(), {append, Player, RollText}},
    receive Resp -> Resp end.

REST API

http://localhost:8000/add/Player/Roll

e.g.

http://localhost:8000/add/colin/4

Spooky App

-module(bowling_web).
-behaviour(spooky).
-export([init/1, get/2]).  % Spooky API

Spooky App

-module(bowling_web).
-behaviour(spooky).
-export([init/1, get/2]).  % Spooky API
-import(bowling_service).

init([])->
    Pid = bowling_service:init(),
    register(bowl_svc, Pid),
    [{port, 8000}].

Spooky App

-module(bowling_web).
-behaviour(spooky).
-export([init/1, get/2]).  % Spooky API
-import(bowling_service).

init([])->
    register(bowl_svc, bowling_service:init()),
    [{port, 8000}].

 %% REST handler
get(_Req, ["add", Player, RollText])->
    ...

Spooky App

-module(bowling_web).
-behaviour(spooky).
-export([init/1, get/2]).  % Spooky API
-import(bowling_service).

init([])->
    register(bowl_svc, bowling_service:init()),
    [{port, 8000}].

 %% REST handler
get(_Req, ["add", Player, RollText])->
    Score = bowling_service:append(Player, RollText, bowl_svc),
    {200, io_lib:format("~p", [Score])};

Spooky App

-module(bowling_web).
-behaviour(spooky).
-export([init/1, get/2]).  % Spooky API
-import(bowling_service).

init([])->
    register(bowl_svc, bowling_service:init()),
    [{port, 8000}].

 %% REST handler
get(_Req, ["add", Player, RollText])->
    Score = bowling_service:append(Player, RollText, bowl_svc),
    {200, io_lib:format("~p", [Score])};

 %% static page handlers
get(Req, [])-> get(Req, ["form.html"]);  % main page

get(_Req, Path)->  % other static resources
    Filename = filename:join(Path),
    case file:read_file(Filename) of
        {ok, PageBytes} -> {200, binary_to_list(PageBytes)};
        {error, Reason} -> {404, Reason}
    end.

Run it!

$ erl -pa $SPOOKY/ebin -pa $SPOOKY/deps/*/ebin
...
Eshell V5.8.4  (abort with ^G)
1> spooky:start_link(bowling_web).
{ok,<0.35.0>}
2> 

…with Escript!

#!/usr/local/bin/escript

main([]) ->
    SpookyDir = os:getenv("SPOOKY_DIR"),
    %% Add spooky and its dependencies to the code path.
    true = code:add_path(SpookyDir ++ "/ebin"),
    Deps = filelib:wildcard(SpookyDir ++ "/deps/*/ebin"),
    ok = code:add_paths(Deps),

    %% Compile our modules, just to be safe.
    c:c(bowling_game),
    c:c(bowling_service),
    c:c(bowling_web),

    spooky:start_link(bowling_web),
    io:format("Started spooky~n"),

    io:get_line("Return to exit...  "),
    spooky:stop().

REST interaction

add/colin/3

REST interaction

add/colin/4

REST interaction

add/colin/10

REST interaction

add/colin/3

Webapp interaction

/

Webapp interaction

add player - client side

Webapp interaction

add/colin/3

Webapp interaction

add/colin/3

Webapp interaction

add/colin/4

Webapp interaction

add/colin/10

Webapp interaction

add/colin/3

Ta-dah!

Extra Stuff

Crafty Erlang

Think small, have fun!

Colin MacDonald
colin@bluegraybox.com