aboutsummaryrefslogtreecommitdiff
path: root/learn-you-some-erlang
diff options
context:
space:
mode:
Diffstat (limited to 'learn-you-some-erlang')
-rw-r--r--learn-you-some-erlang/LICENSE.txt22
-rw-r--r--learn-you-some-erlang/README.txt17
-rw-r--r--learn-you-some-erlang/band_supervisor.erl41
-rw-r--r--learn-you-some-erlang/calc.erl51
-rw-r--r--learn-you-some-erlang/cards.erl16
-rw-r--r--learn-you-some-erlang/cases.erl22
-rw-r--r--learn-you-some-erlang/cat_fsm.erl23
-rw-r--r--learn-you-some-erlang/convert.erl14
-rw-r--r--learn-you-some-erlang/ct/demo/basic_SUITE.erl13
-rw-r--r--learn-you-some-erlang/ct/demo/state_SUITE.erl24
-rw-r--r--learn-you-some-erlang/ct/dist.spec14
-rw-r--r--learn-you-some-erlang/ct/meeting/meeting.erl45
-rwxr-xr-xlearn-you-some-erlang/ct/meeting/meeting_SUITE.erl51
-rw-r--r--learn-you-some-erlang/ct/spec.spec7
-rw-r--r--learn-you-some-erlang/curling.erl37
-rw-r--r--learn-you-some-erlang/curling_accumulator.erl35
-rw-r--r--learn-you-some-erlang/curling_feed.erl24
-rw-r--r--learn-you-some-erlang/curling_scoreboard.erl32
-rw-r--r--learn-you-some-erlang/curling_scoreboard_hw.erl19
-rw-r--r--learn-you-some-erlang/discrep1.erl6
-rw-r--r--learn-you-some-erlang/discrep2.erl12
-rw-r--r--learn-you-some-erlang/discrep3.erl14
-rw-r--r--learn-you-some-erlang/discrep4.erl21
-rw-r--r--learn-you-some-erlang/dog_fsm.erl42
-rw-r--r--learn-you-some-erlang/dolphins.erl34
-rw-r--r--learn-you-some-erlang/erlcount-1.0/Emakefile4
-rw-r--r--learn-you-some-erlang/erlcount-1.0/ebin/erlcount.app13
-rw-r--r--learn-you-some-erlang/erlcount-1.0/src/erlcount.erl9
-rw-r--r--learn-you-some-erlang/erlcount-1.0/src/erlcount_counter.erl35
-rw-r--r--learn-you-some-erlang/erlcount-1.0/src/erlcount_dispatch.erl86
-rw-r--r--learn-you-some-erlang/erlcount-1.0/src/erlcount_lib.erl55
-rw-r--r--learn-you-some-erlang/erlcount-1.0/src/erlcount_sup.erl17
-rw-r--r--learn-you-some-erlang/erlcount-1.0/test/erlcount_tests.erl35
-rw-r--r--learn-you-some-erlang/exceptions.erl76
-rw-r--r--learn-you-some-erlang/fifo.erl15
-rw-r--r--learn-you-some-erlang/fifo_types.erl35
-rw-r--r--learn-you-some-erlang/functions.erl17
-rw-r--r--learn-you-some-erlang/guards.erl15
-rw-r--r--learn-you-some-erlang/hhfuns.erl111
-rw-r--r--learn-you-some-erlang/keyval_benchmark.erl192
-rw-r--r--learn-you-some-erlang/kitchen.erl64
-rw-r--r--learn-you-some-erlang/kitty_gen_server.erl57
-rw-r--r--learn-you-some-erlang/kitty_server.erl74
-rw-r--r--learn-you-some-erlang/kitty_server2.erl49
-rw-r--r--learn-you-some-erlang/linkmon.erl83
-rw-r--r--learn-you-some-erlang/m8ball/Emakefile2
-rw-r--r--learn-you-some-erlang/m8ball/config/a.config9
-rw-r--r--learn-you-some-erlang/m8ball/config/b.config9
-rw-r--r--learn-you-some-erlang/m8ball/config/c.config9
-rw-r--r--learn-you-some-erlang/m8ball/ebin/m8ball.app13
-rw-r--r--learn-you-some-erlang/m8ball/logs/.track-this0
-rw-r--r--learn-you-some-erlang/m8ball/src/m8ball.erl24
-rw-r--r--learn-you-some-erlang/m8ball/src/m8ball_server.erl46
-rw-r--r--learn-you-some-erlang/m8ball/src/m8ball_sup.erl16
-rw-r--r--learn-you-some-erlang/m8ball/test/dist_m8ball_SUITE.erl32
-rw-r--r--learn-you-some-erlang/m8ball/test/dist_m8ball_SUITE_data/backup.config9
-rw-r--r--learn-you-some-erlang/m8ball/test/dist_m8ball_SUITE_data/main.config9
-rw-r--r--learn-you-some-erlang/m8ball/test/m8ball.spec5
-rw-r--r--learn-you-some-erlang/m8ball/test/m8ball_dist.spec20
-rw-r--r--learn-you-some-erlang/m8ball/test/m8ball_server_SUITE.erl45
-rw-r--r--learn-you-some-erlang/mafiapp-1.0.0/Emakefile2
-rw-r--r--learn-you-some-erlang/mafiapp-1.0.0/ebin/mafiapp.app5
-rw-r--r--learn-you-some-erlang/mafiapp-1.0.0/logs/.keep-this0
-rw-r--r--learn-you-some-erlang/mafiapp-1.0.0/mafiapp.spec3
-rw-r--r--learn-you-some-erlang/mafiapp-1.0.0/src/mafiapp.erl172
-rw-r--r--learn-you-some-erlang/mafiapp-1.0.0/src/mafiapp_sup.erl12
-rw-r--r--learn-you-some-erlang/mafiapp-1.0.0/test/mafiapp.spec3
-rw-r--r--learn-you-some-erlang/mafiapp-1.0.0/test/mafiapp_SUITE.erl128
-rw-r--r--learn-you-some-erlang/mafiapp-1.0.1/Emakefile2
-rw-r--r--learn-you-some-erlang/mafiapp-1.0.1/ebin/mafiapp.app5
-rw-r--r--learn-you-some-erlang/mafiapp-1.0.1/logs/.please-track-this0
-rw-r--r--learn-you-some-erlang/mafiapp-1.0.1/mafiapp.spec3
-rw-r--r--learn-you-some-erlang/mafiapp-1.0.1/src/mafiapp.erl142
-rw-r--r--learn-you-some-erlang/mafiapp-1.0.1/src/mafiapp_sup.erl12
-rw-r--r--learn-you-some-erlang/mafiapp-1.0.1/test/mafiapp.spec3
-rw-r--r--learn-you-some-erlang/mafiapp-1.0.1/test/mafiapp_SUITE.erl128
-rw-r--r--learn-you-some-erlang/multiproc.erl39
-rw-r--r--learn-you-some-erlang/musicians.erl84
-rw-r--r--learn-you-some-erlang/my_server.erl42
-rw-r--r--learn-you-some-erlang/oop.erl28
-rw-r--r--learn-you-some-erlang/ops.erl4
-rw-r--r--learn-you-some-erlang/ops_tests.erl25
-rw-r--r--learn-you-some-erlang/pool/ppool.erl25
-rw-r--r--learn-you-some-erlang/pool/ppool_nagger.erl50
-rw-r--r--learn-you-some-erlang/pool/ppool_serv.erl113
-rw-r--r--learn-you-some-erlang/pool/ppool_sup.erl17
-rw-r--r--learn-you-some-erlang/pool/ppool_supersup.erl31
-rw-r--r--learn-you-some-erlang/pool/ppool_tests.erl200
-rw-r--r--learn-you-some-erlang/pool/ppool_worker_sup.erl15
-rw-r--r--learn-you-some-erlang/ppool-1.0/Emakefile2
-rw-r--r--learn-you-some-erlang/ppool-1.0/ebin/ppool.app6
-rw-r--r--learn-you-some-erlang/ppool-1.0/src/ppool.erl26
-rw-r--r--learn-you-some-erlang/ppool-1.0/src/ppool_serv.erl112
-rw-r--r--learn-you-some-erlang/ppool-1.0/src/ppool_sup.erl17
-rw-r--r--learn-you-some-erlang/ppool-1.0/src/ppool_supersup.erl22
-rw-r--r--learn-you-some-erlang/ppool-1.0/src/ppool_worker_sup.erl15
-rw-r--r--learn-you-some-erlang/ppool-1.0/test/ppool_nagger.erl50
-rw-r--r--learn-you-some-erlang/ppool-1.0/test/ppool_tests.erl200
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.0.0/Emakefile2
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.0.0/ebin/.this-file-intentionally-left-blank0
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.0.0/ebin/processquest.app7
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.0.0/include/.this-file-intentionally-left-blank0
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.0.0/priv/.this-file-intentionally-left-blank0
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_enemy.erl17
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_events.erl49
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_market.erl77
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_player.erl175
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_stats.erl19
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_sup.erl31
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_supersup.erl28
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/processquest.erl39
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/pq_enemy_tests.erl33
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/pq_events_handler.erl24
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/pq_events_tests.erl55
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/pq_market_tests.erl15
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/pq_player_tests.erl303
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/pq_stats_tests.erl28
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/processquest_tests.erl49
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.1.0/Emakefile2
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.1.0/ebin/.this-file-intentionally-left-blank0
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.1.0/ebin/processquest.app8
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.1.0/ebin/processquest.appup9
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.1.0/include/.this-file-intentionally-left-blank0
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.1.0/priv/.this-file-intentionally-left-blank0
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_enemy.erl32
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_events.erl53
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_market.erl77
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_player.erl216
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_quest.erl15
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_stats.erl19
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_sup.erl31
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_supersup.erl28
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/processquest.erl39
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_enemy_tests.erl33
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_events_handler.erl24
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_events_tests.erl55
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_market_tests.erl15
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_player_tests.erl395
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_quest_tests.erl30
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_stats_tests.erl28
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/processquest_tests.erl49
-rw-r--r--learn-you-some-erlang/processquest/apps/regis-1.0.0/Emakefile2
-rw-r--r--learn-you-some-erlang/processquest/apps/regis-1.0.0/ebin/regis.app7
-rw-r--r--learn-you-some-erlang/processquest/apps/regis-1.0.0/src/regis.erl29
-rw-r--r--learn-you-some-erlang/processquest/apps/regis-1.0.0/src/regis_server.erl98
-rw-r--r--learn-you-some-erlang/processquest/apps/regis-1.0.0/src/regis_sup.erl18
-rw-r--r--learn-you-some-erlang/processquest/apps/regis-1.0.0/test/regis_server_tests.erl120
-rw-r--r--learn-you-some-erlang/processquest/apps/regis-1.0.0/test/regis_tests.erl18
-rw-r--r--learn-you-some-erlang/processquest/apps/sockserv-1.0.0/Emakefile2
-rw-r--r--learn-you-some-erlang/processquest/apps/sockserv-1.0.0/ebin/sockserv.app11
-rw-r--r--learn-you-some-erlang/processquest/apps/sockserv-1.0.0/src/sockserv.erl13
-rw-r--r--learn-you-some-erlang/processquest/apps/sockserv-1.0.0/src/sockserv_pq_events.erl25
-rw-r--r--learn-you-some-erlang/processquest/apps/sockserv-1.0.0/src/sockserv_serv.erl153
-rw-r--r--learn-you-some-erlang/processquest/apps/sockserv-1.0.0/src/sockserv_sup.erl32
-rw-r--r--learn-you-some-erlang/processquest/apps/sockserv-1.0.0/src/sockserv_trans.erl58
-rw-r--r--learn-you-some-erlang/processquest/apps/sockserv-1.0.1/Emakefile2
-rw-r--r--learn-you-some-erlang/processquest/apps/sockserv-1.0.1/ebin/sockserv.app11
-rw-r--r--learn-you-some-erlang/processquest/apps/sockserv-1.0.1/ebin/sockserv.appup4
-rw-r--r--learn-you-some-erlang/processquest/apps/sockserv-1.0.1/src/sockserv.erl13
-rw-r--r--learn-you-some-erlang/processquest/apps/sockserv-1.0.1/src/sockserv_pq_events.erl25
-rw-r--r--learn-you-some-erlang/processquest/apps/sockserv-1.0.1/src/sockserv_serv.erl154
-rw-r--r--learn-you-some-erlang/processquest/apps/sockserv-1.0.1/src/sockserv_sup.erl32
-rw-r--r--learn-you-some-erlang/processquest/apps/sockserv-1.0.1/src/sockserv_trans.erl58
-rw-r--r--learn-you-some-erlang/processquest/processquest-1.0.0.config21
-rw-r--r--learn-you-some-erlang/processquest/processquest-1.1.0.config21
-rw-r--r--learn-you-some-erlang/processquest/rel/.this-directory-must-be-tracked0
-rw-r--r--learn-you-some-erlang/records.erl39
-rw-r--r--learn-you-some-erlang/records.hrl4
-rw-r--r--learn-you-some-erlang/recursive.erl135
-rw-r--r--learn-you-some-erlang/release/erlcount-1.0.config20
-rw-r--r--learn-you-some-erlang/release/erlcount-1.0.rel7
-rw-r--r--learn-you-some-erlang/release/erlcount-1.0/Emakefile4
-rw-r--r--learn-you-some-erlang/release/erlcount-1.0/ebin/erlcount.app14
-rw-r--r--learn-you-some-erlang/release/erlcount-1.0/src/erlcount.erl9
-rw-r--r--learn-you-some-erlang/release/erlcount-1.0/src/erlcount_counter.erl35
-rw-r--r--learn-you-some-erlang/release/erlcount-1.0/src/erlcount_dispatch.erl86
-rw-r--r--learn-you-some-erlang/release/erlcount-1.0/src/erlcount_lib.erl55
-rw-r--r--learn-you-some-erlang/release/erlcount-1.0/src/erlcount_sup.erl17
-rw-r--r--learn-you-some-erlang/release/erlcount-1.0/test/erlcount_tests.erl35
-rw-r--r--learn-you-some-erlang/release/erlcount-sm.config17
-rw-r--r--learn-you-some-erlang/release/ppool-1.0/Emakefile2
-rw-r--r--learn-you-some-erlang/release/ppool-1.0/ebin/ppool.app8
-rw-r--r--learn-you-some-erlang/release/ppool-1.0/src/ppool.erl26
-rw-r--r--learn-you-some-erlang/release/ppool-1.0/src/ppool_serv.erl113
-rw-r--r--learn-you-some-erlang/release/ppool-1.0/src/ppool_sup.erl17
-rw-r--r--learn-you-some-erlang/release/ppool-1.0/src/ppool_supersup.erl22
-rw-r--r--learn-you-some-erlang/release/ppool-1.0/src/ppool_worker_sup.erl15
-rw-r--r--learn-you-some-erlang/release/ppool-1.0/test/ppool_nagger.erl50
-rw-r--r--learn-you-some-erlang/release/ppool-1.0/test/ppool_tests.erl200
-rw-r--r--learn-you-some-erlang/release/rel/.this-file-intentionally-left-blank0
-rw-r--r--learn-you-some-erlang/reminder/Emakefile4
-rw-r--r--learn-you-some-erlang/reminder/ebin/.this-file-intentionally-left-blank1
-rw-r--r--learn-you-some-erlang/reminder/include/.this-file-intentionally-left-blank1
-rw-r--r--learn-you-some-erlang/reminder/priv/.this-file-intentionally-left-blank1
-rw-r--r--learn-you-some-erlang/reminder/src/.this-file-intentionally-left-blank1
-rw-r--r--learn-you-some-erlang/reminder/src/dev_event.erl82
-rw-r--r--learn-you-some-erlang/reminder/src/event.erl61
-rw-r--r--learn-you-some-erlang/reminder/src/evserv.erl168
-rw-r--r--learn-you-some-erlang/reminder/src/sup.erl22
-rw-r--r--learn-you-some-erlang/reminder/src/tests/dev_event_tests.erl41
-rw-r--r--learn-you-some-erlang/reminder/src/tests/event_tests.erl43
-rw-r--r--learn-you-some-erlang/reminder/src/tests/evserv_tests.erl203
-rw-r--r--learn-you-some-erlang/reminder/src/tests/sup_tests.erl25
-rw-r--r--learn-you-some-erlang/road.erl39
-rw-r--r--learn-you-some-erlang/road.txt12
-rw-r--r--learn-you-some-erlang/tester.erl55
-rw-r--r--learn-you-some-erlang/tests/calc_tests.erl6
-rw-r--r--learn-you-some-erlang/tests/cases_tests.erl24
-rw-r--r--learn-you-some-erlang/tests/cat_fsm_tests.erl21
-rw-r--r--learn-you-some-erlang/tests/dog_fsm_tests.erl81
-rw-r--r--learn-you-some-erlang/tests/dolphins_tests.erl87
-rw-r--r--learn-you-some-erlang/tests/exceptions_tests.erl61
-rw-r--r--learn-you-some-erlang/tests/fifo_tests.erl22
-rw-r--r--learn-you-some-erlang/tests/functions_tests.erl27
-rw-r--r--learn-you-some-erlang/tests/guards_tests.erl22
-rw-r--r--learn-you-some-erlang/tests/hhfuns_tests.erl95
-rw-r--r--learn-you-some-erlang/tests/kitchen_tests.erl118
-rw-r--r--learn-you-some-erlang/tests/kitty_gen_server_tests.erl19
-rw-r--r--learn-you-some-erlang/tests/kitty_server2_tests.erl23
-rw-r--r--learn-you-some-erlang/tests/kitty_server_tests.erl23
-rw-r--r--learn-you-some-erlang/tests/linkmon_tests.erl43
-rw-r--r--learn-you-some-erlang/tests/multiproc_tests.erl28
-rw-r--r--learn-you-some-erlang/tests/musicians_tests.erl56
-rw-r--r--learn-you-some-erlang/tests/oop_tests.erl16
-rw-r--r--learn-you-some-erlang/tests/records_tests.erl46
-rw-r--r--learn-you-some-erlang/tests/recursive_tests.erl109
-rw-r--r--learn-you-some-erlang/tests/road_tests.erl33
-rw-r--r--learn-you-some-erlang/tests/tree_tests.erl64
-rw-r--r--learn-you-some-erlang/tests/useless_tests.erl18
-rw-r--r--learn-you-some-erlang/tests/what_the_if_tests.erl17
-rw-r--r--learn-you-some-erlang/trade/trade_calls.erl144
-rw-r--r--learn-you-some-erlang/trade/trade_fsm.erl359
-rw-r--r--learn-you-some-erlang/tree.erl43
-rw-r--r--learn-you-some-erlang/useless.erl14
-rw-r--r--learn-you-some-erlang/what_the_if.erl30
-rw-r--r--learn-you-some-erlang/zoo.erl45
236 files changed, 10088 insertions, 0 deletions
diff --git a/learn-you-some-erlang/LICENSE.txt b/learn-you-some-erlang/LICENSE.txt
new file mode 100644
index 0000000..5be26d9
--- /dev/null
+++ b/learn-you-some-erlang/LICENSE.txt
@@ -0,0 +1,22 @@
+Copyright (c) 2009 Frederic Trottier-Hebert
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file
diff --git a/learn-you-some-erlang/README.txt b/learn-you-some-erlang/README.txt
new file mode 100644
index 0000000..d46e598
--- /dev/null
+++ b/learn-you-some-erlang/README.txt
@@ -0,0 +1,17 @@
+Man there's not a lot to write in a readme for such a book.
+
+You can get the license info in LICENSE.TXT (hint: it's the MIT license).
+I probably won't copy it in every module. Just pretend it's there, please.
+
+The code in these files has been tested with Erlang releases R13B+. In case you're not sure it will run on another version, you can run the tests yourself by compiling tester.erl and then calling:
+
+1> tester:dir().
+or
+2> tester:dir("/modules/path","/unit/tests/modules/path/").
+
+Which should run all the tests. If you're using the release R13B, there is a patch you need to apply in order to have unit tests running (see http://erlang.org/download.html).
+
+Note that even if the tests run fine, you'd benefit in using releases R13B+.
+
+If you need any more help, don't hesitate contacting me: mononcqc@ferd.ca.
+
diff --git a/learn-you-some-erlang/band_supervisor.erl b/learn-you-some-erlang/band_supervisor.erl
new file mode 100644
index 0000000..dd17ba8
--- /dev/null
+++ b/learn-you-some-erlang/band_supervisor.erl
@@ -0,0 +1,41 @@
+-module(band_supervisor).
+-behaviour(supervisor).
+
+-export([start_link/1]).
+-export([init/1]).
+
+start_link(Type) ->
+ supervisor:start_link({local,?MODULE}, ?MODULE, Type).
+
+%% The band supervisor will allow its band members to make a few
+%% mistakes before shutting down all operations, based on what
+%% mood he's in. A lenient supervisor will tolerate more mistakes
+%% than an angry supervisor, who'll tolerate more than a
+%% complete jerk supervisor
+init(lenient) ->
+ init({one_for_one, 3, 60});
+init(angry) ->
+ init({rest_for_one, 2, 60});
+init(jerk) ->
+ init({one_for_all, 1, 60});
+init(jamband) ->
+ {ok, {{simple_one_for_one, 3, 60},
+ [{jam_musician,
+ {musicians, start_link, []},
+ temporary, 1000, worker, [musicians]}
+ ]}};
+init({RestartStrategy, MaxRestart, MaxTime}) ->
+ {ok, {{RestartStrategy, MaxRestart, MaxTime},
+ [{singer,
+ {musicians, start_link, [singer, good]},
+ permanent, 1000, worker, [musicians]},
+ {bass,
+ {musicians, start_link, [bass, good]},
+ temporary, 1000, worker, [musicians]},
+ {drum,
+ {musicians, start_link, [drum, bad]},
+ transient, 1000, worker, [musicians]},
+ {keytar,
+ {musicians, start_link, [keytar, good]},
+ transient, 1000, worker, [musicians]}
+ ]}}.
diff --git a/learn-you-some-erlang/calc.erl b/learn-you-some-erlang/calc.erl
new file mode 100644
index 0000000..c41ffb6
--- /dev/null
+++ b/learn-you-some-erlang/calc.erl
@@ -0,0 +1,51 @@
+-module(calc).
+-export([rpn/1, rpn_test/0]).
+
+%% rpn(List()) -> Int() | Float()
+%% parses an RPN string and outputs the results.
+rpn(L) when is_list(L) ->
+ [Res] = lists:foldl(fun rpn/2, [], string:tokens(L, " ")),
+ Res.
+
+%% rpn(Str(), List()) -> List()
+%% Returns the new stack after an operation has been done.
+%% If no operator is found, we assume a number.
+rpn("+", [N1,N2|S]) -> [N2+N1|S];
+rpn("-", [N1,N2|S]) -> [N2-N1|S];
+rpn("*", [N1,N2|S]) -> [N2*N1|S];
+rpn("/", [N1,N2|S]) -> [N2/N1|S];
+rpn("^", [N1,N2|S]) -> [math:pow(N2,N1)|S];
+rpn("ln", [N|S]) -> [math:log(N)|S];
+rpn("log10", [N|S]) -> [math:log10(N)|S];
+rpn("sum", Stack) -> [lists:sum(Stack)];
+rpn("prod", Stack) -> [lists:foldl(fun erlang:'*'/2, 1, Stack)];
+rpn(X, Stack) -> [read(X)|Stack].
+
+%% read(String()) -> Int() | Float()
+read(N) ->
+ case string:to_float(N) of
+ {error,no_float} -> list_to_integer(N);
+ {F,_} -> F
+ end.
+
+%% returns 'ok' iff successful
+rpn_test() ->
+ 5 = rpn("2 3 +"),
+ 87 = rpn("90 3 -"),
+ -4 = rpn("10 4 3 + 2 * -"),
+ -2.0 = rpn("10 4 3 + 2 * - 2 /"),
+ ok = try
+ rpn("90 34 12 33 55 66 + * - +")
+ catch
+ error:{badmatch,[_|_]} -> ok
+ end,
+ 4037 = rpn("90 34 12 33 55 66 + * - + -"),
+ 8.0 = rpn("2 3 ^"),
+ true = math:sqrt(2) == rpn("2 0.5 ^"),
+ true = math:log(2.7) == rpn("2.7 ln"),
+ true = math:log10(2.7) == rpn("2.7 log10"),
+ 50 = rpn("10 10 10 20 sum"),
+ 10.0 = rpn("10 10 10 20 sum 5 /"),
+ 1000.0 = rpn("10 10 20 0.5 prod"),
+ ok.
+
diff --git a/learn-you-some-erlang/cards.erl b/learn-you-some-erlang/cards.erl
new file mode 100644
index 0000000..ee8dcd2
--- /dev/null
+++ b/learn-you-some-erlang/cards.erl
@@ -0,0 +1,16 @@
+-module(cards).
+-export([kind/1, main/0]).
+
+-type suit() :: spades | clubs | hearts | diamonds.
+-type value() :: 1..10 | j | q | k.
+-type card() :: {suit(), value()}.
+
+-spec kind(card()) -> face | number.
+kind({_, A}) when A >= 1, A =< 10 -> number;
+kind(_) -> face.
+
+main() ->
+ number = kind({spades, 7}),
+ face = kind({hearts, k}),
+ number = kind({rubies, 4}),
+ face = kind({clubs, q}).
diff --git a/learn-you-some-erlang/cases.erl b/learn-you-some-erlang/cases.erl
new file mode 100644
index 0000000..9b4f931
--- /dev/null
+++ b/learn-you-some-erlang/cases.erl
@@ -0,0 +1,22 @@
+-module(cases).
+-export([insert/2,beach/1]).
+
+insert(X,[]) ->
+ [X];
+insert(X,Set) ->
+ case lists:member(X,Set) of
+ true -> Set;
+ false -> [X|Set]
+ end.
+
+beach(Temperature) ->
+ case Temperature of
+ {celsius, N} when N >= 20, N =< 45 ->
+ 'favorable';
+ {kelvin, N} when N >= 293, N =< 318 ->
+ 'scientifically favorable';
+ {fahrenheit, N} when N >= 68, N =< 113 ->
+ 'favorable in the US';
+ _ ->
+ 'avoid beach'
+ end.
diff --git a/learn-you-some-erlang/cat_fsm.erl b/learn-you-some-erlang/cat_fsm.erl
new file mode 100644
index 0000000..3fa5039
--- /dev/null
+++ b/learn-you-some-erlang/cat_fsm.erl
@@ -0,0 +1,23 @@
+-module(cat_fsm).
+-export([start/0, event/2]).
+
+start() ->
+ spawn(fun() -> dont_give_crap() end).
+
+event(Pid, Event) ->
+ Ref = make_ref(), % won't care for monitors here
+ Pid ! {self(), Ref, Event},
+ receive
+ {Ref, Msg} -> {ok, Msg}
+ after 5000 ->
+ {error, timeout}
+ end.
+
+dont_give_crap() ->
+ receive
+ {Pid, Ref, _Msg} -> Pid ! {Ref, meh};
+ _ -> ok
+ end,
+ io:format("Switching to 'dont_give_crap' state~n"),
+ dont_give_crap().
+
diff --git a/learn-you-some-erlang/convert.erl b/learn-you-some-erlang/convert.erl
new file mode 100644
index 0000000..86c3c2b
--- /dev/null
+++ b/learn-you-some-erlang/convert.erl
@@ -0,0 +1,14 @@
+-module(convert).
+-export([main/0, convert/1]).
+
+main() ->
+ [_,_] = convert({a,b}),
+ {_,_} = convert([a,b]),
+ [_,_] = convert([a,b]),
+ {_,_} = convert({a,b}).
+
+-spec convert(tuple()) -> list()
+ ; (list()) -> tuple().
+convert(Tup) when is_tuple(Tup) -> tuple_to_list(Tup);
+convert(L = [_|_]) -> list_to_tuple(L).
+
diff --git a/learn-you-some-erlang/ct/demo/basic_SUITE.erl b/learn-you-some-erlang/ct/demo/basic_SUITE.erl
new file mode 100644
index 0000000..566ff54
--- /dev/null
+++ b/learn-you-some-erlang/ct/demo/basic_SUITE.erl
@@ -0,0 +1,13 @@
+-module(basic_SUITE).
+-include_lib("common_test/include/ct.hrl").
+-export([all/0]).
+-export([test1/1, test2/1]).
+
+all() -> [test1,test2].
+
+test1(_Config) ->
+ 1 = 1.
+
+test2(_Config) ->
+ A = 0,
+ 1/A.
diff --git a/learn-you-some-erlang/ct/demo/state_SUITE.erl b/learn-you-some-erlang/ct/demo/state_SUITE.erl
new file mode 100644
index 0000000..3ee0e5b
--- /dev/null
+++ b/learn-you-some-erlang/ct/demo/state_SUITE.erl
@@ -0,0 +1,24 @@
+-module(state_SUITE).
+-include_lib("common_test/include/ct.hrl").
+
+-export([all/0, init_per_testcase/2, end_per_testcase/2]).
+-export([ets_tests/1]).
+
+all() -> [ets_tests].
+
+init_per_testcase(ets_tests, Config) ->
+ TabId = ets:new(account, [ordered_set, public]),
+ ets:insert(TabId, {andy, 2131}),
+ ets:insert(TabId, {david, 12}),
+ ets:insert(TabId, {steve, 12943752}),
+ [{table,TabId} | Config].
+
+end_per_testcase(ets_tests, Config) ->
+ ets:delete(?config(table, Config)).
+
+ets_tests(Config) ->
+ TabId = ?config(table, Config),
+ [{david, 12}] = ets:lookup(TabId, david),
+ steve = ets:last(TabId),
+ true = ets:insert(TabId, {zachary, 99}),
+ zachary = ets:last(TabId).
diff --git a/learn-you-some-erlang/ct/dist.spec b/learn-you-some-erlang/ct/dist.spec
new file mode 100644
index 0000000..e25cdfb
--- /dev/null
+++ b/learn-you-some-erlang/ct/dist.spec
@@ -0,0 +1,14 @@
+{node, a, 'a@ferdmbp.local'}.
+{node, b, 'b@ferdmbp.local'}.
+
+{init, [a,b], [{node_start, [{monitor_master, true}]}]}.
+
+{alias, demo, "/Users/ferd/code/self/learn-you-some-erlang/ct/demo/"}.
+{alias, meeting, "/Users/ferd/code/self/learn-you-some-erlang/ct/meeting/"}.
+
+{logdir, all_nodes, "/Users/ferd/code/self/learn-you-some-erlang/ct/logs/"}.
+{logdir, master, "/Users/ferd/code/self/learn-you-some-erlang/ct/logs/"}.
+
+{suites, [b], meeting, all}.
+{suites, [a], demo, all}.
+{skip_cases, [a], demo, basic_SUITE, test2, "This test fails on purpose"}.
diff --git a/learn-you-some-erlang/ct/meeting/meeting.erl b/learn-you-some-erlang/ct/meeting/meeting.erl
new file mode 100644
index 0000000..0de9453
--- /dev/null
+++ b/learn-you-some-erlang/ct/meeting/meeting.erl
@@ -0,0 +1,45 @@
+-module(meeting).
+-export([rent_projector/1, use_chairs/1, book_room/1,
+ get_all_bookings/0, start/0, stop/0]).
+-record(bookings, {projector, room, chairs}).
+
+start() ->
+ Pid = spawn(fun() -> loop(#bookings{}) end),
+ register(?MODULE, Pid).
+
+stop() ->
+ ?MODULE ! stop.
+
+rent_projector(Group) ->
+ ?MODULE ! {projector, Group}.
+
+book_room(Group) ->
+ ?MODULE ! {room, Group}.
+
+use_chairs(Group) ->
+ ?MODULE ! {chairs, Group}.
+
+get_all_bookings() ->
+ Ref = make_ref(),
+ ?MODULE ! {self(), Ref, get_bookings},
+ receive
+ {Ref, Reply} ->
+ Reply
+ end.
+
+loop(B = #bookings{}) ->
+ receive
+ stop -> ok;
+ {From, Ref, get_bookings} ->
+ From ! {Ref, [{room, B#bookings.room},
+ {chairs, B#bookings.chairs},
+ {projector, B#bookings.projector}]},
+ loop(B);
+ {room, Group} ->
+ loop(B#bookings{room=Group});
+ {chairs, Group} ->
+ loop(B#bookings{chairs=Group});
+ {projector, Group} ->
+ loop(B#bookings{projector=Group})
+ end.
+
diff --git a/learn-you-some-erlang/ct/meeting/meeting_SUITE.erl b/learn-you-some-erlang/ct/meeting/meeting_SUITE.erl
new file mode 100755
index 0000000..f699569
--- /dev/null
+++ b/learn-you-some-erlang/ct/meeting/meeting_SUITE.erl
@@ -0,0 +1,51 @@
+-module(meeting_SUITE).
+-include_lib("common_test/include/ct.hrl").
+-export([all/0, groups/0, init_per_group/2, end_per_group/2]).
+-export([carla/1, mark/1, dog/1, all_same_owner/1]).
+
+%% Regroup the tests as groups that will let you try to see if the 'meeting.erl'
+%% modules contains concurrency errors or not.
+
+all() -> [{group, session}].
+
+groups() -> [{session,
+ [],
+ [{group, clients}, all_same_owner]},
+ {clients,
+ [parallel, {repeat, 10}],
+ [carla, mark, dog]}].
+
+init_per_group(session, Config) ->
+ meeting:start(),
+ Config;
+init_per_group(_, Config) ->
+ Config.
+
+end_per_group(session, _Config) ->
+ meeting:stop();
+end_per_group(_, _Config) ->
+ ok.
+
+carla(_Config) ->
+ meeting:book_room(women),
+ timer:sleep(10),
+ meeting:rent_projector(women),
+ timer:sleep(10),
+ meeting:use_chairs(women).
+
+mark(_Config) ->
+ meeting:rent_projector(men),
+ timer:sleep(10),
+ meeting:use_chairs(men),
+ timer:sleep(10),
+ meeting:book_room(men).
+
+dog(_Config) ->
+ meeting:rent_projector(animals),
+ timer:sleep(10),
+ meeting:use_chairs(animals),
+ timer:sleep(10),
+ meeting:book_room(animals).
+
+all_same_owner(_Config) ->
+ [{_,Owner}, {_, Owner}, {_, Owner}] = meeting:get_all_bookings().
diff --git a/learn-you-some-erlang/ct/spec.spec b/learn-you-some-erlang/ct/spec.spec
new file mode 100644
index 0000000..1752664
--- /dev/null
+++ b/learn-you-some-erlang/ct/spec.spec
@@ -0,0 +1,7 @@
+{alias, demo, "./demo/"}.
+{alias, meeting, "./meeting/"}.
+{logdir, "./logs/"}.
+
+{suites, meeting, all}.
+{suites, demo, all}.
+{skip_cases, demo, basic_SUITE, test2, "This test fails on purpose"}.
diff --git a/learn-you-some-erlang/curling.erl b/learn-you-some-erlang/curling.erl
new file mode 100644
index 0000000..2c50074
--- /dev/null
+++ b/learn-you-some-erlang/curling.erl
@@ -0,0 +1,37 @@
+-module(curling).
+-export([start_link/2, set_teams/3, add_points/3, next_round/1]).
+-export([join_feed/2, leave_feed/2]).
+-export([game_info/1]).
+
+start_link(TeamA, TeamB) ->
+ {ok, Pid} = gen_event:start_link(),
+ %% The scoreboard will always be there
+ gen_event:add_handler(Pid, curling_scoreboard, []),
+ %% Start the stats accumulator
+ gen_event:add_handler(Pid, curling_accumulator, []),
+ set_teams(Pid, TeamA, TeamB),
+ {ok, Pid}.
+
+set_teams(Pid, TeamA, TeamB) ->
+ gen_event:notify(Pid, {set_teams, TeamA, TeamB}).
+
+add_points(Pid, Team, N) ->
+ gen_event:notify(Pid, {add_points, Team, N}).
+
+next_round(Pid) ->
+ gen_event:notify(Pid, next_round).
+
+%% Subscribes the pid ToPid to the event feed.
+%% The specific event handler for the newsfeed is
+%% returned in case someone wants to leave
+join_feed(Pid, ToPid) ->
+ HandlerId = {curling_feed, make_ref()},
+ gen_event:add_sup_handler(Pid, HandlerId, [ToPid]),
+ HandlerId.
+
+leave_feed(Pid, HandlerId) ->
+ gen_event:delete_handler(Pid, HandlerId, leave_feed).
+
+%% Returns the current game state.
+game_info(Pid) ->
+ gen_event:call(Pid, curling_accumulator, game_data).
diff --git a/learn-you-some-erlang/curling_accumulator.erl b/learn-you-some-erlang/curling_accumulator.erl
new file mode 100644
index 0000000..f6cacfd
--- /dev/null
+++ b/learn-you-some-erlang/curling_accumulator.erl
@@ -0,0 +1,35 @@
+-module(curling_accumulator).
+-behaviour(gen_event).
+
+-export([init/1, handle_event/2, handle_call/2, handle_info/2, code_change/3,
+ terminate/2]).
+
+-record(state, {teams=orddict:new(), round=0}).
+
+init([]) ->
+ {ok, #state{}}.
+
+handle_event({set_teams, TeamA, TeamB}, S=#state{teams=T}) ->
+ Teams = orddict:store(TeamA, 0, orddict:store(TeamB, 0, T)),
+ {ok, S#state{teams=Teams}};
+handle_event({add_points, Team, N}, S=#state{teams=T}) ->
+ Teams = orddict:update_counter(Team, N, T),
+ {ok, S#state{teams=Teams}};
+handle_event(next_round, S=#state{}) ->
+ {ok, S#state{round = S#state.round+1}};
+handle_event(_Event, Pid) ->
+ {ok, Pid}.
+
+handle_call(game_data, S=#state{teams=T, round=R}) ->
+ {ok, {orddict:to_list(T), {round, R}}, S};
+handle_call(_, State) ->
+ {ok, ok, State}.
+
+handle_info(_, State) ->
+ {ok, State}.
+
+code_change(_OldVsn, State, _Extra) ->
+ {ok, State}.
+
+terminate(_Reason, _State) ->
+ ok.
diff --git a/learn-you-some-erlang/curling_feed.erl b/learn-you-some-erlang/curling_feed.erl
new file mode 100644
index 0000000..b1b789a
--- /dev/null
+++ b/learn-you-some-erlang/curling_feed.erl
@@ -0,0 +1,24 @@
+-module(curling_feed).
+-behaviour(gen_event).
+
+-export([init/1, handle_event/2, handle_call/2, handle_info/2, code_change/3,
+ terminate/2]).
+
+init([Pid]) ->
+ {ok, Pid}.
+
+handle_event(Event, Pid) ->
+ Pid ! {curling_feed, Event},
+ {ok, Pid}.
+
+handle_call(_, State) ->
+ {ok, ok, State}.
+
+handle_info(_, State) ->
+ {ok, State}.
+
+code_change(_OldVsn, State, _Extra) ->
+ {ok, State}.
+
+terminate(_Reason, _State) ->
+ ok.
diff --git a/learn-you-some-erlang/curling_scoreboard.erl b/learn-you-some-erlang/curling_scoreboard.erl
new file mode 100644
index 0000000..34ff48d
--- /dev/null
+++ b/learn-you-some-erlang/curling_scoreboard.erl
@@ -0,0 +1,32 @@
+-module(curling_scoreboard).
+-behaviour(gen_event).
+
+-export([init/1, handle_event/2, handle_call/2, handle_info/2, code_change/3,
+ terminate/2]).
+
+init([]) ->
+ {ok, []}.
+
+handle_event({set_teams, TeamA, TeamB}, State) ->
+ curling_scoreboard_hw:set_teams(TeamA, TeamB),
+ {ok, State};
+handle_event({add_points, Team, N}, State) ->
+ [curling_scoreboard_hw:add_point(Team) || _ <- lists:seq(1,N)],
+ {ok, State};
+handle_event(next_round, State) ->
+ curling_scoreboard_hw:next_round(),
+ {ok, State};
+handle_event(_, State) ->
+ {ok, State}.
+
+handle_call(_, State) ->
+ {ok, ok, State}.
+
+handle_info(_, State) ->
+ {ok, State}.
+
+code_change(_OldVsn, State, _Extra) ->
+ {ok, State}.
+
+terminate(_Reason, _State) ->
+ ok.
diff --git a/learn-you-some-erlang/curling_scoreboard_hw.erl b/learn-you-some-erlang/curling_scoreboard_hw.erl
new file mode 100644
index 0000000..adfe4ab
--- /dev/null
+++ b/learn-you-some-erlang/curling_scoreboard_hw.erl
@@ -0,0 +1,19 @@
+-module(curling_scoreboard_hw).
+-export([add_point/1, next_round/0, set_teams/2, reset_board/0]).
+
+%% This is a 'dumb' module that's only there to replace what a real hardware
+%% controller would likely do. The real hardware controller would likely hold
+%% some state and make sure everything works right, but this one doesn't mind.
+
+%% Shows the teams on the scoreboard.
+set_teams(TeamA, TeamB) ->
+ io:format("Scoreboard: Team ~s vs. Team ~s~n", [TeamA, TeamB]).
+
+next_round() ->
+ io:format("Scoreboard: round over~n").
+
+add_point(Team) ->
+ io:format("Scoreboard: increased score of team ~s by 1~n", [Team]).
+
+reset_board() ->
+ io:format("Scoreboard: All teams are undefined and all scores are 0~n").
diff --git a/learn-you-some-erlang/discrep1.erl b/learn-you-some-erlang/discrep1.erl
new file mode 100644
index 0000000..188e938
--- /dev/null
+++ b/learn-you-some-erlang/discrep1.erl
@@ -0,0 +1,6 @@
+-module(discrep1).
+-export([run/0]).
+
+run() -> some_op(5, you).
+
+some_op(A, B) -> A + B.
diff --git a/learn-you-some-erlang/discrep2.erl b/learn-you-some-erlang/discrep2.erl
new file mode 100644
index 0000000..7462e5f
--- /dev/null
+++ b/learn-you-some-erlang/discrep2.erl
@@ -0,0 +1,12 @@
+-module(discrep2).
+-export([run/0]).
+
+run() ->
+ Tup = money(5, you),
+ some_op(count(Tup), account(Tup)).
+
+money(Num, Name) -> {give, Num, Name}.
+count({give, Num, _}) -> Num.
+account({give, _, X}) -> X.
+
+some_op(A, B) -> A + B.
diff --git a/learn-you-some-erlang/discrep3.erl b/learn-you-some-erlang/discrep3.erl
new file mode 100644
index 0000000..aaec5d6
--- /dev/null
+++ b/learn-you-some-erlang/discrep3.erl
@@ -0,0 +1,14 @@
+-module(discrep3).
+-export([run/0]).
+
+run() ->
+ Tup = money(5, you),
+ some_op(item(count, Tup), item(account, Tup)).
+
+money(Num, Name) -> {give, Num, Name}.
+
+item(count, {give, X, _}) -> X;
+item(account, {give, _, X}) -> X.
+
+some_op(A, B) -> A + B.
+
diff --git a/learn-you-some-erlang/discrep4.erl b/learn-you-some-erlang/discrep4.erl
new file mode 100644
index 0000000..949a843
--- /dev/null
+++ b/learn-you-some-erlang/discrep4.erl
@@ -0,0 +1,21 @@
+-module(discrep4).
+-export([run/0]).
+-type cents() :: integer().
+-type account() :: atom().
+-type transaction() :: {'give', cents(), account()}.
+
+run() ->
+ Tup = money(5, you),
+ some_op(item(count,Tup), item(account,Tup)).
+
+-spec money(cents(), account()) -> transaction().
+money(Num, Name) -> {give, Num, Name}.
+
+-spec item('count', transaction()) -> cents();
+ ('account', transaction()) -> account().
+item(count, {give, X, _}) -> X;
+item(account, {give, _, X}) -> X.
+
+some_op(A,B) -> A + B.
+
+
diff --git a/learn-you-some-erlang/dog_fsm.erl b/learn-you-some-erlang/dog_fsm.erl
new file mode 100644
index 0000000..f784933
--- /dev/null
+++ b/learn-you-some-erlang/dog_fsm.erl
@@ -0,0 +1,42 @@
+-module(dog_fsm).
+-export([start/0, squirrel/1, pet/1]).
+
+start() -> spawn(fun() -> bark() end).
+
+squirrel(Pid) -> Pid ! squirrel.
+
+pet(Pid) -> Pid ! pet.
+
+bark() ->
+ io:format("Dog says: BARK! BARK!~n"),
+ receive
+ pet ->
+ wag_tail();
+ _ ->
+ io:format("Dog is confused~n"),
+ bark()
+ after 2000 ->
+ bark()
+ end.
+
+wag_tail() ->
+ io:format("Dog wags its tail~n"),
+ receive
+ pet ->
+ sit();
+ _ ->
+ io:format("Dog is confused~n"),
+ wag_tail()
+ after 30000 ->
+ bark()
+ end.
+
+sit() ->
+ io:format("Dog is sitting. Gooooood boy!~n"),
+ receive
+ squirrel ->
+ bark();
+ _ ->
+ io:format("Dog is confused~n"),
+ sit()
+ end.
diff --git a/learn-you-some-erlang/dolphins.erl b/learn-you-some-erlang/dolphins.erl
new file mode 100644
index 0000000..1e84b77
--- /dev/null
+++ b/learn-you-some-erlang/dolphins.erl
@@ -0,0 +1,34 @@
+-module(dolphins).
+-compile(export_all).
+
+dolphin1() ->
+ receive
+ do_a_flip ->
+ io:format("How about no?~n");
+ fish ->
+ io:format("So long and thanks for all the fish!~n");
+ _ ->
+ io:format("Heh, we're smarter than you humans.~n")
+ end.
+
+dolphin2() ->
+ receive
+ {From, do_a_flip} ->
+ From ! "How about no?";
+ {From, fish} ->
+ From ! "So long and thanks for all the fish!";
+ _ ->
+ io:format("Heh, we're smarter than you humans.~n")
+ end.
+
+dolphin3() ->
+ receive
+ {From, do_a_flip} ->
+ From ! "How about no?",
+ dolphin3();
+ {From, fish} ->
+ From ! "So long and thanks for all the fish!";
+ _ ->
+ io:format("Heh, we're smarter than you humans.~n"),
+ dolphin3()
+ end.
diff --git a/learn-you-some-erlang/erlcount-1.0/Emakefile b/learn-you-some-erlang/erlcount-1.0/Emakefile
new file mode 100644
index 0000000..76e98fc
--- /dev/null
+++ b/learn-you-some-erlang/erlcount-1.0/Emakefile
@@ -0,0 +1,4 @@
+{"src/*", [debug_info, {i,"include/"}, {outdir, "ebin/"}]}.
+%% The TESTDIR macro assumes the file is running from the 'erlcount-1.0'
+%% directory, sitting within 'learn-you-some-erlang/'.
+{"test/*", [debug_info, {i,"include/"}, {outdir, "ebin/"}, {d, 'TESTDIR', ".."}]}.
diff --git a/learn-you-some-erlang/erlcount-1.0/ebin/erlcount.app b/learn-you-some-erlang/erlcount-1.0/ebin/erlcount.app
new file mode 100644
index 0000000..1d40c4b
--- /dev/null
+++ b/learn-you-some-erlang/erlcount-1.0/ebin/erlcount.app
@@ -0,0 +1,13 @@
+{application, erlcount,
+ [{vsn, "1.0.0"},
+ {modules, [erlcount, erlcount_sup, erlcount_lib,
+ erlcount_dispatch, erlcount_counter]},
+ {applications, [ppool]},
+ {registered, [erlcount]},
+ {mod, {erlcount, []}},
+ {env,
+ [{directory, "."},
+ {regex, ["if\\s.+->", "case\\s.+\\sof"]},
+ {max_files, 10}]}
+ ]}.
+
diff --git a/learn-you-some-erlang/erlcount-1.0/src/erlcount.erl b/learn-you-some-erlang/erlcount-1.0/src/erlcount.erl
new file mode 100644
index 0000000..16a9f23
--- /dev/null
+++ b/learn-you-some-erlang/erlcount-1.0/src/erlcount.erl
@@ -0,0 +1,9 @@
+-module(erlcount).
+-behaviour(application).
+-export([start/2, stop/1]).
+
+start(normal, _Args) ->
+ erlcount_sup:start_link().
+
+stop(_State) ->
+ ok.
diff --git a/learn-you-some-erlang/erlcount-1.0/src/erlcount_counter.erl b/learn-you-some-erlang/erlcount-1.0/src/erlcount_counter.erl
new file mode 100644
index 0000000..c42fd4d
--- /dev/null
+++ b/learn-you-some-erlang/erlcount-1.0/src/erlcount_counter.erl
@@ -0,0 +1,35 @@
+-module(erlcount_counter).
+-behaviour(gen_server).
+-export([start_link/4]).
+-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
+ terminate/2, code_change/3]).
+
+-record(state, {dispatcher, ref, file, re}).
+
+start_link(DispatcherPid, Ref, FileName, Regex) ->
+ gen_server:start_link(?MODULE, [DispatcherPid, Ref, FileName, Regex], []).
+
+init([DispatcherPid, Ref, FileName, Regex]) ->
+ self() ! start,
+ {ok, #state{dispatcher=DispatcherPid,
+ ref = Ref,
+ file = FileName,
+ re = Regex}}.
+
+handle_call(_Msg, _From, State) ->
+ {noreply, State}.
+
+handle_cast(_Msg, State) ->
+ {noreply, State}.
+
+handle_info(start, S = #state{re=Re, ref=Ref}) ->
+ {ok, Bin} = file:read_file(S#state.file),
+ Count = erlcount_lib:regex_count(Re, Bin),
+ erlcount_dispatch:complete(S#state.dispatcher, Re, Ref, Count),
+ {stop, normal, S}.
+
+terminate(_Reason, _State) ->
+ ok.
+
+code_change(_OldVsn, State, _Extra) ->
+ {ok, State}.
diff --git a/learn-you-some-erlang/erlcount-1.0/src/erlcount_dispatch.erl b/learn-you-some-erlang/erlcount-1.0/src/erlcount_dispatch.erl
new file mode 100644
index 0000000..0bc5e45
--- /dev/null
+++ b/learn-you-some-erlang/erlcount-1.0/src/erlcount_dispatch.erl
@@ -0,0 +1,86 @@
+-module(erlcount_dispatch).
+-behaviour(gen_fsm).
+-export([start_link/0, complete/4]).
+-export([init/1, dispatching/2, listening/2, handle_event/3,
+ handle_sync_event/4, handle_info/3, terminate/3, code_change/4]).
+
+-define(POOL, erlcount).
+-record(data, {regex=[], refs=[]}).
+
+%%% PUBLIC API
+start_link() ->
+ gen_fsm:start_link(?MODULE, [], []).
+
+complete(Pid, Regex, Ref, Count) ->
+ gen_fsm:send_all_state_event(Pid, {complete, Regex, Ref, Count}).
+
+%%% GEN_FSM
+%% Two states: dispatching and listening
+init([]) ->
+ %% Move the get_env stuff to the supervisor's init.
+ {ok, Re} = application:get_env(regex),
+ {ok, Dir} = application:get_env(directory),
+ {ok, MaxFiles} = application:get_env(max_files),
+ ppool:start_pool(?POOL, MaxFiles, {erlcount_counter, start_link, []}),
+ case lists:all(fun valid_regex/1, Re) of
+ true ->
+ %% creates a regex entry of the form [{Re, Count}]
+ self() ! {start, Dir},
+ {ok, dispatching, #data{regex=[{R,0} || R <- Re]}};
+ false ->
+ {stop, invalid_regex}
+ end.
+
+dispatching({continue, File, Continuation}, Data = #data{regex=Re, refs=Refs}) ->
+ F = fun({Regex,_Count}, NewRefs) ->
+ Ref = make_ref(),
+ ppool:async_queue(?POOL, [self(), Ref, File, Regex]),
+ [Ref|NewRefs]
+ end,
+ NewRefs = lists:foldl(F, Refs, Re),
+ gen_fsm:send_event(self(), Continuation()),
+ {next_state, dispatching, Data#data{refs = NewRefs}};
+dispatching(done, Data) ->
+ %% This is a special case. We can not assume that all messages have NOT
+ %% been received by the time we hit 'done'. As such, we directly move to
+ %% listening/2 without waiting for an external event.
+ listening(done, Data).
+
+listening(done, #data{regex=Re, refs=[]}) -> % all received!
+ [io:format("Regex ~s has ~p results~n", [R,C]) || {R, C} <- Re],
+ {stop, normal, done};
+listening(done, Data) -> % entries still missing
+ {next_state, listening, Data}.
+
+handle_event({complete, Regex, Ref, Count}, State, Data = #data{regex=Re, refs=Refs}) ->
+ {Regex, OldCount} = lists:keyfind(Regex, 1, Re),
+ NewRe = lists:keyreplace(Regex, 1, Re, {Regex, OldCount+Count}),
+ NewData = Data#data{regex=NewRe, refs=Refs--[Ref]},
+ case State of
+ dispatching ->
+ {next_state, dispatching, NewData};
+ listening ->
+ listening(done, NewData)
+ end.
+
+handle_sync_event(Event, _From, State, Data) ->
+ io:format("Unexpected event: ~p~n", [Event]),
+ {next_state, State, Data}.
+
+handle_info({start, Dir}, State, Data) ->
+ gen_fsm:send_event(self(), erlcount_lib:find_erl(Dir)),
+ {next_state, State, Data}.
+
+terminate(_Reason, _State, _Data) ->
+ ok.
+
+code_change(_OldVsn, State, Data, _Extra) ->
+ {ok, State, Data}.
+
+%%% PRIVATE
+valid_regex(Re) ->
+ try re:run("", Re) of
+ _ -> true
+ catch
+ error:badarg -> false
+ end.
diff --git a/learn-you-some-erlang/erlcount-1.0/src/erlcount_lib.erl b/learn-you-some-erlang/erlcount-1.0/src/erlcount_lib.erl
new file mode 100644
index 0000000..70c062f
--- /dev/null
+++ b/learn-you-some-erlang/erlcount-1.0/src/erlcount_lib.erl
@@ -0,0 +1,55 @@
+-module(erlcount_lib).
+-export([find_erl/1, regex_count/2]).
+-include_lib("kernel/include/file.hrl").
+
+%% Finds all files ending in .erl
+find_erl(Directory) ->
+ find_erl(Directory, queue:new()).
+
+regex_count(Re, Str) ->
+ case re:run(Str, Re, [global]) of
+ nomatch -> 0;
+ {match, List} -> length(List)
+ end.
+
+%%% Private
+%% Dispatches based on file type
+find_erl(Name, Queue) ->
+ {ok, F = #file_info{}} = file:read_file_info(Name),
+ case F#file_info.type of
+ directory -> handle_directory(Name, Queue);
+ regular -> handle_regular_file(Name, Queue);
+ _Other -> dequeue_and_run(Queue)
+ end.
+
+%% Opens directories and enqueues files in there
+handle_directory(Dir, Queue) ->
+ case file:list_dir(Dir) of
+ {ok, []} ->
+ dequeue_and_run(Queue);
+ {ok, Files} ->
+ dequeue_and_run(enqueue_many(Dir, Files, Queue))
+ end.
+
+%% Checks if the file finishes in .erl
+handle_regular_file(Name, Queue) ->
+ case filename:extension(Name) of
+ ".erl" ->
+ {continue, Name, fun() -> dequeue_and_run(Queue) end};
+ _NonErl ->
+ dequeue_and_run(Queue)
+ end.
+
+%% Pops an item from the queue and runs it.
+dequeue_and_run(Queue) ->
+ case queue:out(Queue) of
+ {empty, _} -> done;
+ {{value, File}, NewQueue} -> find_erl(File, NewQueue)
+ end.
+
+%% Adds a bunch of items to the queue.
+enqueue_many(Path, Files, Queue) ->
+ F = fun(File, Q) -> queue:in(filename:join(Path,File), Q) end,
+ lists:foldl(F, Queue, Files).
+
+
diff --git a/learn-you-some-erlang/erlcount-1.0/src/erlcount_sup.erl b/learn-you-some-erlang/erlcount-1.0/src/erlcount_sup.erl
new file mode 100644
index 0000000..b8633a3
--- /dev/null
+++ b/learn-you-some-erlang/erlcount-1.0/src/erlcount_sup.erl
@@ -0,0 +1,17 @@
+-module(erlcount_sup).
+-behaviour(supervisor).
+-export([start_link/0, init/1]).
+
+start_link() ->
+ supervisor:start_link(?MODULE, []).
+
+init([]) ->
+ MaxRestart = 5,
+ MaxTime = 100,
+ {ok, {{one_for_one, MaxRestart, MaxTime},
+ [{dispatch,
+ {erlcount_dispatch, start_link, []},
+ transient,
+ 60000,
+ worker,
+ [erlcount_dispatch]}]}}.
diff --git a/learn-you-some-erlang/erlcount-1.0/test/erlcount_tests.erl b/learn-you-some-erlang/erlcount-1.0/test/erlcount_tests.erl
new file mode 100644
index 0000000..7459a39
--- /dev/null
+++ b/learn-you-some-erlang/erlcount-1.0/test/erlcount_tests.erl
@@ -0,0 +1,35 @@
+-module(erlcount_tests).
+-include_lib("eunit/include/eunit.hrl").
+-ifndef(TESTDIR).
+%% Assumes we're running from the app's directory. We want to target the
+%% 'learn-you-some-erlang' directory.
+-define(TESTDIR, "..").
+-endif.
+
+%% NOTE:
+%% Because we do not want the tests to be bound to a specific snapshot in time
+%% of our app, we will instead compare it to an oracle built with unix
+%% commands. Users running windows sadly won't be able to run these tests.
+
+%% We'll be forcing the design to be continuation-based when it comes to
+%% reading files. This will require some explaining to the user, but will
+%% allow to show how we can read files and schedule them at the same time,
+%% but without breaking functional principles of referential transparency
+%% and while allowing specialised functions to be written in a testable manner.
+find_erl_test_() ->
+ ?_assertEqual(lists:sort(string:tokens(os:cmd("find "++?TESTDIR++" -name *.erl"), "\n")),
+ lists:sort(build_list(erlcount_lib:find_erl(?TESTDIR)))).
+
+build_list(Term) -> build_list(Term, []).
+
+build_list(done, List) -> List;
+build_list({continue, Entry, Fun}, List) ->
+ build_list(Fun(), [Entry|List]).
+
+regex_count_test_() ->
+ [?_assertEqual(5, erlcount_lib:regex_count("a", "a a a a a")),
+ ?_assertEqual(0, erlcount_lib:regex_count("o", "a a a a a")),
+ ?_assertEqual(2, erlcount_lib:regex_count("a.*", "a a a\na a a")),
+ ?_assertEqual(3, erlcount_lib:regex_count("if", "myiffun() ->\n if 1 < \" if \" -> ok;\n true -> other\n end.\n")),
+ ?_assertEqual(1, erlcount_lib:regex_count("if[\\s]{1}(?:.+)->", "myiffun() ->\n if 1 < \" if \" -> ok;\n true -> other\n end.\n")),
+ ?_assertEqual(2, erlcount_lib:regex_count("if[\\s]{1}(?:.+)->", "myiffun() ->\n if 1 < \" if \" -> ok;\n true -> other\n end,\n if true -> ok end.\n"))].
diff --git a/learn-you-some-erlang/exceptions.erl b/learn-you-some-erlang/exceptions.erl
new file mode 100644
index 0000000..0ed6742
--- /dev/null
+++ b/learn-you-some-erlang/exceptions.erl
@@ -0,0 +1,76 @@
+-module(exceptions).
+-compile(export_all).
+
+throws(F) ->
+ try F() of
+ _ -> ok
+ catch
+ Throw -> {throw, caught, Throw}
+ end.
+
+errors(F) ->
+ try F() of
+ _ -> ok
+ catch
+ error:Error -> {error, caught, Error}
+ end.
+
+exits(F) ->
+ try F() of
+ _ -> ok
+ catch
+ exit:Exit -> {exit, caught, Exit}
+ end.
+
+sword(1) -> throw(slice);
+sword(2) -> erlang:error(cut_arm);
+sword(3) -> exit(cut_leg);
+sword(4) -> throw(punch);
+sword(5) -> exit(cross_bridge).
+
+%%"I must cross this bridge"
+black_knight(Attack) when is_function(Attack, 0) ->
+ try Attack() of
+ _ -> "None shall pass."
+ catch
+ throw:slice -> "It is but a scratch.";
+ error:cut_arm -> "I've had worse.";
+ exit:cut_leg -> "Come on you pansy!";
+ _:_ -> "Just a flesh wound."
+ end.
+%"We'll call it a draw..."
+
+talk() -> "blah blah".
+
+whoa() ->
+ try
+ talk(),
+ _Knight = "None shall Pass!",
+ _Doubles = [N*2 || N <- lists:seq(1,100)],
+ throw(up),
+ _WillReturnThis = tequila
+ of
+ tequila -> "hey this worked!"
+ catch
+ Exception:Reason -> {caught, Exception, Reason}
+ end.
+
+im_impressed() ->
+ try
+ talk(),
+ _Knight = "None shall Pass!",
+ _Doubles = [N*2 || N <- lists:seq(1,100)],
+ throw(up),
+ _WillReturnThis = tequila
+ catch
+ Exception:Reason -> {caught, Exception, Reason}
+ end.
+
+catcher(X,Y) ->
+ case catch X/Y of
+ {'EXIT', {badarith,_}} -> "uh oh";
+ N -> N
+ end.
+
+one_or_two(1) -> return;
+one_or_two(2) -> throw(return).
diff --git a/learn-you-some-erlang/fifo.erl b/learn-you-some-erlang/fifo.erl
new file mode 100644
index 0000000..9687456
--- /dev/null
+++ b/learn-you-some-erlang/fifo.erl
@@ -0,0 +1,15 @@
+-module(fifo).
+-export([new/0, push/2, pop/1, empty/1]).
+
+%% implemented as two stacks; push on the first, pop on the second.
+%% when the second is empty, reverse the first and make it the second.
+new() -> {fifo, [], []}.
+
+push({fifo, In, Out}, X) -> {fifo, [X|In], Out}.
+
+pop({fifo, [], []}) -> erlang:error('empty fifo');
+pop({fifo, In, []}) -> pop({fifo, [], lists:reverse(In)});
+pop({fifo, In, [H|T]}) -> {H, {fifo, In, T}}.
+
+empty({fifo, [], []}) -> true;
+empty({fifo, _, _}) -> false.
diff --git a/learn-you-some-erlang/fifo_types.erl b/learn-you-some-erlang/fifo_types.erl
new file mode 100644
index 0000000..d1bf4ed
--- /dev/null
+++ b/learn-you-some-erlang/fifo_types.erl
@@ -0,0 +1,35 @@
+-module(fifo_types).
+-export([new/0, push/2, pop/1, empty/1]).
+-export([test/0]).
+
+-spec new() -> {fifo, [], []}.
+new() -> {fifo, [], []}.
+
+-spec push({fifo, In::list(), Out::list()}, term()) -> {fifo, list(), list()}.
+push({fifo, In, Out}, X) -> {fifo, [X|In], Out}.
+
+-spec pop({fifo, In::list(), Out::list()}) -> {term(), {fifo, list(), list()}}.
+pop({fifo, [], []}) -> erlang:error('empty fifo');
+pop({fifo, In, []}) -> pop({fifo, [], lists:reverse(In)});
+pop({fifo, In, [H|T]}) -> {H, {fifo, In, T}}.
+
+%-spec empty({fifo, [], []}) -> true;
+% ({fifo, list(), list()}) -> false.
+%-spec empty({fifo, [], []}) -> true;
+% ({fifo, [any(), ...], [any(), ...]}) -> false.
+-spec empty({fifo, [], []}) -> true;
+ ({fifo, [any(), ...], []}) -> false;
+ ({fifo, [], [any(), ...]}) -> false;
+ ({fifo, [any(), ...], [any(), ...]}) -> false.
+empty({fifo, [], []}) -> true;
+empty({fifo, _, _}) -> false.
+
+test() ->
+ N = new(),
+ {2, N2} = pop(push(push(new(), 2), 5)),
+ {5, N3} = pop(N2),
+ N = N3,
+ true = empty(N3),
+ false = empty(N2),
+ %pop({fifo, [a|b], [e]}).
+ pop({fifo, [a, b], [e]}).
diff --git a/learn-you-some-erlang/functions.erl b/learn-you-some-erlang/functions.erl
new file mode 100644
index 0000000..730a80d
--- /dev/null
+++ b/learn-you-some-erlang/functions.erl
@@ -0,0 +1,17 @@
+-module(functions).
+-export([head/1, second/1, same/2, valid_time/1]).
+
+head([H|_]) -> H.
+
+second([_,X|_]) -> X.
+
+same(X,X) ->
+ true;
+same(_,_) ->
+ false.
+
+valid_time({Date = {Y,M,D}, Time = {H,Min,S}}) ->
+ io:format("The Date tuple (~p) says today is: ~p/~p/~p,~n",[Date,Y,M,D]),
+ io:format("The time tuple (~p) indicates: ~p:~p:~p.~n", [Time,H,Min,S]);
+valid_time(_) ->
+ io:format("Stop feeding me wrong data!~n").
diff --git a/learn-you-some-erlang/guards.erl b/learn-you-some-erlang/guards.erl
new file mode 100644
index 0000000..3b1a2bc
--- /dev/null
+++ b/learn-you-some-erlang/guards.erl
@@ -0,0 +1,15 @@
+-module(guards).
+-export([old_enough/1, right_age/1, wrong_age/1]).
+
+old_enough(X) when X >= 16 -> true;
+old_enough(_) -> false.
+
+right_age(X) when X >= 16, X =< 104 ->
+ true;
+right_age(_) ->
+ false.
+
+wrong_age(X) when X < 16; X > 104 ->
+ true;
+wrong_age(_) ->
+ false.
diff --git a/learn-you-some-erlang/hhfuns.erl b/learn-you-some-erlang/hhfuns.erl
new file mode 100644
index 0000000..ebe07a4
--- /dev/null
+++ b/learn-you-some-erlang/hhfuns.erl
@@ -0,0 +1,111 @@
+-module(hhfuns).
+-compile(export_all).
+
+one() -> 1.
+two() -> 2.
+
+add(X,Y) -> X() + Y().
+
+increment([]) -> [];
+increment([H|T]) -> [H+1|increment(T)].
+
+decrement([]) -> [];
+decrement([H|T]) -> [H-1|decrement(T)].
+
+
+map(_, []) -> [];
+map(F, [H|T]) -> [F(H)|map(F,T)].
+
+incr(X) -> X + 1.
+decr(X) -> X - 1.
+
+%% bases/1. Refered as the same function refactored in the book
+base1(A) ->
+ B = A + 1,
+ F = fun() -> A * B end,
+ F().
+
+%%% can't compile this one
+%% base(A) ->
+%% B = A + 1,
+%% F = fun() -> C = A * B end,
+%% F(),
+%% C.
+
+base2() ->
+ A = 1,
+ (fun() -> A = 2 end)().
+
+base3() ->
+ A = 1,
+ (fun(A) -> A = 2 end)(2).
+
+a() ->
+ Secret = "pony",
+ fun() -> Secret end.
+
+b(F) ->
+ "a/0's password is "++F().
+
+even(L) -> lists:reverse(even(L,[])).
+
+even([], Acc) -> Acc;
+even([H|T], Acc) when H rem 2 == 0 ->
+ even(T, [H|Acc]);
+even([_|T], Acc) ->
+ even(T, Acc).
+
+old_men(L) -> lists:reverse(old_men(L,[])).
+
+old_men([], Acc) -> Acc;
+old_men([Person = {male, Age}|People], Acc) when Age > 60 ->
+ old_men(People, [Person|Acc]);
+old_men([_|People], Acc) ->
+ old_men(People, Acc).
+
+filter(Pred, L) -> lists:reverse(filter(Pred, L,[])).
+
+filter(_, [], Acc) -> Acc;
+filter(Pred, [H|T], Acc) ->
+ case Pred(H) of
+ true -> filter(Pred, T, [H|Acc]);
+ false -> filter(Pred, T, Acc)
+ end.
+
+%% find the maximum of a list
+max([H|T]) -> max2(T, H).
+
+max2([], Max) -> Max;
+max2([H|T], Max) when H > Max -> max2(T, H);
+max2([_|T], Max) -> max2(T, Max).
+
+%% find the minimum of a list
+min([H|T]) -> min2(T,H).
+
+min2([], Min) -> Min;
+min2([H|T], Min) when H < Min -> min2(T,H);
+min2([_|T], Min) -> min2(T, Min).
+
+%% sum of all the elements of a list
+sum(L) -> sum(L,0).
+
+sum([], Sum) -> Sum;
+sum([H|T], Sum) -> sum(T, H+Sum).
+
+fold(_, Start, []) -> Start;
+fold(F, Start, [H|T]) -> fold(F, F(H,Start), T).
+
+reverse(L) ->
+ fold(fun(X,Acc) -> [X|Acc] end, [], L).
+
+map2(F,L) ->
+ reverse(fold(fun(X,Acc) -> [F(X)|Acc] end, [], L)).
+
+filter2(Pred, L) ->
+ F = fun(X,Acc) ->
+ case Pred(X) of
+ true -> [X|Acc];
+ false -> Acc
+ end
+ end,
+ reverse(fold(F, [], L)).
diff --git a/learn-you-some-erlang/keyval_benchmark.erl b/learn-you-some-erlang/keyval_benchmark.erl
new file mode 100644
index 0000000..45c83be
--- /dev/null
+++ b/learn-you-some-erlang/keyval_benchmark.erl
@@ -0,0 +1,192 @@
+-module(keyval_benchmark).
+-compile(export_all).
+
+%% Runs all benchmarks with Reps number of elements.
+bench(Reps) ->
+ io:format("Base Case:~n"),
+ io:format("Operation\tTotal (µs)\tAverage (µs)~n"),
+ print(base_case(Reps)),
+ io:format("~nNaive Orddict:~n"),
+ io:format("Operation\tTotal (µs)\tAverage (µs)~n"),
+ print(naive_orddict(Reps)),
+ io:format("~nSmart Orddict:~n"),
+ io:format("Operation\tTotal (µs)\tAverage (µs)~n"),
+ print(smart_orddict(Reps)),
+ io:format("~nNaive Dict:~n"),
+ io:format("Operation\tTotal (µs)\tAverage (µs)~n"),
+ print(naive_dict(Reps)),
+ io:format("~nSmart Dict:~n"),
+ io:format("Operation\tTotal (µs)\tAverage (µs)~n"),
+ print(smart_dict(Reps)),
+ io:format("~nNaive gb_trees:~n"),
+ io:format("Operation\tTotal (µs)\tAverage (µs)~n"),
+ print(naive_gb_trees(Reps)),
+ io:format("~nSmart gb_trees:~n"),
+ io:format("Operation\tTotal (µs)\tAverage (µs)~n"),
+ print(smart_gb_trees(Reps)).
+
+%% formats the benchmark results cleanly.
+print([]) -> ok;
+print([{Op, Total, Avg} | Rest]) ->
+ io:format("~8s\t~10B\t~.6f~n", [Op, Total, Avg]),
+ print(Rest).
+
+%% Example of a base benchmark function. This one actually does
+%% nothing and can be used as a control for all the benchmark - it
+%% will measure how much time it takes just to loop over data and
+%% apply functions.
+base_case(Reps) ->
+ benchmark(Reps, % N repetitions
+ [], % Empty data structure
+ fun ?MODULE:null/3, % Create
+ fun ?MODULE:null/2, % Read
+ fun ?MODULE:null/3, % Update
+ fun ?MODULE:null/2). % Delete
+
+%% Ordered dictionaries. Assumes that the value is present on reads.
+smart_orddict(Reps) ->
+ benchmark(Reps,
+ orddict:new(),
+ fun orddict:store/3,
+ fun orddict:fetch/2,
+ fun orddict:store/3,
+ fun orddict:erase/2).
+
+%% Ordered dictionaries. Doesn't know whether a key is there or not on
+%% reads.
+naive_orddict(Reps) ->
+ benchmark(Reps,
+ orddict:new(),
+ fun orddict:store/3,
+ fun orddict:find/2,
+ fun orddict:store/3,
+ fun orddict:erase/2).
+
+%% Dictionary benchmark. Assumes that the value is present on reads.
+smart_dict(Reps) ->
+ benchmark(Reps,
+ dict:new(),
+ fun dict:store/3,
+ fun dict:fetch/2,
+ fun dict:store/3,
+ fun dict:erase/2).
+
+%% Dictionary benchmark. Doesn't know if the value exisst at read time.
+naive_dict(Reps) ->
+ benchmark(Reps,
+ dict:new(),
+ fun dict:store/3,
+ fun dict:find/2,
+ fun dict:store/3,
+ fun dict:erase/2).
+
+%% gb_trees benchmark. Uses the most general functions -- i.e.: it never
+%% assumes that the value is not in a tree when inserting and never assumes
+%% it is there when updating or deleting.
+naive_gb_trees(Reps) ->
+ benchmark(Reps,
+ gb_trees:empty(),
+ fun gb_trees:enter/3,
+ fun gb_trees:lookup/2,
+ fun gb_trees:enter/3,
+ fun gb_trees:delete_any/2).
+
+%% gb_trees benchmark. Uses specific function: it assumes that the values
+%% are not there when inserting and assumes it exists when updating or
+%% deleting.
+smart_gb_trees(Reps) ->
+ benchmark(Reps,
+ gb_trees:empty(),
+ fun gb_trees:insert/3,
+ fun gb_trees:get/2,
+ fun gb_trees:update/3,
+ fun gb_trees:delete/2).
+
+%% Empty functions used for the 'base_case/1' benchmark. They must do
+%% nothing interesting.
+null(_, _) -> ok.
+null(_, _, _) -> ok.
+
+%% Runs all the functions of 4 formats: Create, Read, Update, Delete.
+%% Create: it's a regular insertion, but it goes from an empty structure
+%% to a filled one. Requires an empty data structure,
+%% Read: reads every key from a complete data structure.
+%% Update: usually, this is the same as the insertion from 'create',
+%% except that it's run on full data structures. In some cases,
+%% like gb_trees, there also exist operations for updates when
+%% the keys are known that act differently from regular inserts.
+%% Delete: removes a key from a tree. Because we want to test the
+%% efficiency of it all, we will always delete from a complete
+%% structure.
+%% The function returns a list of all times averaged over the number
+%% of repetitions (Reps) needed.
+benchmark(Reps, Empty, CreateFun, ReadFun, UpdateFun, DeleteFun) ->
+ Keys = make_keys(Reps),
+ {TimeC, Struct} = timer:tc(?MODULE, create, [Keys, CreateFun, Empty]),
+ {TimeR, _} = timer:tc(?MODULE, read, [Keys, Struct, ReadFun]),
+ {TimeU, _} = timer:tc(?MODULE, update, [Keys, Struct, UpdateFun]),
+ {TimeD, _} = timer:tc(?MODULE, delete, [Keys, Struct, DeleteFun]),
+ [{create, TimeC, TimeC/Reps},
+ {read, TimeR, TimeR/Reps},
+ {update, TimeU, TimeU/Reps},
+ {delete, TimeD, TimeD/Reps}].
+
+%% Generate unique random numbers. No repetition allowed
+make_keys(N) ->
+ %% The trick is to generate all numbers as usual, but match them
+ %% with a random value in a tuple of the form {Random, Number}.
+ %% The idea is to then sort the list generated that way; done in
+ %% this manner, we know all values will be unique and the randomness
+ %% will be done by the sorting.
+ Random = lists:sort([{random:uniform(N), X} || X <- lists:seq(1, N)]),
+ %% it's a good idea to then filter out the index (the random index)
+ %% to only return the real numbers we want. This is simple to do
+ %% with a list comprehension where '_' removes the extraneous data.
+ %% The keys are then fit into a tuple to make the test a bit more
+ %% realistic for comparison.
+ [{some, key, X} || {_, X} <- Random].
+
+%% Loop function to apply the construction of a data structure.
+%% The parameters passed are a list of all keys to use and then the
+%% higher order function responsible of the creation of a data
+%% structure. This is usually a function of the form
+%% F(Key, Value, Structure).
+%% In the first call, the structure has to be the empty data structure
+%% that will progressively be filled.
+%% The only value inserted for each key is 'some_data'; we only care
+%% about the keys when dealing with key/value stuff.
+create([], _, Acc) -> Acc;
+create([Key|Rest], Fun, Acc) ->
+ create(Rest, Fun, Fun(Key, some_data, Acc)).
+
+%% Loop function to apply successive readings to a data structure.
+%% The parameters passed are a list of all keys, the complete data
+%% structure and then a higher order function responsible for
+%% fetching the data. Such functions are usually of the form
+%% F(Key, Structure).
+read([], _, _) -> ok;
+read([Key|Rest], Struct, Fun) ->
+ Fun(Key, Struct),
+ read(Rest, Struct, Fun).
+
+%% Loop function to apply updates to a data structure.
+%% Takes a list of keys, a full data structure and a higher order
+%% function responsible for the updating, usually of the form
+%% F(Key, NewValue, Structure).
+%% All values for a given key are replaced by 'newval', again because
+%% we don't care about the values, but merely the operations with
+%% the keys.
+update([], _, _) -> ok;
+update([Key|Rest], Struct, Fun) ->
+ Fun(Key, newval, Struct),
+ update(Rest, Struct, Fun).
+
+%% Loop function to apply deletions to a data structure.
+%% Each deletion is made on a full data structure.
+%% Takes a list of keys, a data structure and a higher order function
+%% to do the deletion. Usually of the form
+%% F(Key, Structure).
+delete([], _, _) -> ok;
+delete([Key|Rest], Struct, Fun) ->
+ Fun(Key, Struct),
+ delete(Rest, Struct, Fun).
diff --git a/learn-you-some-erlang/kitchen.erl b/learn-you-some-erlang/kitchen.erl
new file mode 100644
index 0000000..bbe0271
--- /dev/null
+++ b/learn-you-some-erlang/kitchen.erl
@@ -0,0 +1,64 @@
+-module(kitchen).
+-compile(export_all).
+
+start(FoodList) ->
+ spawn(?MODULE, fridge2, [FoodList]).
+
+store(Pid, Food) ->
+ Pid ! {self(), {store, Food}},
+ receive
+ {Pid, Msg} -> Msg
+ end.
+
+take(Pid, Food) ->
+ Pid ! {self(), {take, Food}},
+ receive
+ {Pid, Msg} -> Msg
+ end.
+
+store2(Pid, Food) ->
+ Pid ! {self(), {store, Food}},
+ receive
+ {Pid, Msg} -> Msg
+ after 3000 ->
+ timeout
+ end.
+
+take2(Pid, Food) ->
+ Pid ! {self(), {take, Food}},
+ receive
+ {Pid, Msg} -> Msg
+ after 3000 ->
+ timeout
+ end.
+
+fridge1() ->
+ receive
+ {From, {store, _Food}} ->
+ From ! {self(), ok},
+ fridge1();
+ {From, {take, _Food}} ->
+ %% uh....
+ From ! {self(), not_found},
+ fridge1();
+ terminate ->
+ ok
+ end.
+
+fridge2(FoodList) ->
+ receive
+ {From, {store, Food}} ->
+ From ! {self(), ok},
+ fridge2([Food|FoodList]);
+ {From, {take, Food}} ->
+ case lists:member(Food, FoodList) of
+ true ->
+ From ! {self(), {ok, Food}},
+ fridge2(lists:delete(Food, FoodList));
+ false ->
+ From ! {self(), not_found},
+ fridge2(FoodList)
+ end;
+ terminate ->
+ ok
+ end.
diff --git a/learn-you-some-erlang/kitty_gen_server.erl b/learn-you-some-erlang/kitty_gen_server.erl
new file mode 100644
index 0000000..c906a6a
--- /dev/null
+++ b/learn-you-some-erlang/kitty_gen_server.erl
@@ -0,0 +1,57 @@
+-module(kitty_gen_server).
+-behaviour(gen_server).
+
+-export([start_link/0, order_cat/4, return_cat/2, close_shop/1]).
+-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
+ terminate/2, code_change/3]).
+
+-record(cat, {name, color=green, description}).
+
+%%% Client API
+start_link() ->
+ gen_server:start_link(?MODULE, [], []).
+
+%% Synchronous call
+order_cat(Pid, Name, Color, Description) ->
+ gen_server:call(Pid, {order, Name, Color, Description}).
+
+%% This call is asynchronous
+return_cat(Pid, Cat = #cat{}) ->
+ gen_server:cast(Pid, {return, Cat}).
+
+%% Synchronous call
+close_shop(Pid) ->
+ gen_server:call(Pid, terminate).
+
+%%% Server functions
+init([]) -> {ok, []}. %% no treatment of info here!
+
+handle_call({order, Name, Color, Description}, _From, Cats) ->
+ if Cats =:= [] ->
+ {reply, make_cat(Name, Color, Description), Cats};
+ Cats =/= [] ->
+ {reply, hd(Cats), tl(Cats)}
+ end;
+handle_call(terminate, _From, Cats) ->
+ {stop, normal, ok, Cats}.
+
+handle_cast({return, Cat = #cat{}}, Cats) ->
+ {noreply, [Cat|Cats]}.
+
+handle_info(Msg, Cats) ->
+ io:format("Unexpected message: ~p~n",[Msg]),
+ {noreply, Cats}.
+
+terminate(normal, Cats) ->
+ [io:format("~p was set free.~n",[C#cat.name]) || C <- Cats],
+ ok.
+
+code_change(_OldVsn, State, _Extra) ->
+ %% No change planned. The function is there for the behaviour,
+ %% but will not be used. Only a version on the next
+ {ok, State}.
+
+%%% Private functions
+make_cat(Name, Col, Desc) ->
+ #cat{name=Name, color=Col, description=Desc}.
+
diff --git a/learn-you-some-erlang/kitty_server.erl b/learn-you-some-erlang/kitty_server.erl
new file mode 100644
index 0000000..a40e20a
--- /dev/null
+++ b/learn-you-some-erlang/kitty_server.erl
@@ -0,0 +1,74 @@
+%%%%% Naive version
+-module(kitty_server).
+
+-export([start_link/0, order_cat/4, return_cat/2, close_shop/1]).
+
+-record(cat, {name, color=green, description}).
+
+%%% Client API
+start_link() -> spawn_link(fun init/0).
+
+%% Synchronous call
+order_cat(Pid, Name, Color, Description) ->
+ Ref = erlang:monitor(process, Pid),
+ Pid ! {self(), Ref, {order, Name, Color, Description}},
+ receive
+ {Ref, Cat = #cat{}} ->
+ erlang:demonitor(Ref, [flush]),
+ Cat;
+ {'DOWN', Ref, process, Pid, Reason} ->
+ erlang:error(Reason)
+ after 5000 ->
+ erlang:error(timeout)
+ end.
+
+%% This call is asynchronous
+return_cat(Pid, Cat = #cat{}) ->
+ Pid ! {return, Cat},
+ ok.
+
+%% Synchronous call
+close_shop(Pid) ->
+ Ref = erlang:monitor(process, Pid),
+ Pid ! {self(), Ref, terminate},
+ receive
+ {Ref, ok} ->
+ erlang:demonitor(Ref, [flush]),
+ ok;
+ {'DOWN', Ref, process, Pid, Reason} ->
+ erlang:error(Reason)
+ after 5000 ->
+ erlang:error(timeout)
+ end.
+
+%%% Server functions
+init() -> loop([]).
+
+loop(Cats) ->
+ receive
+ {Pid, Ref, {order, Name, Color, Description}} ->
+ if Cats =:= [] ->
+ Pid ! {Ref, make_cat(Name, Color, Description)},
+ loop(Cats);
+ Cats =/= [] -> % got to empty the stock
+ Pid ! {Ref, hd(Cats)},
+ loop(tl(Cats))
+ end;
+ {return, Cat = #cat{}} ->
+ loop([Cat|Cats]);
+ {Pid, Ref, terminate} ->
+ Pid ! {Ref, ok},
+ terminate(Cats);
+ Unknown ->
+ %% do some logging here too
+ io:format("Unknown message: ~p~n", [Unknown]),
+ loop(Cats)
+ end.
+
+%%% Private functions
+make_cat(Name, Col, Desc) ->
+ #cat{name=Name, color=Col, description=Desc}.
+
+terminate(Cats) ->
+ [io:format("~p was set free.~n",[C#cat.name]) || C <- Cats],
+ ok.
diff --git a/learn-you-some-erlang/kitty_server2.erl b/learn-you-some-erlang/kitty_server2.erl
new file mode 100644
index 0000000..b2acbb2
--- /dev/null
+++ b/learn-you-some-erlang/kitty_server2.erl
@@ -0,0 +1,49 @@
+%%%%% Abstracted version
+-module(kitty_server2).
+
+-export([start_link/0, order_cat/4, return_cat/2, close_shop/1]).
+-export([init/1, handle_call/3, handle_cast/2]).
+
+-record(cat, {name, color=green, description}).
+
+%%% Client API
+start_link() -> my_server:start_link(?MODULE, []).
+
+%% Synchronous call
+order_cat(Pid, Name, Color, Description) ->
+ my_server:call(Pid, {order, Name, Color, Description}).
+
+%% This call is asynchronous
+return_cat(Pid, Cat = #cat{}) ->
+ my_server:cast(Pid, {return, Cat}).
+
+%% Synchronous call
+close_shop(Pid) ->
+ my_server:call(Pid, terminate).
+
+%%% Server functions
+init([]) -> []. %% no treatment of info here!
+
+handle_call({order, Name, Color, Description}, From, Cats) ->
+ if Cats =:= [] ->
+ my_server:reply(From, make_cat(Name, Color, Description)),
+ Cats;
+ Cats =/= [] ->
+ my_server:reply(From, hd(Cats)),
+ tl(Cats)
+ end;
+
+handle_call(terminate, From, Cats) ->
+ my_server:reply(From, ok),
+ terminate(Cats).
+
+handle_cast({return, Cat = #cat{}}, Cats) ->
+ [Cat|Cats].
+
+%%% Private functions
+make_cat(Name, Col, Desc) ->
+ #cat{name=Name, color=Col, description=Desc}.
+
+terminate(Cats) ->
+ [io:format("~p was set free.~n",[C#cat.name]) || C <- Cats],
+ exit(normal).
diff --git a/learn-you-some-erlang/linkmon.erl b/learn-you-some-erlang/linkmon.erl
new file mode 100644
index 0000000..f93e9e5
--- /dev/null
+++ b/learn-you-some-erlang/linkmon.erl
@@ -0,0 +1,83 @@
+-module(linkmon).
+-compile([export_all]).
+
+myproc() ->
+ timer:sleep(5000),
+ exit(reason).
+
+chain(0) ->
+ receive
+ _ -> ok
+ after 2000 ->
+ exit("chain dies here")
+ end;
+chain(N) ->
+ Pid = spawn(fun() -> chain(N-1) end),
+ link(Pid),
+ receive
+ _ -> ok
+ end.
+
+start_critic() ->
+ spawn(?MODULE, critic, []).
+
+judge(Pid, Band, Album) ->
+ Pid ! {self(), {Band, Album}},
+ receive
+ {Pid, Criticism} -> Criticism
+ after 2000 ->
+ timeout
+ end.
+
+critic() ->
+ receive
+ {From, {"Rage Against the Turing Machine", "Unit Testify"}} ->
+ From ! {self(), "They are great!"};
+ {From, {"System of a Downtime", "Memoize"}} ->
+ From ! {self(), "They're not Johnny Crash but they're good."};
+ {From, {"Johnny Crash", "The Token Ring of Fire"}} ->
+ From ! {self(), "Simply incredible."};
+ {From, {_Band, _Album}} ->
+ From ! {self(), "They are terrible!"}
+ end,
+ critic().
+
+
+start_critic2() ->
+ spawn(?MODULE, restarter, []).
+
+restarter() ->
+ process_flag(trap_exit, true),
+ Pid = spawn_link(?MODULE, critic2, []),
+ register(critic, Pid),
+ receive
+ {'EXIT', Pid, normal} -> % not a crash
+ ok;
+ {'EXIT', Pid, shutdown} -> % manual shutdown, not a crash
+ ok;
+ {'EXIT', Pid, _} ->
+ restarter()
+ end.
+
+judge2(Band, Album) ->
+ Ref = make_ref(),
+ critic ! {self(), Ref, {Band, Album}},
+ receive
+ {Ref, Criticism} -> Criticism
+ after 2000 ->
+ timeout
+ end.
+
+critic2() ->
+ receive
+ {From, Ref, {"Rage Against the Turing Machine", "Unit Testify"}} ->
+ From ! {Ref, "They are great!"};
+ {From, Ref, {"System of a Downtime", "Memoize"}} ->
+ From ! {Ref, "They're not Johnny Crash but they're good."};
+ {From, Ref, {"Johnny Crash", "The Token Ring of Fire"}} ->
+ From ! {Ref, "Simply incredible."};
+ {From, Ref, {_Band, _Album}} ->
+ From ! {Ref, "They are terrible!"}
+ end,
+ critic2().
+
diff --git a/learn-you-some-erlang/m8ball/Emakefile b/learn-you-some-erlang/m8ball/Emakefile
new file mode 100644
index 0000000..b203ea3
--- /dev/null
+++ b/learn-you-some-erlang/m8ball/Emakefile
@@ -0,0 +1,2 @@
+{["src/*", "test/*"],
+ [{i,"include"}, {outdir, "ebin"}]}.
diff --git a/learn-you-some-erlang/m8ball/config/a.config b/learn-you-some-erlang/m8ball/config/a.config
new file mode 100644
index 0000000..983d5e1
--- /dev/null
+++ b/learn-you-some-erlang/m8ball/config/a.config
@@ -0,0 +1,9 @@
+[{kernel,
+ [{distributed, [{m8ball,
+ 5000,
+ [a@ferdmbp, {b@ferdmbp, c@ferdmbp}]}]},
+ {sync_nodes_mandatory, [b@ferdmbp, c@ferdmbp]},
+ {sync_nodes_timeout, 30000}
+ ]
+ }
+].
diff --git a/learn-you-some-erlang/m8ball/config/b.config b/learn-you-some-erlang/m8ball/config/b.config
new file mode 100644
index 0000000..2d0ccfc
--- /dev/null
+++ b/learn-you-some-erlang/m8ball/config/b.config
@@ -0,0 +1,9 @@
+[{kernel,
+ [{distributed, [{m8ball,
+ 5000,
+ [a@ferdmbp, {b@ferdmbp, c@ferdmbp}]}]},
+ {sync_nodes_mandatory, [a@ferdmbp, c@ferdmbp]},
+ {sync_nodes_timeout, 30000}
+ ]
+ }
+].
diff --git a/learn-you-some-erlang/m8ball/config/c.config b/learn-you-some-erlang/m8ball/config/c.config
new file mode 100644
index 0000000..3dd3609
--- /dev/null
+++ b/learn-you-some-erlang/m8ball/config/c.config
@@ -0,0 +1,9 @@
+[{kernel,
+ [{distributed, [{m8ball,
+ 5000,
+ [a@ferdmbp, {b@ferdmbp, c@ferdmbp}]}]},
+ {sync_nodes_mandatory, [a@ferdmbp, b@ferdmbp]},
+ {sync_nodes_timeout, 30000}
+ ]
+ }
+].
diff --git a/learn-you-some-erlang/m8ball/ebin/m8ball.app b/learn-you-some-erlang/m8ball/ebin/m8ball.app
new file mode 100644
index 0000000..352c1d4
--- /dev/null
+++ b/learn-you-some-erlang/m8ball/ebin/m8ball.app
@@ -0,0 +1,13 @@
+{application, m8ball,
+ [{vsn, "1.0.0"},
+ {description, "Answer vital questions"},
+ {modules, [m8ball, m8ball_sup, m8ball_server]},
+ {applications, [stdlib, kernel, crypto]},
+ {registered, [m8ball, m8ball_sup, m8ball_server]},
+ {mod, {m8ball, []}},
+ {env, [
+ {answers, {<<"Yes">>, <<"No">>, <<"Doubtful">>,
+ <<"I don't like your tone">>, <<"Of course">>,
+ <<"Of course not">>, <<"*backs away slowly and runs away*">>}}
+ ]}
+ ]}.
diff --git a/learn-you-some-erlang/m8ball/logs/.track-this b/learn-you-some-erlang/m8ball/logs/.track-this
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/learn-you-some-erlang/m8ball/logs/.track-this
diff --git a/learn-you-some-erlang/m8ball/src/m8ball.erl b/learn-you-some-erlang/m8ball/src/m8ball.erl
new file mode 100644
index 0000000..041601b
--- /dev/null
+++ b/learn-you-some-erlang/m8ball/src/m8ball.erl
@@ -0,0 +1,24 @@
+-module(m8ball).
+-behaviour(application).
+-export([start/2, stop/1]).
+-export([ask/1]).
+
+%%%%%%%%%%%%%%%%%
+%%% CALLBACKS %%%
+%%%%%%%%%%%%%%%%%
+
+%% start({failover, Node}, Args) is only called
+%% when a start_phase key is defined.
+start(normal, []) ->
+ m8ball_sup:start_link();
+start({takeover, _OtherNode}, []) ->
+ m8ball_sup:start_link().
+
+stop(_State) ->
+ ok.
+
+%%%%%%%%%%%%%%%%%
+%%% INTERFACE %%%
+%%%%%%%%%%%%%%%%%
+ask(Question) ->
+ m8ball_server:ask(Question).
diff --git a/learn-you-some-erlang/m8ball/src/m8ball_server.erl b/learn-you-some-erlang/m8ball/src/m8ball_server.erl
new file mode 100644
index 0000000..4e821ad
--- /dev/null
+++ b/learn-you-some-erlang/m8ball/src/m8ball_server.erl
@@ -0,0 +1,46 @@
+-module(m8ball_server).
+-behaviour(gen_server).
+-export([start_link/0, stop/0, ask/1]).
+-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
+ code_change/3, terminate/2]).
+
+%%%%%%%%%%%%%%%%%
+%%% INTERFACE %%%
+%%%%%%%%%%%%%%%%%
+start_link() ->
+ gen_server:start_link({global, ?MODULE}, ?MODULE, [], []).
+
+stop() ->
+ gen_server:call({global, ?MODULE}, stop).
+
+ask(_Question) -> % the question doesn't matter!
+ gen_server:call({global, ?MODULE}, question).
+
+%%%%%%%%%%%%%%%%%
+%%% CALLBACKS %%%
+%%%%%%%%%%%%%%%%%
+init([]) ->
+ <<A:32, B:32, C:32>> = crypto:rand_bytes(12),
+ random:seed(A,B,C),
+ {ok, []}.
+
+handle_call(question, _From, State) ->
+ {ok, Answers} = application:get_env(m8ball, answers),
+ Answer = element(random:uniform(tuple_size(Answers)), Answers),
+ {reply, Answer, State};
+handle_call(stop, _From, State) ->
+ {stop, normal, ok, State};
+handle_call(_Call, _From, State) ->
+ {noreply, State}.
+
+handle_cast(_Cast, State) ->
+ {noreply, State}.
+
+handle_info(_Info, State) ->
+ {noreply, State}.
+
+code_change(_OldVsn, State, _Extra) ->
+ {ok, State}.
+
+terminate(_Reason, _State) ->
+ ok.
diff --git a/learn-you-some-erlang/m8ball/src/m8ball_sup.erl b/learn-you-some-erlang/m8ball/src/m8ball_sup.erl
new file mode 100644
index 0000000..ca5e426
--- /dev/null
+++ b/learn-you-some-erlang/m8ball/src/m8ball_sup.erl
@@ -0,0 +1,16 @@
+-module(m8ball_sup).
+-behaviour(supervisor).
+-export([start_link/0, init/1]).
+
+start_link() ->
+ supervisor:start_link({global,?MODULE}, ?MODULE, []).
+
+init([]) ->
+ {ok, {{one_for_one, 1, 10},
+ [{m8ball,
+ {m8ball_server, start_link, []},
+ permanent,
+ 5000,
+ worker,
+ [m8ball_server]
+ }]}}.
diff --git a/learn-you-some-erlang/m8ball/test/dist_m8ball_SUITE.erl b/learn-you-some-erlang/m8ball/test/dist_m8ball_SUITE.erl
new file mode 100644
index 0000000..3afba6e
--- /dev/null
+++ b/learn-you-some-erlang/m8ball/test/dist_m8ball_SUITE.erl
@@ -0,0 +1,32 @@
+-module(dist_m8ball_SUITE).
+-include_lib("common_test/include/ct.hrl").
+-export([all/0, groups/0, init_per_group/2, end_per_group/2]).
+-export([can_contact/1]).
+
+all() -> [{group, main}, {group, backup}].
+
+groups() -> [{main,
+ [],
+ [can_contact]},
+ {backup,
+ [],
+ [can_contact]}].
+
+init_per_group(main, Config) ->
+ application:start(crypto),
+ application:start(m8ball),
+ Config;
+init_per_group(backup, Config) ->
+ application:start(crypto),
+ application:start(m8ball),
+ Config.
+
+end_per_group(main, _Config) ->
+ %application:stop(m8ball),
+ ok;
+end_per_group(backup, _Config) ->
+ %application:stop(m8ball),
+ ok.
+
+can_contact(_Config) ->
+ <<_/binary>> = m8ball:ask(<<"Some Question">>).
diff --git a/learn-you-some-erlang/m8ball/test/dist_m8ball_SUITE_data/backup.config b/learn-you-some-erlang/m8ball/test/dist_m8ball_SUITE_data/backup.config
new file mode 100644
index 0000000..7fbe260
--- /dev/null
+++ b/learn-you-some-erlang/m8ball/test/dist_m8ball_SUITE_data/backup.config
@@ -0,0 +1,9 @@
+[{kernel,
+ [{distributed, [{m8ball,
+ 5000,
+ ['a@li101-172.members.linode.com', 'b@li101-172.members.linode.com']}]},
+ {sync_nodes_mandatory, ['a@li101-172.members.linode.com']},
+ {sync_nodes_timeout, 5000}
+ ]
+ }
+].
diff --git a/learn-you-some-erlang/m8ball/test/dist_m8ball_SUITE_data/main.config b/learn-you-some-erlang/m8ball/test/dist_m8ball_SUITE_data/main.config
new file mode 100644
index 0000000..bcac447
--- /dev/null
+++ b/learn-you-some-erlang/m8ball/test/dist_m8ball_SUITE_data/main.config
@@ -0,0 +1,9 @@
+[{kernel,
+ [{distributed, [{m8ball,
+ 5000,
+ ['a@li101-172.members.linode.com', 'b@li101-172.members.linode.com']}]},
+ {sync_nodes_mandatory, ['b@li101-172.members.linode.com']},
+ {sync_nodes_timeout, 5000}
+ ]
+ }
+].
diff --git a/learn-you-some-erlang/m8ball/test/m8ball.spec b/learn-you-some-erlang/m8ball/test/m8ball.spec
new file mode 100644
index 0000000..51d9cf3
--- /dev/null
+++ b/learn-you-some-erlang/m8ball/test/m8ball.spec
@@ -0,0 +1,5 @@
+{include, "../include/"}.
+{alias, root, "/home/ferd/code/learn-you-some-erlang/m8ball/test"}.
+{logdir, "/home/ferd/code/learn-you-some-erlang/m8ball/logs"}.
+
+{suites, root, all}.
diff --git a/learn-you-some-erlang/m8ball/test/m8ball_dist.spec b/learn-you-some-erlang/m8ball/test/m8ball_dist.spec
new file mode 100644
index 0000000..c49a6ca
--- /dev/null
+++ b/learn-you-some-erlang/m8ball/test/m8ball_dist.spec
@@ -0,0 +1,20 @@
+{node, master, 'ct@li101-172.members.linode.com'}.
+{node, a, 'a@li101-172.members.linode.com'}.
+{node, b, 'b@li101-172.members.linode.com'}.
+
+{init, a, [{node_start, [{monitor_master, true},
+ {boot_timeout, 10000},
+ {erl_flags, "-pa /home/ferd/code/learn-you-some-erlang/m8ball/ebin/ "
+ "-config /home/ferd/code/learn-you-some-erlang/m8ball/test/dist_m8ball_SUITE_data/main.config"}]}]}.
+{init, b, [{node_start, [{monitor_master, true},
+ {boot_timeout, 10000},
+ {erl_flags, "-pa /home/ferd/code/learn-you-some-erlang/m8ball/ebin/ "
+ "-config /home/ferd/code/learn-you-some-erlang/m8ball/test/dist_m8ball_SUITE_data/backup.config"}]}]}.
+
+{include, "../include/"}.
+{alias, root, "/home/ferd/code/learn-you-some-erlang/m8ball/test"}.
+{logdir, "/home/ferd/code/learn-you-some-erlang/m8ball/logs"}.
+{logdir, master, "/home/ferd/code/learn-you-some-erlang/m8ball/logs"}.
+
+{groups, a, root, dist_m8ball_SUITE, main}.
+{groups, b, root, dist_m8ball_SUITE, backup}.
diff --git a/learn-you-some-erlang/m8ball/test/m8ball_server_SUITE.erl b/learn-you-some-erlang/m8ball/test/m8ball_server_SUITE.erl
new file mode 100644
index 0000000..6bc4156
--- /dev/null
+++ b/learn-you-some-erlang/m8ball/test/m8ball_server_SUITE.erl
@@ -0,0 +1,45 @@
+-module(m8ball_server_SUITE).
+-include_lib("common_test/include/ct.hrl").
+-export([all/0, init_per_testcase/2, end_per_testcase/2]).
+-export([random_answer/1, binary_answer/1, determined_answers/1,
+ reload_answers/1]).
+
+all() -> [random_answer, binary_answer, determined_answers,
+ reload_answers].
+
+init_per_testcase(_Test, Config) ->
+ Answers = [<<"Outlook not so good">>,
+ <<"I don't think so">>,
+ <<"Yes, definitely">>,
+ <<"STOP SHAKING ME">>],
+ application:set_env(m8ball, answers, list_to_tuple(Answers)),
+ m8ball_server:start_link(),
+ [{answers, Answers} | Config].
+
+end_per_testcase(_Test, _Config) ->
+ m8ball_server:stop().
+
+%% The answer should come from the config
+random_answer(_Config) ->
+ Answers1 = [m8ball_server:ask("Dummy question") || _ <- lists:seq(1,15)],
+ Answers2 = [m8ball_server:ask("Dummy question") || _ <- lists:seq(1,15)],
+ true = 1 < length(sets:to_list(sets:from_list(Answers1))),
+ true = Answers1 =/= Answers2.
+
+binary_answer(_Config) ->
+ Answers = [m8ball_server:ask("Dummy question") || _ <- lists:seq(1,15)],
+ L = length(Answers),
+ L = length(lists:filter(fun erlang:is_binary/1, Answers)).
+
+determined_answers(Config) ->
+ Answers = ?config(answers, Config),
+ Res = [m8ball_server:ask("Dummy question") || _ <- lists:seq(1,15)],
+ true = lists:all(fun(X) -> lists:member(X, Answers) end, Res).
+
+reload_answers(_Config) ->
+ Ref = make_ref(),
+ application:set_env(m8ball, answers, {Ref}),
+ [Ref,Ref,Ref] = [m8ball_server:ask("Question") || _ <- lists:seq(1,3)].
+
+
+%% NOTE: do distributed testing in m8ball_SUITE or something
diff --git a/learn-you-some-erlang/mafiapp-1.0.0/Emakefile b/learn-you-some-erlang/mafiapp-1.0.0/Emakefile
new file mode 100644
index 0000000..b203ea3
--- /dev/null
+++ b/learn-you-some-erlang/mafiapp-1.0.0/Emakefile
@@ -0,0 +1,2 @@
+{["src/*", "test/*"],
+ [{i,"include"}, {outdir, "ebin"}]}.
diff --git a/learn-you-some-erlang/mafiapp-1.0.0/ebin/mafiapp.app b/learn-you-some-erlang/mafiapp-1.0.0/ebin/mafiapp.app
new file mode 100644
index 0000000..972432f
--- /dev/null
+++ b/learn-you-some-erlang/mafiapp-1.0.0/ebin/mafiapp.app
@@ -0,0 +1,5 @@
+{application, mafiapp,
+ [{description, "Help the boss keep track of his friends"},
+ {vsn, "1.0.0"},
+ {modules, [mafiapp, mafiapp_sup]},
+ {applications, [stdlib, kernel, mnesia]}]}.
diff --git a/learn-you-some-erlang/mafiapp-1.0.0/logs/.keep-this b/learn-you-some-erlang/mafiapp-1.0.0/logs/.keep-this
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/learn-you-some-erlang/mafiapp-1.0.0/logs/.keep-this
diff --git a/learn-you-some-erlang/mafiapp-1.0.0/mafiapp.spec b/learn-you-some-erlang/mafiapp-1.0.0/mafiapp.spec
new file mode 100644
index 0000000..4953e67
--- /dev/null
+++ b/learn-you-some-erlang/mafiapp-1.0.0/mafiapp.spec
@@ -0,0 +1,3 @@
+{alias, root, "./test/"}.
+{logdir, "./logs/"}.
+{suites, root, all}.
diff --git a/learn-you-some-erlang/mafiapp-1.0.0/src/mafiapp.erl b/learn-you-some-erlang/mafiapp-1.0.0/src/mafiapp.erl
new file mode 100644
index 0000000..0e6a2b0
--- /dev/null
+++ b/learn-you-some-erlang/mafiapp-1.0.0/src/mafiapp.erl
@@ -0,0 +1,172 @@
+-module(mafiapp).
+-behaviour(application).
+-include_lib("stdlib/include/ms_transform.hrl").
+-export([start/2, stop/1]).
+-export([install/1]).
+-export([add_friend/4, friend_by_name/1, friend_by_expertise/1,
+ add_service/4, debts/1]).
+-export([add_enemy/2, find_enemy/1, enemy_killed/1]).
+
+
+-record(mafiapp_friends, {name,
+ contact=[],
+ info=[],
+ expertise}).
+-record(mafiapp_services, {from,
+ to,
+ date,
+ description}).
+-record(mafiapp_enemies, {name,
+ info=[]}).
+
+start(normal, []) ->
+ mnesia:wait_for_tables([mafiapp_friends,
+ mafiapp_services,
+ mafiapp_enemies], 5000),
+ mafiapp_sup:start_link().
+
+
+stop(_) -> ok.
+
+install(Nodes) ->
+ ok = mnesia:create_schema(Nodes),
+ rpc:multicall(Nodes, application, start, [mnesia]),
+ mnesia:create_table(mafiapp_friends,
+ [{attributes, record_info(fields, mafiapp_friends)},
+ {index, [#mafiapp_friends.expertise]},
+ {disc_copies, Nodes}]),
+ mnesia:create_table(mafiapp_services,
+ [{attributes, record_info(fields, mafiapp_services)},
+ {index, [#mafiapp_services.to]},
+ {disc_copies, Nodes},
+ {type, bag}]),
+ mnesia:create_table(mafiapp_enemies,
+ [{attributes, record_info(fields, mafiapp_enemies)},
+ {disc_copies, Nodes},
+ {local_content, true}]),
+ rpc:multicall(Nodes, application, stop, [mnesia]).
+
+add_friend(Name, Contact, Info, Expertise) ->
+ F = fun() ->
+ mnesia:write(#mafiapp_friends{name=Name,
+ contact=Contact,
+ info=Info,
+ expertise=Expertise})
+ end,
+ mnesia:activity(transaction, F).
+
+friend_by_name(Name) ->
+ F = fun() ->
+ case mnesia:read({mafiapp_friends, Name}) of
+ [#mafiapp_friends{contact=C, info=I, expertise=E}] ->
+ {Name,C,I,E,find_services(Name)};
+ [] ->
+ undefined
+ end
+ end,
+ mnesia:activity(transaction, F).
+
+friend_by_expertise(Expertise) ->
+ %% Alternative form:
+ %% MatchSpec = [{#mafiapp_friends{name = '$1',
+ %% contact = '$2',
+ %% info = '$3',
+ %% expertise = Expertise},
+ %% [],
+ %% [{{'$1','$2','$3',Expertise}}]}],
+ %% F = fun() ->
+ %% [{Name,C,I,E,find_services(Name)} ||
+ %% {Name,C,I,E} <- mnesia:select(mafiapp_friends, MatchSpec)]
+ %% end,
+ Pattern = #mafiapp_friends{_ = '_',
+ expertise = Expertise},
+ F = fun() ->
+ Res = mnesia:match_object(Pattern),
+ [{Name,C,I,Expertise,find_services(Name)} ||
+ #mafiapp_friends{name=Name,
+ contact=C,
+ info=I} <- Res]
+ end,
+ mnesia:activity(transaction, F).
+
+%% Adding validation is left to the reader
+add_service(From, To, Date, Description) ->
+ F = fun() ->
+ case mnesia:read({mafiapp_friends, From}) =:= [] orelse
+ mnesia:read({mafiapp_friends, To}) =:= [] of
+ true ->
+ {error, unknown_friend};
+ false ->
+ mnesia:write(#mafiapp_services{from=From,
+ to=To,
+ date=Date,
+ description=Description})
+ end
+ end,
+ mnesia:activity(transaction,F).
+
+debts(Name) ->
+ Match = ets:fun2ms(
+ fun(#mafiapp_services{from=From, to=To}) when From =:= Name ->
+ {To,-1};
+ (#mafiapp_services{from=From, to=To}) when To =:= Name ->
+ {From,1}
+ end),
+ F = fun() -> mnesia:select(mafiapp_services, Match) end,
+ Dict = lists:foldl(fun({Person,N}, Dict) ->
+ dict:update(Person, fun(X) -> X + N end, N, Dict)
+ end,
+ dict:new(),
+ mnesia:activity(transaction, F)),
+ lists:sort([{V,K} || {K,V} <- dict:to_list(Dict)]).
+%% The following implementation is a somewhat more efficient
+%% alternative on large databases, given it will use indexes
+%% rather than traversing the entire table.
+% F = fun() ->
+% Given = mnesia:read({mafiapp_services, Name}),
+% Received = mnesia:match_object(#mafiapp_services{to=Name, _='_'}),
+% {Given, Received}
+% end,
+% {Given, Received} = mnesia:activity(transaction, F),
+% Dict1 = lists:foldl(fun(#mafiapp_services{to=N}, Dict) ->
+% dict:update(N, fun(X) -> X - 1 end, -1, Dict)
+% end,
+% dict:new(),
+% Given),
+% Dict2 = lists:foldl(fun(#mafiapp_services{from=N}, Dict) ->
+% dict:update(N, fun(X) -> X + 1 end, 1, Dict)
+% end,
+% Dict1,
+% Received),
+% lists:sort([{V,K} || {K,V} <- dict:to_list(Dict2)]).
+
+add_enemy(Name, Info) ->
+ F = fun() -> mnesia:write(#mafiapp_enemies{name=Name, info=Info}) end,
+ mnesia:activity(transaction, F).
+
+find_enemy(Name) ->
+ F = fun() -> mnesia:read({mafiapp_enemies, Name}) end,
+ case mnesia:activity(transaction, F) of
+ [] -> undefined;
+ [#mafiapp_enemies{name=N, info=I}] -> {N,I}
+ end.
+
+enemy_killed(Name) ->
+ F = fun() -> mnesia:delete({mafiapp_enemies, Name}) end,
+ mnesia:activity(transaction, F).
+
+%%%%%%%%%%%%%%%
+%%% PRIVATE %%%
+%%%%%%%%%%%%%%%
+
+find_services(Name) ->
+ Match = ets:fun2ms(
+ fun(#mafiapp_services{from=From, to=To, date=D, description=Desc})
+ when From =:= Name ->
+ {to, To, D, Desc};
+ (#mafiapp_services{from=From, to=To, date=D, description=Desc})
+ when To =:= Name ->
+ {from, From, D, Desc}
+ end
+ ),
+ mnesia:select(mafiapp_services, Match).
diff --git a/learn-you-some-erlang/mafiapp-1.0.0/src/mafiapp_sup.erl b/learn-you-some-erlang/mafiapp-1.0.0/src/mafiapp_sup.erl
new file mode 100644
index 0000000..e31b221
--- /dev/null
+++ b/learn-you-some-erlang/mafiapp-1.0.0/src/mafiapp_sup.erl
@@ -0,0 +1,12 @@
+-module(mafiapp_sup).
+-behaviour(supervisor).
+-export([start_link/0]).
+-export([init/1]).
+
+start_link() ->
+ supervisor:start_link(?MODULE, []).
+
+%% This does absolutely nothing, only there to
+%% allow to wait for tables.
+init([]) ->
+ {ok, {{one_for_one, 1, 1}, []}}.
diff --git a/learn-you-some-erlang/mafiapp-1.0.0/test/mafiapp.spec b/learn-you-some-erlang/mafiapp-1.0.0/test/mafiapp.spec
new file mode 100644
index 0000000..4953e67
--- /dev/null
+++ b/learn-you-some-erlang/mafiapp-1.0.0/test/mafiapp.spec
@@ -0,0 +1,3 @@
+{alias, root, "./test/"}.
+{logdir, "./logs/"}.
+{suites, root, all}.
diff --git a/learn-you-some-erlang/mafiapp-1.0.0/test/mafiapp_SUITE.erl b/learn-you-some-erlang/mafiapp-1.0.0/test/mafiapp_SUITE.erl
new file mode 100644
index 0000000..6005d29
--- /dev/null
+++ b/learn-you-some-erlang/mafiapp-1.0.0/test/mafiapp_SUITE.erl
@@ -0,0 +1,128 @@
+-module(mafiapp_SUITE).
+-include_lib("common_test/include/ct.hrl").
+-export([init_per_suite/1, end_per_suite/1,
+ init_per_testcase/2, end_per_testcase/2,
+ all/0]).
+-export([add_service/1, friend_by_name/1, friend_by_expertise/1,
+ friend_with_services/1, accounts/1, enemies/1]).
+
+all() -> [add_service, friend_by_name, friend_by_expertise,
+ friend_with_services, accounts, enemies].
+
+init_per_suite(Config) ->
+ Priv = ?config(priv_dir, Config),
+ application:load(mnesia),
+ application:set_env(mnesia, dir, Priv),
+ application:load(mafiapp),
+ mafiapp:install([node()]),
+ application:start(mnesia),
+ application:start(mafiapp),
+ Config.
+
+end_per_suite(_Config) ->
+ application:stop(mnesia),
+ ok.
+
+init_per_testcase(add_service, Config) ->
+ Config;
+init_per_testcase(accounts, Config) ->
+ ok = mafiapp:add_friend("Consigliere", [], [you], consigliere),
+ Config;
+init_per_testcase(_, Config) ->
+ ok = mafiapp:add_friend("Don Corleone", [], [boss], boss),
+ Config.
+
+end_per_testcase(_, _Config) ->
+ ok.
+
+%% services can go both way: from a friend to the boss, or
+%% from the boss to a friend! A boss friend is required!
+add_service(_Config) ->
+ {error, unknown_friend} = mafiapp:add_service("from name",
+ "to name",
+ {1946,5,23},
+ "a fake service"),
+ ok = mafiapp:add_friend("Don Corleone", [], [boss], boss),
+ ok = mafiapp:add_friend("Alan Parsons",
+ [{twitter,"@ArtScienceSound"}],
+ [{born, {1948,12,20}},
+ musician, 'audio engineer',
+ producer, "has projects"],
+ mixing),
+ ok = mafiapp:add_service("Alan Parsons", "Don Corleone",
+ {1973,3,1},
+ "Helped release a Pink Floyd album").
+
+friend_by_name(_Config) ->
+ ok = mafiapp:add_friend("Pete Cityshend",
+ [{phone, "418-542-3000"},
+ {email, "quadrophonia@example.org"},
+ {other, "yell real loud"}],
+ [{born, {1945,5,19}},
+ musician, popular],
+ music),
+ {"Pete Cityshend",
+ _Contact, _Info, music,
+ _Services} = mafiapp:friend_by_name("Pete Cityshend"),
+ undefined = mafiapp:friend_by_name(make_ref()).
+
+friend_by_expertise(_Config) ->
+ ok = mafiapp:add_friend("A Red Panda",
+ [{location, "in a zoo"}],
+ [animal,cute],
+ climbing),
+ [{"A Red Panda",
+ _Contact, _Info, climbing,
+ _Services}] = mafiapp:friend_by_expertise(climbing),
+ [] = mafiapp:friend_by_expertise(make_ref()).
+
+friend_with_services(_Config) ->
+ ok = mafiapp:add_friend("Someone", [{other, "at the fruit stand"}],
+ [weird, mysterious], shadiness),
+ ok = mafiapp:add_service("Don Corleone", "Someone",
+ {1949,2,14}, "Increased business"),
+ ok = mafiapp:add_service("Someone", "Don Corleone",
+ {1949,12,25}, "Gave a Christmas gift"),
+ %% We don't care about the order. The test was made to fit
+ %% whatever the functions returned.
+ {"Someone",
+ _Contact, _Info, shadiness,
+ [{to, "Don Corleone", {1949,12,25}, "Gave a Christmas gift"},
+ {from, "Don Corleone", {1949,2,14}, "Increased business"}]} =
+ mafiapp:friend_by_name("Someone").
+
+%% It should be possible to find all people who owe us things.
+accounts(_Config) ->
+ ok = mafiapp:add_friend("Gill Bates", [{email, "ceo@macrohard.com"}],
+ [clever,rich], computers),
+ ok = mafiapp:add_service("Consigliere", "Gill Bates",
+ {1985,11,20}, "Bought 15 copies of software"),
+ ok = mafiapp:add_service("Gill Bates", "Consigliere",
+ {1986,8,17}, "Made computer faster"),
+ ok = mafiapp:add_friend("Pierre Gauthier", [{other, "city arena"}],
+ [{job, "sports team GM"}], sports),
+ ok = mafiapp:add_service("Pierre Gauthier", "Consigliere", {2009,6,30},
+ "Took on a huge, bad contract"),
+ ok = mafiapp:add_friend("Wayne Gretzky", [{other, "Canada"}],
+ [{born, {1961,1,26}}, "hockey legend"],
+ hockey),
+ ok = mafiapp:add_service("Consigliere", "Wayne Gretzky", {1964,1,26},
+ "Gave first pair of ice skates"),
+ %% Wayne Gretzky owes us something so the debt is negative
+ %% Gill Bates are equal
+ %% Gauthier is owed a service.
+ [{-1,"Wayne Gretzky"},
+ {0,"Gill Bates"},
+ {1,"Pierre Gauthier"}] = mafiapp:debts("Consigliere"),
+ [{1, "Consigliere"}] = mafiapp:debts("Wayne Gretzky").
+
+enemies(_Config) ->
+ undefined = mafiapp:find_enemy("Edward"),
+ ok = mafiapp:add_enemy("Edward", [{bio, "Vampire"},
+ {comment, "He sucks (blood)"}]),
+ {"Edward", [{bio, "Vampire"},
+ {comment, "He sucks (blood)"}]} =
+ mafiapp:find_enemy("Edward"),
+ ok = mafiapp:enemy_killed("Edward"),
+ undefined = mafiapp:find_enemy("Edward").
+
diff --git a/learn-you-some-erlang/mafiapp-1.0.1/Emakefile b/learn-you-some-erlang/mafiapp-1.0.1/Emakefile
new file mode 100644
index 0000000..b203ea3
--- /dev/null
+++ b/learn-you-some-erlang/mafiapp-1.0.1/Emakefile
@@ -0,0 +1,2 @@
+{["src/*", "test/*"],
+ [{i,"include"}, {outdir, "ebin"}]}.
diff --git a/learn-you-some-erlang/mafiapp-1.0.1/ebin/mafiapp.app b/learn-you-some-erlang/mafiapp-1.0.1/ebin/mafiapp.app
new file mode 100644
index 0000000..f4268ae
--- /dev/null
+++ b/learn-you-some-erlang/mafiapp-1.0.1/ebin/mafiapp.app
@@ -0,0 +1,5 @@
+{application, mafiapp,
+ [{description, "Help the boss keep track of his friends"},
+ {vsn, "1.0.1"},
+ {modules, [mafiapp, mafiapp_sup]},
+ {applications, [stdlib, kernel, mnesia]}]}.
diff --git a/learn-you-some-erlang/mafiapp-1.0.1/logs/.please-track-this b/learn-you-some-erlang/mafiapp-1.0.1/logs/.please-track-this
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/learn-you-some-erlang/mafiapp-1.0.1/logs/.please-track-this
diff --git a/learn-you-some-erlang/mafiapp-1.0.1/mafiapp.spec b/learn-you-some-erlang/mafiapp-1.0.1/mafiapp.spec
new file mode 100644
index 0000000..4953e67
--- /dev/null
+++ b/learn-you-some-erlang/mafiapp-1.0.1/mafiapp.spec
@@ -0,0 +1,3 @@
+{alias, root, "./test/"}.
+{logdir, "./logs/"}.
+{suites, root, all}.
diff --git a/learn-you-some-erlang/mafiapp-1.0.1/src/mafiapp.erl b/learn-you-some-erlang/mafiapp-1.0.1/src/mafiapp.erl
new file mode 100644
index 0000000..f40ebce
--- /dev/null
+++ b/learn-you-some-erlang/mafiapp-1.0.1/src/mafiapp.erl
@@ -0,0 +1,142 @@
+-module(mafiapp).
+-behaviour(application).
+-include_lib("stdlib/include/ms_transform.hrl").
+-include_lib("stdlib/include/qlc.hrl").
+-export([start/2, stop/1]).
+-export([install/1]).
+-export([add_friend/4, friend_by_name/1, friend_by_expertise/1,
+ add_service/4, debts/1]).
+-export([add_enemy/2, find_enemy/1, enemy_killed/1]).
+
+
+-record(mafiapp_friends, {name,
+ contact=[],
+ info=[],
+ expertise}).
+-record(mafiapp_services, {from,
+ to,
+ date,
+ description}).
+-record(mafiapp_enemies, {name,
+ info=[]}).
+
+start(normal, []) ->
+ mnesia:wait_for_tables([mafiapp_friends,
+ mafiapp_services,
+ mafiapp_enemies], 5000),
+ mafiapp_sup:start_link().
+
+stop(_) -> ok.
+
+install(Nodes) ->
+ ok = mnesia:create_schema(Nodes),
+ rpc:multicall(Nodes, application, start, [mnesia]),
+ mnesia:create_table(mafiapp_friends,
+ [{attributes, record_info(fields, mafiapp_friends)},
+ {index, [#mafiapp_friends.expertise]},
+ {disc_copies, Nodes}]),
+ mnesia:create_table(mafiapp_services,
+ [{attributes, record_info(fields, mafiapp_services)},
+ {index, [#mafiapp_services.to]},
+ {disc_copies, Nodes},
+ {type, bag}]),
+ mnesia:create_table(mafiapp_enemies,
+ [{attributes, record_info(fields, mafiapp_enemies)},
+ {disc_copies, Nodes},
+ {local_content, true}]),
+ rpc:multicall(Nodes, application, stop, [mnesia]).
+
+add_friend(Name, Contact, Info, Expertise) ->
+ F = fun() ->
+ mnesia:write(#mafiapp_friends{name=Name,
+ contact=Contact,
+ info=Info,
+ expertise=Expertise})
+ end,
+ mnesia:activity(transaction, F).
+
+friend_by_name(Name) ->
+ F = fun() ->
+ case mnesia:read({mafiapp_friends, Name}) of
+ [#mafiapp_friends{contact=C, info=I, expertise=E}] ->
+ {Name,C,I,E,find_services(Name)};
+ [] ->
+ undefined
+ end
+ end,
+ mnesia:activity(transaction, F).
+
+friend_by_expertise(Expertise) ->
+ F = fun() ->
+ qlc:eval(qlc:q(
+ [{Name,C,I,E,find_services(Name)} ||
+ #mafiapp_friends{name=Name,
+ contact=C,
+ info=I,
+ expertise=E} <- mnesia:table(mafiapp_friends),
+ E =:= Expertise]))
+ end,
+ mnesia:activity(transaction, F).
+
+%% Adding validation is left to the reader
+add_service(From, To, Date, Description) ->
+ F = fun() ->
+ case mnesia:read({mafiapp_friends, From}) =:= [] orelse
+ mnesia:read({mafiapp_friends, To}) =:= [] of
+ true ->
+ {error, unknown_friend};
+ false ->
+ mnesia:write(#mafiapp_services{from=From,
+ to=To,
+ date=Date,
+ description=Description})
+ end
+ end,
+ mnesia:activity(transaction,F).
+
+debts(Name) ->
+ F = fun() ->
+ QH = qlc:q(
+ [if Name =:= To -> {From,1};
+ Name =:= From -> {To,-1}
+ end || #mafiapp_services{from=From, to=To} <-
+ mnesia:table(mafiapp_services),
+ Name =:= To orelse Name =:= From]),
+ qlc:fold(fun({Person,N}, Dict) ->
+ dict:update(Person, fun(X) -> X + N end, N, Dict)
+ end,
+ dict:new(),
+ QH)
+ end,
+ lists:sort([{V,K} || {K,V} <- dict:to_list(mnesia:activity(transaction, F))]).
+
+add_enemy(Name, Info) ->
+ F = fun() -> mnesia:write(#mafiapp_enemies{name=Name, info=Info}) end,
+ mnesia:activity(transaction, F).
+
+find_enemy(Name) ->
+ F = fun() -> mnesia:read({mafiapp_enemies, Name}) end,
+ case mnesia:activity(transaction, F) of
+ [] -> undefined;
+ [#mafiapp_enemies{name=N, info=I}] -> {N,I}
+ end.
+
+enemy_killed(Name) ->
+ F = fun() -> mnesia:delete({mafiapp_enemies, Name}) end,
+ mnesia:activity(transaction, F).
+
+%%%%%%%%%%%%%%%
+%%% PRIVATE %%%
+%%%%%%%%%%%%%%%
+
+find_services(Name) ->
+ Match = ets:fun2ms(
+ fun(#mafiapp_services{from=From, to=To, date=D, description=Desc})
+ when From =:= Name ->
+ {to, To, D, Desc};
+ (#mafiapp_services{from=From, to=To, date=D, description=Desc})
+ when To =:= Name ->
+ {from, From, D, Desc}
+ end
+ ),
+ mnesia:select(mafiapp_services, Match).
diff --git a/learn-you-some-erlang/mafiapp-1.0.1/src/mafiapp_sup.erl b/learn-you-some-erlang/mafiapp-1.0.1/src/mafiapp_sup.erl
new file mode 100644
index 0000000..e31b221
--- /dev/null
+++ b/learn-you-some-erlang/mafiapp-1.0.1/src/mafiapp_sup.erl
@@ -0,0 +1,12 @@
+-module(mafiapp_sup).
+-behaviour(supervisor).
+-export([start_link/0]).
+-export([init/1]).
+
+start_link() ->
+ supervisor:start_link(?MODULE, []).
+
+%% This does absolutely nothing, only there to
+%% allow to wait for tables.
+init([]) ->
+ {ok, {{one_for_one, 1, 1}, []}}.
diff --git a/learn-you-some-erlang/mafiapp-1.0.1/test/mafiapp.spec b/learn-you-some-erlang/mafiapp-1.0.1/test/mafiapp.spec
new file mode 100644
index 0000000..4953e67
--- /dev/null
+++ b/learn-you-some-erlang/mafiapp-1.0.1/test/mafiapp.spec
@@ -0,0 +1,3 @@
+{alias, root, "./test/"}.
+{logdir, "./logs/"}.
+{suites, root, all}.
diff --git a/learn-you-some-erlang/mafiapp-1.0.1/test/mafiapp_SUITE.erl b/learn-you-some-erlang/mafiapp-1.0.1/test/mafiapp_SUITE.erl
new file mode 100644
index 0000000..6005d29
--- /dev/null
+++ b/learn-you-some-erlang/mafiapp-1.0.1/test/mafiapp_SUITE.erl
@@ -0,0 +1,128 @@
+-module(mafiapp_SUITE).
+-include_lib("common_test/include/ct.hrl").
+-export([init_per_suite/1, end_per_suite/1,
+ init_per_testcase/2, end_per_testcase/2,
+ all/0]).
+-export([add_service/1, friend_by_name/1, friend_by_expertise/1,
+ friend_with_services/1, accounts/1, enemies/1]).
+
+all() -> [add_service, friend_by_name, friend_by_expertise,
+ friend_with_services, accounts, enemies].
+
+init_per_suite(Config) ->
+ Priv = ?config(priv_dir, Config),
+ application:load(mnesia),
+ application:set_env(mnesia, dir, Priv),
+ application:load(mafiapp),
+ mafiapp:install([node()]),
+ application:start(mnesia),
+ application:start(mafiapp),
+ Config.
+
+end_per_suite(_Config) ->
+ application:stop(mnesia),
+ ok.
+
+init_per_testcase(add_service, Config) ->
+ Config;
+init_per_testcase(accounts, Config) ->
+ ok = mafiapp:add_friend("Consigliere", [], [you], consigliere),
+ Config;
+init_per_testcase(_, Config) ->
+ ok = mafiapp:add_friend("Don Corleone", [], [boss], boss),
+ Config.
+
+end_per_testcase(_, _Config) ->
+ ok.
+
+%% services can go both way: from a friend to the boss, or
+%% from the boss to a friend! A boss friend is required!
+add_service(_Config) ->
+ {error, unknown_friend} = mafiapp:add_service("from name",
+ "to name",
+ {1946,5,23},
+ "a fake service"),
+ ok = mafiapp:add_friend("Don Corleone", [], [boss], boss),
+ ok = mafiapp:add_friend("Alan Parsons",
+ [{twitter,"@ArtScienceSound"}],
+ [{born, {1948,12,20}},
+ musician, 'audio engineer',
+ producer, "has projects"],
+ mixing),
+ ok = mafiapp:add_service("Alan Parsons", "Don Corleone",
+ {1973,3,1},
+ "Helped release a Pink Floyd album").
+
+friend_by_name(_Config) ->
+ ok = mafiapp:add_friend("Pete Cityshend",
+ [{phone, "418-542-3000"},
+ {email, "quadrophonia@example.org"},
+ {other, "yell real loud"}],
+ [{born, {1945,5,19}},
+ musician, popular],
+ music),
+ {"Pete Cityshend",
+ _Contact, _Info, music,
+ _Services} = mafiapp:friend_by_name("Pete Cityshend"),
+ undefined = mafiapp:friend_by_name(make_ref()).
+
+friend_by_expertise(_Config) ->
+ ok = mafiapp:add_friend("A Red Panda",
+ [{location, "in a zoo"}],
+ [animal,cute],
+ climbing),
+ [{"A Red Panda",
+ _Contact, _Info, climbing,
+ _Services}] = mafiapp:friend_by_expertise(climbing),
+ [] = mafiapp:friend_by_expertise(make_ref()).
+
+friend_with_services(_Config) ->
+ ok = mafiapp:add_friend("Someone", [{other, "at the fruit stand"}],
+ [weird, mysterious], shadiness),
+ ok = mafiapp:add_service("Don Corleone", "Someone",
+ {1949,2,14}, "Increased business"),
+ ok = mafiapp:add_service("Someone", "Don Corleone",
+ {1949,12,25}, "Gave a Christmas gift"),
+ %% We don't care about the order. The test was made to fit
+ %% whatever the functions returned.
+ {"Someone",
+ _Contact, _Info, shadiness,
+ [{to, "Don Corleone", {1949,12,25}, "Gave a Christmas gift"},
+ {from, "Don Corleone", {1949,2,14}, "Increased business"}]} =
+ mafiapp:friend_by_name("Someone").
+
+%% It should be possible to find all people who owe us things.
+accounts(_Config) ->
+ ok = mafiapp:add_friend("Gill Bates", [{email, "ceo@macrohard.com"}],
+ [clever,rich], computers),
+ ok = mafiapp:add_service("Consigliere", "Gill Bates",
+ {1985,11,20}, "Bought 15 copies of software"),
+ ok = mafiapp:add_service("Gill Bates", "Consigliere",
+ {1986,8,17}, "Made computer faster"),
+ ok = mafiapp:add_friend("Pierre Gauthier", [{other, "city arena"}],
+ [{job, "sports team GM"}], sports),
+ ok = mafiapp:add_service("Pierre Gauthier", "Consigliere", {2009,6,30},
+ "Took on a huge, bad contract"),
+ ok = mafiapp:add_friend("Wayne Gretzky", [{other, "Canada"}],
+ [{born, {1961,1,26}}, "hockey legend"],
+ hockey),
+ ok = mafiapp:add_service("Consigliere", "Wayne Gretzky", {1964,1,26},
+ "Gave first pair of ice skates"),
+ %% Wayne Gretzky owes us something so the debt is negative
+ %% Gill Bates are equal
+ %% Gauthier is owed a service.
+ [{-1,"Wayne Gretzky"},
+ {0,"Gill Bates"},
+ {1,"Pierre Gauthier"}] = mafiapp:debts("Consigliere"),
+ [{1, "Consigliere"}] = mafiapp:debts("Wayne Gretzky").
+
+enemies(_Config) ->
+ undefined = mafiapp:find_enemy("Edward"),
+ ok = mafiapp:add_enemy("Edward", [{bio, "Vampire"},
+ {comment, "He sucks (blood)"}]),
+ {"Edward", [{bio, "Vampire"},
+ {comment, "He sucks (blood)"}]} =
+ mafiapp:find_enemy("Edward"),
+ ok = mafiapp:enemy_killed("Edward"),
+ undefined = mafiapp:find_enemy("Edward").
+
diff --git a/learn-you-some-erlang/multiproc.erl b/learn-you-some-erlang/multiproc.erl
new file mode 100644
index 0000000..abddaf9
--- /dev/null
+++ b/learn-you-some-erlang/multiproc.erl
@@ -0,0 +1,39 @@
+-module(multiproc).
+-compile([export_all]).
+
+sleep(T) ->
+ receive
+ after T -> ok
+ end.
+
+flush() ->
+ receive
+ _ -> flush()
+ after 0 ->
+ ok
+ end.
+
+important() ->
+ receive
+ {Priority, Message} when Priority > 10 ->
+ [Message | important()]
+ after 0 ->
+ normal()
+ end.
+
+normal() ->
+ receive
+ {_, Message} ->
+ [Message | normal()]
+ after 0 ->
+ []
+ end.
+
+%% optimized in R14A
+optimized(Pid) ->
+ Ref = make_ref(),
+ Pid ! {self(), Ref, hello},
+ receive
+ {Pid, Ref, Msg} ->
+ io:format("~p~n", [Msg])
+ end.
diff --git a/learn-you-some-erlang/musicians.erl b/learn-you-some-erlang/musicians.erl
new file mode 100644
index 0000000..e369d44
--- /dev/null
+++ b/learn-you-some-erlang/musicians.erl
@@ -0,0 +1,84 @@
+-module(musicians).
+-behaviour(gen_server).
+
+-export([start_link/2, stop/1]).
+-export([init/1, handle_call/3, handle_cast/2,
+ handle_info/2, code_change/3, terminate/2]).
+
+-record(state, {name="", role, skill=good}).
+-define(DELAY, 750).
+
+start_link(Role, Skill) ->
+ gen_server:start_link({local, Role}, ?MODULE, [Role, Skill], []).
+
+stop(Role) -> gen_server:call(Role, stop).
+
+init([Role, Skill]) ->
+ %% To know when the parent shuts down
+ process_flag(trap_exit, true),
+ %% sets a seed for random number generation for the life of the process
+ %% uses the current time to do it. Unique value guaranteed by now()
+ random:seed(now()),
+ TimeToPlay = random:uniform(3000),
+ Name = pick_name(),
+ StrRole = atom_to_list(Role),
+ io:format("Musician ~s, playing the ~s entered the room~n",
+ [Name, StrRole]),
+ {ok, #state{name=Name, role=StrRole, skill=Skill}, TimeToPlay}.
+
+handle_call(stop, _From, S=#state{}) ->
+ {stop, normal, ok, S};
+handle_call(_Message, _From, S) ->
+ {noreply, S, ?DELAY}.
+
+handle_cast(_Message, S) ->
+ {noreply, S, ?DELAY}.
+
+handle_info(timeout, S = #state{name=N, skill=good}) ->
+ io:format("~s produced sound!~n",[N]),
+ {noreply, S, ?DELAY};
+handle_info(timeout, S = #state{name=N, skill=bad}) ->
+ case random:uniform(5) of
+ 1 ->
+ io:format("~s played a false note. Uh oh~n",[N]),
+ {stop, bad_note, S};
+ _ ->
+ io:format("~s produced sound!~n",[N]),
+ {noreply, S, ?DELAY}
+ end;
+handle_info(_Message, S) ->
+ {noreply, S, ?DELAY}.
+
+code_change(_OldVsn, State, _Extra) ->
+ {ok, State}.
+
+terminate(normal, S) ->
+ io:format("~s left the room (~s)~n",[S#state.name, S#state.role]);
+terminate(bad_note, S) ->
+ io:format("~s sucks! kicked that member out of the band! (~s)~n",
+ [S#state.name, S#state.role]);
+terminate(shutdown, S) ->
+ io:format("The manager is mad and fired the whole band! "
+ "~s just got back to playing in the subway~n",
+ [S#state.name]);
+terminate(_Reason, S) ->
+ io:format("~s has been kicked out (~s)~n", [S#state.name, S#state.role]).
+
+%% Yes, the names are based off the magic school bus characters
+%% 10 names!
+pick_name() ->
+ %% the seed must be set for the random functions. Use within the
+ %% process that started with init/1
+ lists:nth(random:uniform(10), firstnames())
+ ++ " " ++
+ lists:nth(random:uniform(10), lastnames()).
+
+firstnames() ->
+ ["Valerie", "Arnold", "Carlos", "Dorothy", "Keesha",
+ "Phoebe", "Ralphie", "Tim", "Wanda", "Janet"].
+
+lastnames() ->
+ ["Frizzle", "Perlstein", "Ramon", "Ann", "Franklin",
+ "Terese", "Tennelli", "Jamal", "Li", "Perlstein"].
+
+
diff --git a/learn-you-some-erlang/my_server.erl b/learn-you-some-erlang/my_server.erl
new file mode 100644
index 0000000..ed83a7a
--- /dev/null
+++ b/learn-you-some-erlang/my_server.erl
@@ -0,0 +1,42 @@
+-module(my_server).
+-export([start/2, start_link/2, call/2, cast/2, reply/2]).
+
+%%% Public API
+start(Module, InitialState) ->
+ spawn(fun() -> init(Module, InitialState) end).
+
+start_link(Module, InitialState) ->
+ spawn_link(fun() -> init(Module, InitialState) end).
+
+call(Pid, Msg) ->
+ Ref = erlang:monitor(process, Pid),
+ Pid ! {sync, self(), Ref, Msg},
+ receive
+ {Ref, Reply} ->
+ erlang:demonitor(Ref, [flush]),
+ Reply;
+ {'DOWN', Ref, process, Pid, Reason} ->
+ erlang:error(Reason)
+ after 5000 ->
+ erlang:error(timeout)
+ end.
+
+cast(Pid, Msg) ->
+ Pid ! {async, Msg},
+ ok.
+
+reply({Pid, Ref}, Reply) ->
+ Pid ! {Ref, Reply}.
+
+%%% Private stuff
+init(Module, InitialState) ->
+ loop(Module, Module:init(InitialState)).
+
+loop(Module, State) ->
+ receive
+ {async, Msg} ->
+ loop(Module, Module:handle_cast(Msg, State));
+ {sync, Pid, Ref, Msg} ->
+ loop(Module, Module:handle_call(Msg, {Pid, Ref}, State))
+ end.
+
diff --git a/learn-you-some-erlang/oop.erl b/learn-you-some-erlang/oop.erl
new file mode 100644
index 0000000..19653d3
--- /dev/null
+++ b/learn-you-some-erlang/oop.erl
@@ -0,0 +1,28 @@
+-module(oop).
+-export([animal/1, dog/1, cat/1]).
+
+%% all the method calls need to be in tuples when they have more than
+%% one argument so we can use functions of arity 1 for every call we make.
+
+animal(Name) ->
+ fun(type) -> "living thing";
+ (name) -> Name;
+ (move) -> Name++" moves around...";
+ ({eat, Item}) -> Name++" eats "++Item;
+ (_) -> "I'm sorry Dave, I can't do that."
+ end.
+
+dog(Name) ->
+ Parent = animal(Name),
+ fun(talk) -> Name++" says: Woof!";
+ ({chase, Animal}) when is_function(Animal) ->
+ Name++" chases a "++Animal(type)++" named "++Animal(name)++" around";
+ (X) -> Parent(X)
+ end.
+
+cat(Name) ->
+ Parent = animal(Name),
+ fun(type) -> "cat";
+ (talk) -> Name++" says: Meow!";
+ (X) -> Parent(X)
+ end.
diff --git a/learn-you-some-erlang/ops.erl b/learn-you-some-erlang/ops.erl
new file mode 100644
index 0000000..ccd95cd
--- /dev/null
+++ b/learn-you-some-erlang/ops.erl
@@ -0,0 +1,4 @@
+-module(ops).
+-export([add/2]).
+
+add(A,B) -> A + B.
diff --git a/learn-you-some-erlang/ops_tests.erl b/learn-you-some-erlang/ops_tests.erl
new file mode 100644
index 0000000..53323cf
--- /dev/null
+++ b/learn-you-some-erlang/ops_tests.erl
@@ -0,0 +1,25 @@
+-module(ops_tests).
+-include_lib("eunit/include/eunit.hrl").
+
+add_test() ->
+ 4 = ops:add(2,2).
+
+new_add_test() ->
+ ?assertEqual(4, ops:add(2,2)),
+ ?assertEqual(3, ops:add(1,2)),
+ ?assert(is_number(ops:add(1,2))),
+ ?assertEqual(3, ops:add(1,1)),
+ ?assertError(badarith, 1/0).
+
+add_test_() ->
+ [test_them_types(),
+ test_them_values(),
+ ?_assertError(badarith, 1/0)].
+
+test_them_types() ->
+ ?_assert(is_number(ops:add(1,2))).
+
+test_them_values() ->
+ [?_assertEqual(4, ops:add(2,2)),
+ ?_assertEqual(3, ops:add(1,2)),
+ ?_assertEqual(3, ops:add(1,1))].
diff --git a/learn-you-some-erlang/pool/ppool.erl b/learn-you-some-erlang/pool/ppool.erl
new file mode 100644
index 0000000..b5d34c0
--- /dev/null
+++ b/learn-you-some-erlang/pool/ppool.erl
@@ -0,0 +1,25 @@
+%%% API module for the pool
+-module(ppool).
+-export([start_link/0, stop/0, start_pool/3,
+ run/2, sync_queue/2, async_queue/2, stop_pool/1]).
+
+start_link() ->
+ ppool_supersup:start_link().
+
+stop() ->
+ ppool_supersup:stop().
+
+start_pool(Name, Limit, {M,F,A}) ->
+ ppool_supersup:start_pool(Name, Limit, {M,F,A}).
+
+stop_pool(Name) ->
+ ppool_supersup:stop_pool(Name).
+
+run(Name, Args) ->
+ ppool_serv:run(Name, Args).
+
+async_queue(Name, Args) ->
+ ppool_serv:async_queue(Name, Args).
+
+sync_queue(Name, Args) ->
+ ppool_serv:sync_queue(Name, Args).
diff --git a/learn-you-some-erlang/pool/ppool_nagger.erl b/learn-you-some-erlang/pool/ppool_nagger.erl
new file mode 100644
index 0000000..903f821
--- /dev/null
+++ b/learn-you-some-erlang/pool/ppool_nagger.erl
@@ -0,0 +1,50 @@
+%% demo module, a nagger for tasks,
+%% because the previous one wasn't good enough
+%%
+%% Can take:
+%% - a time delay for which to nag,
+%% - an adress to say where the messages should be sent
+%% - a message to send in the mailbox telling you what to nag,
+%% with an id to be able to call: ->
+%% - a command to say the task is done
+-module(ppool_nagger).
+-behaviour(gen_server).
+-export([start_link/4, stop/1]).
+-export([init/1, handle_call/3, handle_cast/2,
+ handle_info/2, code_change/3, terminate/2]).
+
+start_link(Task, Delay, Max, SendTo) ->
+ gen_server:start_link(?MODULE, {Task, Delay, Max, SendTo} , []).
+
+stop(Pid) ->
+ gen_server:call(Pid, stop).
+
+init({Task, Delay, Max, SendTo}) ->
+ process_flag(trap_exit, true), % for tests & terminate too
+ {ok, {Task, Delay, Max, SendTo}, Delay}.
+
+%%% OTP Callbacks
+handle_call(stop, _From, State) ->
+ {stop, normal, ok, State};
+handle_call(_Msg, _From, State) ->
+ {noreply, State}.
+
+handle_cast(_Msg, State) ->
+ {noreply, State}.
+
+handle_info(timeout, {Task, Delay, Max, SendTo}) ->
+ SendTo ! {self(), Task},
+ if Max =:= infinity ->
+ {noreply, {Task, Delay, Max, SendTo}, Delay};
+ Max =< 1 ->
+ {stop, normal, {Task, Delay, 0, SendTo}};
+ Max > 1 ->
+ {noreply, {Task, Delay, Max-1, SendTo}, Delay}
+ end;
+handle_info(_Msg, State) ->
+ {noreply, State}.
+
+code_change(_OldVsn, State, _Extra) ->
+ {ok, State}.
+
+terminate(_Reason, _State) -> ok.
diff --git a/learn-you-some-erlang/pool/ppool_serv.erl b/learn-you-some-erlang/pool/ppool_serv.erl
new file mode 100644
index 0000000..512b49f
--- /dev/null
+++ b/learn-you-some-erlang/pool/ppool_serv.erl
@@ -0,0 +1,113 @@
+-module(ppool_serv).
+-behaviour(gen_server).
+-export([start/4, start_link/4, run/2, sync_queue/2, async_queue/2, stop/1]).
+-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
+ code_change/3, terminate/2]).
+
+%% The friendly supervisor is started dynamically!
+-define(SPEC(MFA),
+ {worker_sup,
+ {ppool_worker_sup, start_link, [MFA]},
+ temporary,
+ 10000,
+ supervisor,
+ [ppool_worker_sup]}).
+
+-record(state, {limit=0,
+ sup,
+ refs,
+ queue=queue:new()}).
+
+start(Name, Limit, Sup, MFA) when is_atom(Name), is_integer(Limit) ->
+ gen_server:start({local, Name}, ?MODULE, {Limit, MFA, Sup}, []).
+
+start_link(Name, Limit, Sup, MFA) when is_atom(Name), is_integer(Limit) ->
+ gen_server:start_link({local, Name}, ?MODULE, {Limit, MFA, Sup}, []).
+
+run(Name, Args) ->
+ gen_server:call(Name, {run, Args}).
+
+sync_queue(Name, Args) ->
+ gen_server:call(Name, {sync, Args}, infinity).
+
+async_queue(Name, Args) ->
+ gen_server:cast(Name, {async, Args}).
+
+stop(Name) ->
+ gen_server:call(Name, stop).
+
+%% Gen server
+init({Limit, MFA, Sup}) ->
+ %% We need to find the Pid of the worker supervisor from here,
+ %% but alas, this would be calling the supervisor while it waits for us!
+ self() ! {start_worker_supervisor, Sup, MFA},
+ {ok, #state{limit=Limit, refs=gb_sets:empty()}}.
+
+handle_call({run, Args}, _From, S = #state{limit=N, sup=Sup, refs=R}) when N > 0 ->
+ {ok, Pid} = supervisor:start_child(Sup, Args),
+ Ref = erlang:monitor(process, Pid),
+ {reply, {ok,Pid}, S#state{limit=N-1, refs=gb_sets:add(Ref,R)}};
+handle_call({run, _Args}, _From, S=#state{limit=N}) when N =< 0 ->
+ {reply, noalloc, S};
+
+handle_call({sync, Args}, _From, S = #state{limit=N, sup=Sup, refs=R}) when N > 0 ->
+ {ok, Pid} = supervisor:start_child(Sup, Args),
+ Ref = erlang:monitor(process, Pid),
+ {reply, {ok,Pid}, S#state{limit=N-1, refs=gb_sets:add(Ref,R)}};
+handle_call({sync, Args}, From, S = #state{queue=Q}) ->
+ {noreply, S#state{queue=queue:in({From, Args}, Q)}};
+
+handle_call(stop, _From, State) ->
+ {stop, normal, ok, State};
+handle_call(_Msg, _From, State) ->
+ {noreply, State}.
+
+
+handle_cast({async, Args}, S=#state{limit=N, sup=Sup, refs=R}) when N > 0 ->
+ {ok, Pid} = supervisor:start_child(Sup, Args),
+ Ref = erlang:monitor(process, Pid),
+ {noreply, S#state{limit=N-1, refs=gb_sets:add(Ref,R)}};
+handle_cast({async, Args}, S=#state{limit=N, queue=Q}) when N =< 0 ->
+ {noreply, S#state{queue=queue:in(Args,Q)}};
+
+handle_cast(_Msg, State) ->
+ {noreply, State}.
+
+handle_info({'DOWN', Ref, process, _Pid, _}, S = #state{refs=Refs}) ->
+ io:format("received down msg~n"),
+ case gb_sets:is_element(Ref, Refs) of
+ true ->
+ handle_down_worker(Ref, S);
+ false -> %% Not our responsibility
+ {noreply, S}
+ end;
+handle_info({start_worker_supervisor, Sup, MFA}, S = #state{}) ->
+ {ok, Pid} = supervisor:start_child(Sup, ?SPEC(MFA)),
+ link(Pid),
+ {noreply, S#state{sup=Pid}};
+handle_info(Msg, State) ->
+ io:format("Unknown msg: ~p~n", [Msg]),
+ {noreply, State}.
+
+code_change(_OldVsn, State, _Extra) ->
+ {ok, State}.
+
+terminate(_Reason, _State) ->
+ ok.
+
+handle_down_worker(Ref, S = #state{limit=L, sup=Sup, refs=Refs}) ->
+ case queue:out(S#state.queue) of
+ {{value, {From, Args}}, Q} ->
+ {ok, Pid} = supervisor:start_child(Sup, Args),
+ NewRef = erlang:monitor(process, Pid),
+ NewRefs = gb_sets:insert(NewRef, gb_sets:delete(Ref,Refs)),
+ gen_server:reply(From, {ok, Pid}),
+ {noreply, S#state{refs=NewRefs, queue=Q}};
+ {{value, Args}, Q} ->
+ {ok, Pid} = supervisor:start_child(Sup, Args),
+ NewRef = erlang:monitor(process, Pid),
+ NewRefs = gb_sets:insert(NewRef, gb_sets:delete(Ref,Refs)),
+ {noreply, S#state{refs=NewRefs, queue=Q}};
+ {empty, _} ->
+ {noreply, S#state{limit=L+1, refs=gb_sets:delete(Ref,Refs)}}
+ end.
diff --git a/learn-you-some-erlang/pool/ppool_sup.erl b/learn-you-some-erlang/pool/ppool_sup.erl
new file mode 100644
index 0000000..71ec31d
--- /dev/null
+++ b/learn-you-some-erlang/pool/ppool_sup.erl
@@ -0,0 +1,17 @@
+-module(ppool_sup).
+-export([start_link/3, init/1]).
+-behaviour(supervisor).
+
+start_link(Name, Limit, MFA) ->
+ supervisor:start_link(?MODULE, {Name, Limit, MFA}).
+
+init({Name, Limit, MFA}) ->
+ MaxRestart = 1,
+ MaxTime = 3000,
+ {ok, {{one_for_all, MaxRestart, MaxTime},
+ [{serv,
+ {ppool_serv, start_link, [Name, Limit, self(), MFA]},
+ permanent,
+ 5000,
+ worker,
+ [ppool_serv]}]}}.
diff --git a/learn-you-some-erlang/pool/ppool_supersup.erl b/learn-you-some-erlang/pool/ppool_supersup.erl
new file mode 100644
index 0000000..5790c5f
--- /dev/null
+++ b/learn-you-some-erlang/pool/ppool_supersup.erl
@@ -0,0 +1,31 @@
+-module(ppool_supersup).
+-behaviour(supervisor).
+-export([start_link/0, stop/0, start_pool/3, stop_pool/1]).
+-export([init/1]).
+
+start_link() ->
+ supervisor:start_link({local, ppool}, ?MODULE, []).
+
+%% technically, a supervisor can not be killed in an easy way.
+%% Let's do it brutally!
+stop() ->
+ case whereis(ppool) of
+ P when is_pid(P) ->
+ exit(P, kill);
+ _ -> ok
+ end.
+
+start_pool(Name, Limit, MFA) ->
+ ChildSpec = {Name,
+ {ppool_sup, start_link, [Name, Limit, MFA]},
+ permanent, 10500, supervisor, [ppool_sup]},
+ supervisor:start_child(ppool, ChildSpec).
+
+stop_pool(Name) ->
+ supervisor:terminate_child(ppool, Name),
+ supervisor:delete_child(ppool, Name).
+
+init([]) ->
+ MaxRestart = 6,
+ MaxTime = 3000,
+ {ok, {{one_for_one, MaxRestart, MaxTime}, []}}.
diff --git a/learn-you-some-erlang/pool/ppool_tests.erl b/learn-you-some-erlang/pool/ppool_tests.erl
new file mode 100644
index 0000000..7cec6d0
--- /dev/null
+++ b/learn-you-some-erlang/pool/ppool_tests.erl
@@ -0,0 +1,200 @@
+-module(ppool_tests).
+-include_lib("eunit/include/eunit.hrl").
+-export([test_mfa/1, wait_mfa/1]).
+
+%%% All Test Fixtures
+start_test_() ->
+ {"It should be possible to start a pool server and give it a name",
+ {setup,
+ fun find_unique_name/0,
+ fun(Name) ->
+ [start_and_test_name(Name)]
+ end}}.
+
+mfa_test_() ->
+ {"A pool process can be allocated which will be ordered "
+ "to run an MFA call determined at start time, with arguments "
+ "provided at call time",
+ {setup,
+ fun start_ppool/0,
+ fun kill_ppool/1,
+ fun(Name) ->
+ [pool_run_mfa(Name)]
+ end}
+ }.
+
+alloc_test_() ->
+ {"A pool process can be allocated which will be ordered "
+ "to run a worker, only if there are enough which "
+ "haven't been ordered to run yet.",
+ {setup,
+ fun start_ppool/0,
+ fun kill_ppool/1,
+ fun(Name) ->
+ [pool_run_alloc(Name),
+ pool_run_noalloc(Name)]
+ end}
+ }.
+
+realloc_test_() ->
+ {"When an allocated process dies, "
+ "A new one can be allocated to replace it.",
+ {setup,
+ fun start_ppool/0,
+ fun kill_ppool/1,
+ fun(Name) ->
+ [pool_run_realloc(Name)]
+ end}
+ }.
+
+queue_test_() ->
+ {"The queue function can be used to run the function as soon as possible. "
+ "If no space is available, the worker call is added to the queue.",
+ {foreach,
+ fun start_ppool/0,
+ fun kill_ppool/1,
+ [fun(Name) -> test_async_queue(Name) end,
+ fun(Name) -> test_sync_queue(Name) end]}
+ }.
+
+supervision_test_() ->
+ {"The ppool will never restart a dead child, but all children (OTP "
+ "compliant) will be shut down when closing the pool, even if they "
+ "are trapping exits",
+ {setup,
+ fun find_unique_name/0,
+ fun test_supervision/1}}.
+
+auth_test_() ->
+ {"The ppool should only dequeue tasks after receiving a down signal "
+ "from a worker and nobody else",
+ {setup,
+ fun start_ppool/0,
+ fun kill_ppool/1,
+ fun test_auth_dealloc/1}}.
+
+%%% Setups/teardowns
+find_unique_name() ->
+ ppool:start_link(),
+ Name = list_to_atom(lists:flatten(io_lib:format("~p",[now()]))),
+ ?assertEqual(undefined, whereis(Name)),
+ Name.
+
+start_ppool() ->
+ Name = find_unique_name(),
+ ppool:start_pool(Name, 2, {ppool_nagger, start_link, []}),
+ Name.
+
+kill_ppool(Name) ->
+ ppool:stop_pool(Name).
+
+%%% Actual tests
+start_and_test_name(Name) ->
+ ppool:start_pool(Name, 1, {ppool_nagger, start_link, []}),
+ A = whereis(Name),
+ ppool:stop_pool(Name),
+ timer:sleep(100),
+ B = whereis(Name),
+ [?_assert(undefined =/= A),
+ ?_assertEqual(undefined, B)].
+
+pool_run_mfa(Name) ->
+ ppool:run(Name, [i_am_running, 1, 1, self()]),
+ X = receive
+ {_Pid, i_am_running} -> ok
+ after 3000 ->
+ timeout
+ end,
+ ?_assertEqual(ok, X).
+
+pool_run_alloc(Name) ->
+ {ok, Pid} = ppool:run(Name, [i_am_running, 1, 1, self()]),
+ X = receive
+ {Pid, i_am_running} -> ok
+ after 3000 ->
+ timeout
+ end,
+ [?_assert(is_pid(Pid)),
+ ?_assertEqual(ok, X)].
+
+pool_run_noalloc(Name) ->
+ %% Init function should have set the limit to 2
+ ppool:run(Name, [i_am_running, 300, 1, self()]),
+ ppool:run(Name, [i_am_running, 300, 1, self()]),
+ X = ppool:run(Name, [i_am_running, 1, 1, self()]),
+ ?_assertEqual(noalloc, X).
+
+pool_run_realloc(Name) ->
+ %% Init function should have set the limit to 2
+ {ok, A} = ppool:run(Name, [i_am_running, 500, 1, self()]),
+ timer:sleep(100),
+ {ok, B} = ppool:run(Name, [i_am_running, 500, 1, self()]),
+ timer:sleep(600),
+ {ok, Pid} = ppool:run(Name, [i_am_running, 1, 1, self()]),
+ timer:sleep(100),
+ L = flush(),
+ [?_assert(is_pid(Pid)),
+ ?_assertEqual([{A,i_am_running}, {B,i_am_running}, {Pid,i_am_running}],
+ L)].
+
+test_async_queue(Name) ->
+ %% Still two elements max!
+ ok = ppool:async_queue(Name, [i_am_running, 2000, 1, self()]),
+ ok = ppool:async_queue(Name, [i_am_running, 2000, 1, self()]),
+ noalloc = ppool:run(Name, [i_am_running, 2000, 1, self()]),
+ ok = ppool:async_queue(Name, [i_am_running, 500, 1, self()]),
+ timer:sleep(3500),
+ L = flush(),
+ ?_assertMatch([{_, i_am_running}, {_, i_am_running}, {_, i_am_running}], L).
+
+test_sync_queue(Name) ->
+ %% Hell yase, two max
+ {ok, Pid} = ppool:sync_queue(Name, [i_am_running, 200, 1, self()]),
+ ok = ppool:async_queue(Name, [i_am_running, 200, 1, self()]),
+ ok = ppool:async_queue(Name, [i_am_running, 200, 1, self()]),
+ {ok, Pid2} = ppool:sync_queue(Name, [i_am_running, 100, 1, self()]),
+ timer:sleep(300),
+ L = flush(),
+ [?_assert(is_pid(Pid)),
+ ?_assert(is_pid(Pid2)),
+ ?_assertMatch([{_,i_am_running}, {_,i_am_running},
+ {_,i_am_running}, {_,i_am_running}],
+ L)].
+
+test_supervision(Name) ->
+ ppool:start_pool(Name, 1, {ppool_nagger, start_link, []}),
+ {ok, Pid} = ppool:run(Name, [sup, 10000, 100, self()]),
+ ppool:stop_pool(Name),
+ timer:sleep(100),
+ ?_assertEqual(undefined, process_info(Pid)).
+
+test_auth_dealloc(Name) ->
+ %% Hell yase, two max
+ {ok, _Pid} = ppool:sync_queue(Name, [i_am_running, 500, 1, self()]),
+ ok = ppool:async_queue(Name, [i_am_running, 10000, 1, self()]),
+ ok = ppool:async_queue(Name, [i_am_running, 10000, 1, self()]),
+ ok = ppool:async_queue(Name, [i_am_running, 1, 1, self()]),
+ timer:sleep(600),
+ Name ! {'DOWN', make_ref(), process, self(), normal},
+ Name ! {'DOWN', make_ref(), process, self(), normal},
+ Name ! {'DOWN', make_ref(), process, self(), normal},
+ timer:sleep(200),
+ L = flush(),
+ ?_assertMatch([{_,i_am_running}], L).
+
+
+
+flush() ->
+ receive
+ X -> [X|flush()]
+ after 0 ->
+ []
+ end.
+
+%% Exported Helper functions
+test_mfa(Pid) ->
+ Pid ! i_am_running.
+
+wait_mfa(Pid) ->
+ Pid ! i_am_running,
+ timer:sleep(3000).
diff --git a/learn-you-some-erlang/pool/ppool_worker_sup.erl b/learn-you-some-erlang/pool/ppool_worker_sup.erl
new file mode 100644
index 0000000..2467c47
--- /dev/null
+++ b/learn-you-some-erlang/pool/ppool_worker_sup.erl
@@ -0,0 +1,15 @@
+-module(ppool_worker_sup).
+-export([start_link/1, init/1]).
+-behaviour(supervisor).
+
+start_link(MFA = {_,_,_}) ->
+ supervisor:start_link(?MODULE, MFA).
+
+init({M,F,A}) ->
+ MaxRestart = 5,
+ MaxTime = 3600,
+ {ok, {{simple_one_for_one, MaxRestart, MaxTime},
+ [{ppool_worker,
+ {M,F,A},
+ temporary, 5000, worker, [M]}]}}.
+
diff --git a/learn-you-some-erlang/ppool-1.0/Emakefile b/learn-you-some-erlang/ppool-1.0/Emakefile
new file mode 100644
index 0000000..8e1f951
--- /dev/null
+++ b/learn-you-some-erlang/ppool-1.0/Emakefile
@@ -0,0 +1,2 @@
+{"src/*", [debug_info, {i,"include/"}, {outdir, "ebin/"}]}.
+{"test/*", [debug_info, {i,"include/"}, {outdir, "ebin/"}]}.
diff --git a/learn-you-some-erlang/ppool-1.0/ebin/ppool.app b/learn-you-some-erlang/ppool-1.0/ebin/ppool.app
new file mode 100644
index 0000000..a791f54
--- /dev/null
+++ b/learn-you-some-erlang/ppool-1.0/ebin/ppool.app
@@ -0,0 +1,6 @@
+{application, ppool,
+ [{vsn, "1.0.0"},
+ {modules, [ppool, ppool_serv, ppool_sup, ppool_supersup, ppool_worker_sup]},
+ {registered, [ppool]},
+ {mod, {ppool, []}}
+ ]}.
diff --git a/learn-you-some-erlang/ppool-1.0/src/ppool.erl b/learn-you-some-erlang/ppool-1.0/src/ppool.erl
new file mode 100644
index 0000000..5723f98
--- /dev/null
+++ b/learn-you-some-erlang/ppool-1.0/src/ppool.erl
@@ -0,0 +1,26 @@
+%%% API module for the pool
+-module(ppool).
+-behaviour(application).
+-export([start/2, stop/1, start_pool/3,
+ run/2, sync_queue/2, async_queue/2, stop_pool/1]).
+
+start(normal, _Args) ->
+ ppool_supersup:start_link().
+
+stop(_State) ->
+ ok.
+
+start_pool(Name, Limit, {M,F,A}) ->
+ ppool_supersup:start_pool(Name, Limit, {M,F,A}).
+
+stop_pool(Name) ->
+ ppool_supersup:stop_pool(Name).
+
+run(Name, Args) ->
+ ppool_serv:run(Name, Args).
+
+async_queue(Name, Args) ->
+ ppool_serv:async_queue(Name, Args).
+
+sync_queue(Name, Args) ->
+ ppool_serv:sync_queue(Name, Args).
diff --git a/learn-you-some-erlang/ppool-1.0/src/ppool_serv.erl b/learn-you-some-erlang/ppool-1.0/src/ppool_serv.erl
new file mode 100644
index 0000000..a16130c
--- /dev/null
+++ b/learn-you-some-erlang/ppool-1.0/src/ppool_serv.erl
@@ -0,0 +1,112 @@
+-module(ppool_serv).
+-behaviour(gen_server).
+-export([start/4, start_link/4, run/2, sync_queue/2, async_queue/2, stop/1]).
+-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
+ code_change/3, terminate/2]).
+
+%% The friendly supervisor is started dynamically!
+-define(SPEC(MFA),
+ {worker_sup,
+ {ppool_worker_sup, start_link, [MFA]},
+ temporary,
+ 10000,
+ supervisor,
+ [ppool_worker_sup]}).
+
+-record(state, {limit=0,
+ sup,
+ refs,
+ queue=queue:new()}).
+
+start(Name, Limit, Sup, MFA) when is_atom(Name), is_integer(Limit) ->
+ gen_server:start({local, Name}, ?MODULE, {Limit, MFA, Sup}, []).
+
+start_link(Name, Limit, Sup, MFA) when is_atom(Name), is_integer(Limit) ->
+ gen_server:start_link({local, Name}, ?MODULE, {Limit, MFA, Sup}, []).
+
+run(Name, Args) ->
+ gen_server:call(Name, {run, Args}).
+
+sync_queue(Name, Args) ->
+ gen_server:call(Name, {sync, Args}, infinity).
+
+async_queue(Name, Args) ->
+ gen_server:cast(Name, {async, Args}).
+
+stop(Name) ->
+ gen_server:call(Name, stop).
+
+%% Gen server
+init({Limit, MFA, Sup}) ->
+ %% We need to find the Pid of the worker supervisor from here,
+ %% but alas, this would be calling the supervisor while it waits for us!
+ self() ! {start_worker_supervisor, Sup, MFA},
+ {ok, #state{limit=Limit, refs=gb_sets:empty()}}.
+
+handle_call({run, Args}, _From, S = #state{limit=N, sup=Sup, refs=R}) when N > 0 ->
+ {ok, Pid} = supervisor:start_child(Sup, Args),
+ Ref = erlang:monitor(process, Pid),
+ {reply, {ok,Pid}, S#state{limit=N-1, refs=gb_sets:add(Ref,R)}};
+handle_call({run, _Args}, _From, S=#state{limit=N}) when N =< 0 ->
+ {reply, noalloc, S};
+
+handle_call({sync, Args}, _From, S = #state{limit=N, sup=Sup, refs=R}) when N > 0 ->
+ {ok, Pid} = supervisor:start_child(Sup, Args),
+ Ref = erlang:monitor(process, Pid),
+ {reply, {ok,Pid}, S#state{limit=N-1, refs=gb_sets:add(Ref,R)}};
+handle_call({sync, Args}, From, S = #state{queue=Q}) ->
+ {noreply, S#state{queue=queue:in({From, Args}, Q)}};
+
+handle_call(stop, _From, State) ->
+ {stop, normal, ok, State};
+handle_call(_Msg, _From, State) ->
+ {noreply, State}.
+
+
+handle_cast({async, Args}, S=#state{limit=N, sup=Sup, refs=R}) when N > 0 ->
+ {ok, Pid} = supervisor:start_child(Sup, Args),
+ Ref = erlang:monitor(process, Pid),
+ {noreply, S#state{limit=N-1, refs=gb_sets:add(Ref,R)}};
+handle_cast({async, Args}, S=#state{limit=N, queue=Q}) when N =< 0 ->
+ {noreply, S#state{queue=queue:in(Args,Q)}};
+
+handle_cast(_Msg, State) ->
+ {noreply, State}.
+
+handle_info({'DOWN', Ref, process, _Pid, _}, S = #state{refs=Refs}) ->
+ case gb_sets:is_element(Ref, Refs) of
+ true ->
+ handle_down_worker(Ref, S);
+ false -> %% Not our responsibility
+ {noreply, S}
+ end;
+handle_info({start_worker_supervisor, Sup, MFA}, S = #state{}) ->
+ {ok, Pid} = supervisor:start_child(Sup, ?SPEC(MFA)),
+ link(Pid),
+ {noreply, S#state{sup=Pid}};
+handle_info(Msg, State) ->
+ io:format("Unknown msg: ~p~n", [Msg]),
+ {noreply, State}.
+
+code_change(_OldVsn, State, _Extra) ->
+ {ok, State}.
+
+terminate(_Reason, _State) ->
+ ok.
+
+handle_down_worker(Ref, S = #state{limit=L, sup=Sup, refs=Refs}) ->
+ case queue:out(S#state.queue) of
+ {{value, {From, Args}}, Q} ->
+ {ok, Pid} = supervisor:start_child(Sup, Args),
+ NewRef = erlang:monitor(process, Pid),
+ NewRefs = gb_sets:insert(NewRef, gb_sets:delete(Ref,Refs)),
+ gen_server:reply(From, {ok, Pid}),
+ {noreply, S#state{refs=NewRefs, queue=Q}};
+ {{value, Args}, Q} ->
+ {ok, Pid} = supervisor:start_child(Sup, Args),
+ NewRef = erlang:monitor(process, Pid),
+ NewRefs = gb_sets:insert(NewRef, gb_sets:delete(Ref,Refs)),
+ {noreply, S#state{refs=NewRefs, queue=Q}};
+ {empty, _} ->
+ {noreply, S#state{limit=L+1, refs=gb_sets:delete(Ref,Refs)}}
+ end.
diff --git a/learn-you-some-erlang/ppool-1.0/src/ppool_sup.erl b/learn-you-some-erlang/ppool-1.0/src/ppool_sup.erl
new file mode 100644
index 0000000..71ec31d
--- /dev/null
+++ b/learn-you-some-erlang/ppool-1.0/src/ppool_sup.erl
@@ -0,0 +1,17 @@
+-module(ppool_sup).
+-export([start_link/3, init/1]).
+-behaviour(supervisor).
+
+start_link(Name, Limit, MFA) ->
+ supervisor:start_link(?MODULE, {Name, Limit, MFA}).
+
+init({Name, Limit, MFA}) ->
+ MaxRestart = 1,
+ MaxTime = 3000,
+ {ok, {{one_for_all, MaxRestart, MaxTime},
+ [{serv,
+ {ppool_serv, start_link, [Name, Limit, self(), MFA]},
+ permanent,
+ 5000,
+ worker,
+ [ppool_serv]}]}}.
diff --git a/learn-you-some-erlang/ppool-1.0/src/ppool_supersup.erl b/learn-you-some-erlang/ppool-1.0/src/ppool_supersup.erl
new file mode 100644
index 0000000..06fa0af
--- /dev/null
+++ b/learn-you-some-erlang/ppool-1.0/src/ppool_supersup.erl
@@ -0,0 +1,22 @@
+-module(ppool_supersup).
+-behaviour(supervisor).
+-export([start_link/0, start_pool/3, stop_pool/1]).
+-export([init/1]).
+
+start_link() ->
+ supervisor:start_link({local, ppool}, ?MODULE, []).
+
+start_pool(Name, Limit, MFA) ->
+ ChildSpec = {Name,
+ {ppool_sup, start_link, [Name, Limit, MFA]},
+ permanent, 10500, supervisor, [ppool_sup]},
+ supervisor:start_child(ppool, ChildSpec).
+
+stop_pool(Name) ->
+ supervisor:terminate_child(ppool, Name),
+ supervisor:delete_child(ppool, Name).
+
+init([]) ->
+ MaxRestart = 6,
+ MaxTime = 3000,
+ {ok, {{one_for_one, MaxRestart, MaxTime}, []}}.
diff --git a/learn-you-some-erlang/ppool-1.0/src/ppool_worker_sup.erl b/learn-you-some-erlang/ppool-1.0/src/ppool_worker_sup.erl
new file mode 100644
index 0000000..2467c47
--- /dev/null
+++ b/learn-you-some-erlang/ppool-1.0/src/ppool_worker_sup.erl
@@ -0,0 +1,15 @@
+-module(ppool_worker_sup).
+-export([start_link/1, init/1]).
+-behaviour(supervisor).
+
+start_link(MFA = {_,_,_}) ->
+ supervisor:start_link(?MODULE, MFA).
+
+init({M,F,A}) ->
+ MaxRestart = 5,
+ MaxTime = 3600,
+ {ok, {{simple_one_for_one, MaxRestart, MaxTime},
+ [{ppool_worker,
+ {M,F,A},
+ temporary, 5000, worker, [M]}]}}.
+
diff --git a/learn-you-some-erlang/ppool-1.0/test/ppool_nagger.erl b/learn-you-some-erlang/ppool-1.0/test/ppool_nagger.erl
new file mode 100644
index 0000000..903f821
--- /dev/null
+++ b/learn-you-some-erlang/ppool-1.0/test/ppool_nagger.erl
@@ -0,0 +1,50 @@
+%% demo module, a nagger for tasks,
+%% because the previous one wasn't good enough
+%%
+%% Can take:
+%% - a time delay for which to nag,
+%% - an adress to say where the messages should be sent
+%% - a message to send in the mailbox telling you what to nag,
+%% with an id to be able to call: ->
+%% - a command to say the task is done
+-module(ppool_nagger).
+-behaviour(gen_server).
+-export([start_link/4, stop/1]).
+-export([init/1, handle_call/3, handle_cast/2,
+ handle_info/2, code_change/3, terminate/2]).
+
+start_link(Task, Delay, Max, SendTo) ->
+ gen_server:start_link(?MODULE, {Task, Delay, Max, SendTo} , []).
+
+stop(Pid) ->
+ gen_server:call(Pid, stop).
+
+init({Task, Delay, Max, SendTo}) ->
+ process_flag(trap_exit, true), % for tests & terminate too
+ {ok, {Task, Delay, Max, SendTo}, Delay}.
+
+%%% OTP Callbacks
+handle_call(stop, _From, State) ->
+ {stop, normal, ok, State};
+handle_call(_Msg, _From, State) ->
+ {noreply, State}.
+
+handle_cast(_Msg, State) ->
+ {noreply, State}.
+
+handle_info(timeout, {Task, Delay, Max, SendTo}) ->
+ SendTo ! {self(), Task},
+ if Max =:= infinity ->
+ {noreply, {Task, Delay, Max, SendTo}, Delay};
+ Max =< 1 ->
+ {stop, normal, {Task, Delay, 0, SendTo}};
+ Max > 1 ->
+ {noreply, {Task, Delay, Max-1, SendTo}, Delay}
+ end;
+handle_info(_Msg, State) ->
+ {noreply, State}.
+
+code_change(_OldVsn, State, _Extra) ->
+ {ok, State}.
+
+terminate(_Reason, _State) -> ok.
diff --git a/learn-you-some-erlang/ppool-1.0/test/ppool_tests.erl b/learn-you-some-erlang/ppool-1.0/test/ppool_tests.erl
new file mode 100644
index 0000000..8f0dfe2
--- /dev/null
+++ b/learn-you-some-erlang/ppool-1.0/test/ppool_tests.erl
@@ -0,0 +1,200 @@
+-module(ppool_tests).
+-include_lib("eunit/include/eunit.hrl").
+-export([test_mfa/1, wait_mfa/1]).
+
+%%% All Test Fixtures
+start_test_() ->
+ {"It should be possible to start a pool server and give it a name",
+ {setup,
+ fun find_unique_name/0,
+ fun(Name) ->
+ [start_and_test_name(Name)]
+ end}}.
+
+mfa_test_() ->
+ {"A pool process can be allocated which will be ordered "
+ "to run an MFA call determined at start time, with arguments "
+ "provided at call time",
+ {setup,
+ fun start_ppool/0,
+ fun kill_ppool/1,
+ fun(Name) ->
+ [pool_run_mfa(Name)]
+ end}
+ }.
+
+alloc_test_() ->
+ {"A pool process can be allocated which will be ordered "
+ "to run a worker, only if there are enough which "
+ "haven't been ordered to run yet.",
+ {setup,
+ fun start_ppool/0,
+ fun kill_ppool/1,
+ fun(Name) ->
+ [pool_run_alloc(Name),
+ pool_run_noalloc(Name)]
+ end}
+ }.
+
+realloc_test_() ->
+ {"When an allocated process dies, "
+ "A new one can be allocated to replace it.",
+ {setup,
+ fun start_ppool/0,
+ fun kill_ppool/1,
+ fun(Name) ->
+ [pool_run_realloc(Name)]
+ end}
+ }.
+
+queue_test_() ->
+ {"The queue function can be used to run the function as soon as possible. "
+ "If no space is available, the worker call is added to the queue.",
+ {foreach,
+ fun start_ppool/0,
+ fun kill_ppool/1,
+ [fun(Name) -> test_async_queue(Name) end,
+ fun(Name) -> test_sync_queue(Name) end]}
+ }.
+
+supervision_test_() ->
+ {"The ppool will never restart a dead child, but all children (OTP "
+ "compliant) will be shut down when closing the pool, even if they "
+ "are trapping exits",
+ {setup,
+ fun find_unique_name/0,
+ fun test_supervision/1}}.
+
+auth_test_() ->
+ {"The ppool should only dequeue tasks after receiving a down signal "
+ "from a worker and nobody else",
+ {setup,
+ fun start_ppool/0,
+ fun kill_ppool/1,
+ fun test_auth_dealloc/1}}.
+
+%%% Setups/teardowns
+find_unique_name() ->
+ application:start(ppool),
+ Name = list_to_atom(lists:flatten(io_lib:format("~p",[now()]))),
+ ?assertEqual(undefined, whereis(Name)),
+ Name.
+
+start_ppool() ->
+ Name = find_unique_name(),
+ ppool:start_pool(Name, 2, {ppool_nagger, start_link, []}),
+ Name.
+
+kill_ppool(Name) ->
+ ppool:stop_pool(Name).
+
+%%% Actual tests
+start_and_test_name(Name) ->
+ ppool:start_pool(Name, 1, {ppool_nagger, start_link, []}),
+ A = whereis(Name),
+ ppool:stop_pool(Name),
+ timer:sleep(100),
+ B = whereis(Name),
+ [?_assert(undefined =/= A),
+ ?_assertEqual(undefined, B)].
+
+pool_run_mfa(Name) ->
+ ppool:run(Name, [i_am_running, 1, 1, self()]),
+ X = receive
+ {_Pid, i_am_running} -> ok
+ after 3000 ->
+ timeout
+ end,
+ ?_assertEqual(ok, X).
+
+pool_run_alloc(Name) ->
+ {ok, Pid} = ppool:run(Name, [i_am_running, 1, 1, self()]),
+ X = receive
+ {Pid, i_am_running} -> ok
+ after 3000 ->
+ timeout
+ end,
+ [?_assert(is_pid(Pid)),
+ ?_assertEqual(ok, X)].
+
+pool_run_noalloc(Name) ->
+ %% Init function should have set the limit to 2
+ ppool:run(Name, [i_am_running, 300, 1, self()]),
+ ppool:run(Name, [i_am_running, 300, 1, self()]),
+ X = ppool:run(Name, [i_am_running, 1, 1, self()]),
+ ?_assertEqual(noalloc, X).
+
+pool_run_realloc(Name) ->
+ %% Init function should have set the limit to 2
+ {ok, A} = ppool:run(Name, [i_am_running, 500, 1, self()]),
+ timer:sleep(100),
+ {ok, B} = ppool:run(Name, [i_am_running, 500, 1, self()]),
+ timer:sleep(600),
+ {ok, Pid} = ppool:run(Name, [i_am_running, 1, 1, self()]),
+ timer:sleep(100),
+ L = flush(),
+ [?_assert(is_pid(Pid)),
+ ?_assertEqual([{A,i_am_running}, {B,i_am_running}, {Pid,i_am_running}],
+ L)].
+
+test_async_queue(Name) ->
+ %% Still two elements max!
+ ok = ppool:async_queue(Name, [i_am_running, 2000, 1, self()]),
+ ok = ppool:async_queue(Name, [i_am_running, 2000, 1, self()]),
+ noalloc = ppool:run(Name, [i_am_running, 2000, 1, self()]),
+ ok = ppool:async_queue(Name, [i_am_running, 500, 1, self()]),
+ timer:sleep(3500),
+ L = flush(),
+ ?_assertMatch([{_, i_am_running}, {_, i_am_running}, {_, i_am_running}], L).
+
+test_sync_queue(Name) ->
+ %% Hell yase, two max
+ {ok, Pid} = ppool:sync_queue(Name, [i_am_running, 200, 1, self()]),
+ ok = ppool:async_queue(Name, [i_am_running, 200, 1, self()]),
+ ok = ppool:async_queue(Name, [i_am_running, 200, 1, self()]),
+ {ok, Pid2} = ppool:sync_queue(Name, [i_am_running, 100, 1, self()]),
+ timer:sleep(300),
+ L = flush(),
+ [?_assert(is_pid(Pid)),
+ ?_assert(is_pid(Pid2)),
+ ?_assertMatch([{_,i_am_running}, {_,i_am_running},
+ {_,i_am_running}, {_,i_am_running}],
+ L)].
+
+test_supervision(Name) ->
+ ppool:start_pool(Name, 1, {ppool_nagger, start_link, []}),
+ {ok, Pid} = ppool:run(Name, [sup, 10000, 100, self()]),
+ ppool:stop_pool(Name),
+ timer:sleep(100),
+ ?_assertEqual(undefined, process_info(Pid)).
+
+test_auth_dealloc(Name) ->
+ %% Hell yase, two max
+ {ok, _Pid} = ppool:sync_queue(Name, [i_am_running, 500, 1, self()]),
+ ok = ppool:async_queue(Name, [i_am_running, 10000, 1, self()]),
+ ok = ppool:async_queue(Name, [i_am_running, 10000, 1, self()]),
+ ok = ppool:async_queue(Name, [i_am_running, 1, 1, self()]),
+ timer:sleep(600),
+ Name ! {'DOWN', make_ref(), process, self(), normal},
+ Name ! {'DOWN', make_ref(), process, self(), normal},
+ Name ! {'DOWN', make_ref(), process, self(), normal},
+ timer:sleep(200),
+ L = flush(),
+ ?_assertMatch([{_,i_am_running}], L).
+
+
+
+flush() ->
+ receive
+ X -> [X|flush()]
+ after 0 ->
+ []
+ end.
+
+%% Exported Helper functions
+test_mfa(Pid) ->
+ Pid ! i_am_running.
+
+wait_mfa(Pid) ->
+ Pid ! i_am_running,
+ timer:sleep(3000).
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.0.0/Emakefile b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/Emakefile
new file mode 100644
index 0000000..b203ea3
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/Emakefile
@@ -0,0 +1,2 @@
+{["src/*", "test/*"],
+ [{i,"include"}, {outdir, "ebin"}]}.
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.0.0/ebin/.this-file-intentionally-left-blank b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/ebin/.this-file-intentionally-left-blank
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/ebin/.this-file-intentionally-left-blank
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.0.0/ebin/processquest.app b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/ebin/processquest.app
new file mode 100644
index 0000000..abda4b2
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/ebin/processquest.app
@@ -0,0 +1,7 @@
+{application, processquest,
+ [{description, "Game inspired by the Progress Quest game (http://progressquest.com)"},
+ {vsn, "1.0.0"},
+ {mod, {processquest, []}},
+ {registered, [pq_supersup]},
+ {modules, [processquest, pq_stats, pq_enemy, pq_events, pq_player]},
+ {applications, [stdlib, kernel, regis, crypto]}]}.
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.0.0/include/.this-file-intentionally-left-blank b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/include/.this-file-intentionally-left-blank
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/include/.this-file-intentionally-left-blank
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.0.0/priv/.this-file-intentionally-left-blank b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/priv/.this-file-intentionally-left-blank
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/priv/.this-file-intentionally-left-blank
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_enemy.erl b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_enemy.erl
new file mode 100644
index 0000000..6f8e9b3
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_enemy.erl
@@ -0,0 +1,17 @@
+%% Gives random enemies
+-module(pq_enemy).
+-export([fetch/0]).
+
+fetch() ->
+ L = enemies(),
+ lists:nth(random:uniform(length(L)), L).
+
+enemies() ->
+ [{<<"Ant">>, [{drop, {<<"Ant Egg">>, 1}}, {experience, 1}]},
+ {<<"Wildcat">>, [{drop, {<<"Pelt">>, 1}}, {experience, 1}]},
+ {<<"Pig">>, [{drop, {<<"Bacon">>, 1}}, {experience, 1}]},
+ {<<"Wild Pig">>, [{drop, {<<"Tasty Ribs">>, 2}}, {experience, 1}]},
+ {<<"Goblin">>, [{drop, {<<"Goblin hair">>, 1}}, {experience, 2}]},
+ {<<"Robot">>, [{drop, {<<"Chunks of Metal">>, 3}}, {experience, 2}]}].
+
+
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_events.erl b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_events.erl
new file mode 100644
index 0000000..324c432
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_events.erl
@@ -0,0 +1,49 @@
+%%% Wrapper module for the event manager of ProgressQuest players.
+%%% It adds a few functions to wrap the events and sleep on the right
+%%% scale on behalf of the pq_player process.
+-module(pq_events).
+-export([killed/3, location/3, lvl_up/5, buy/4, sell/3]).
+-export([start_link/1, stop/1, add_handler/3, delete_handler/3, notify/2]).
+
+start_link(Name) ->
+ {ok, Pid} = gen_event:start_link(),
+ ok = regis:register({events, Name}, Pid),
+ {ok, Pid}.
+
+stop(Name) ->
+ ManagerPid = regis:whereis({events, Name}),
+ gen_event:stop(ManagerPid).
+
+add_handler(Name, Handler, Args) ->
+ ManagerPid = regis:whereis({events, Name}),
+ gen_event:add_handler(ManagerPid, Handler, Args).
+
+delete_handler(Name, Handler, Args) ->
+ ManagerPid = regis:whereis({events, Name}),
+ gen_event:delete_handler(ManagerPid, Handler, Args).
+
+notify(Name, Msg) ->
+ ManagerPid = regis:whereis({events, Name}),
+ gen_event:notify(ManagerPid, Msg).
+
+killed(Name, Enemy = {_EnemyName, _Props}, Time) ->
+ notify(Name, {Name, killed, Time*2, Enemy}),
+ timer:sleep(Time*2).
+
+location(Name, Place, Time) ->
+ notify(Name, {Name, heading, Time, Place}),
+ timer:sleep(Time).
+
+lvl_up(Name, NewStats, NewLvl, NewExp, _Time) ->
+ notify(Name, {Name, lvl_up, 0, NewStats, NewLvl, NewExp}),
+ ok.
+
+buy(Name, Slot, Item, Time) ->
+ T = round(Time/2),
+ notify(Name, {Name, buy, T, Slot, Item}),
+ timer:sleep(T).
+
+sell(Name, Item, Time) ->
+ T = round(Time/5),
+ notify(Name, {Name, sell, T, Item}),
+ timer:sleep(Time).
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_market.erl b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_market.erl
new file mode 100644
index 0000000..241e9cd
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_market.erl
@@ -0,0 +1,77 @@
+%%% Can be used to obtain weapons and pieces of equipment of various types
+%%% to be equipped by the hero. The standard format is:
+%%% {Name, LevelModifier, Level, Price}.
+-module(pq_market).
+-export([helmet/2, weapon/2, shield/2, armor/2]).
+
+weapon(CombinedLvl, Money) ->
+ L = [
+ {<<"plastic knife">>, -1, 1, 2},
+ {<<"plastic knife">>, 0, 1, 3},
+ {<<"plastic knife">>, 1, 1, 5},
+ {<<"metal spoon">>, -1, 4, 3},
+ {<<"metal spoon">>, 0, 4, 4},
+ {<<"butter knife">>, -1, 6, 5},
+ {<<"butter knife">>, 0, 6, 7},
+ {<<"butter knife">>, 1, 6, 9},
+ {<<"machete">>, -1, 9, 15},
+ {<<"machete">>, 0, 9, 20},
+ {<<"machete">>, 1, 9, 25},
+ {<<"broad sword">>, -1, 12, 23},
+ {<<"broad sword">>, 0, 12, 30},
+ {<<"broad sword">>, 1, 12, 38},
+ {<<"lance">>, -1, 15, 32},
+ {<<"lance">>, 0, 15, 44},
+ {<<"lance">>, 1, 15, 57},
+ {<<"pistol">>, -1, 25, 95},
+ {<<"pistol">>, 0, 25, 105},
+ {<<"pistol">>, 1, 25, 155},
+ {<<"submachine gun">>, -1, 40, 200},
+ {<<"submachine gun">>, 0, 40, 245},
+ {<<"submachine gun">>, 1, 40, 365}
+ ],
+ first_match(fun(W = {_, Modifier, Lvl, Price}) ->
+ if Modifier+Lvl > CombinedLvl, Price =< Money -> W;
+ true -> continue
+ end
+ end, L).
+
+helmet(CombinedLvl, Money) -> pick_material(CombinedLvl, Money).
+shield(CombinedLvl, Money) -> pick_material(CombinedLvl, Money).
+armor(CombinedLvl, Money) -> pick_material(CombinedLvl, Money).
+
+pick_material(CombinedLvl, Money) ->
+ L = materials(),
+ first_match(fun(W = {_, Modifier, Lvl, Price}) ->
+ if Modifier+Lvl > CombinedLvl, Price =< Money -> W;
+ true -> continue
+ end
+ end, L).
+
+
+first_match(_, []) -> undefined;
+first_match(F, [H|T]) ->
+ case F(H) of
+ continue -> first_match(F,T);
+ Val -> Val
+ end.
+
+materials() ->
+ [{<<"wool">>, 0, 1, 25},
+ {<<"pleather">>, 0, 2, 45},
+ {<<"pleather">>, 1, 2, 50},
+ {<<"pleather">>, 2, 2, 65},
+ {<<"leather">>, -2, 7, 30},
+ {<<"leather">>, -1, 7, 35},
+ {<<"leather">>, 0, 7, 45},
+ {<<"leather">>, 2, 7, 65},
+ {<<"chain mail">>, -2, 12, 70},
+ {<<"chain mail">>, 0, 12, 85},
+ {<<"chain mail">>, 1, 12, 95},
+ {<<"chain mail">>, 2, 12, 105},
+ {<<"plate mail">>, -2, 17, 90},
+ {<<"plate mail">>, -1, 17, 95},
+ {<<"plate mail">>, 0, 17, 105},
+ {<<"plate mail">>, 1, 17, 115},
+ {<<"plate mail">>, 2, 17, 135}].
+
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_player.erl b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_player.erl
new file mode 100644
index 0000000..b304f69
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_player.erl
@@ -0,0 +1,175 @@
+%%% The core of ProcessQuest -- the player FSM,
+%%% acting for each of the players in the game.
+%%%
+%%% The FSM actually depends on no external events and only sends messages
+%%% to itself to prompt state changes. This is somewhat unusual as far as
+%%% gen_fsm usages go, but it's pretty useful when needing to work with
+%%% a standalone process.
+-module(pq_player).
+-behaviour(gen_fsm).
+-export([start_link/2]).
+-export([init/1, market/2, killing/2, handle_event/3, handle_sync_event/4,
+ handle_info/3, terminate/3, code_change/4]).
+
+-record(state, {name, stats, exp=0, lvlexp=1000, lvl=1,
+ equip=[], money=0, loot=[], bought=[], time=0}).
+
+
+%%% Possible states & events
+%%
+% sell buy
+% / | | \
+% \ ^ ^ /
+% [market]<--,
+% | |
+% done buying |
+% | bag full
+% v /
+% [killing fields]
+% / V V |
+% \ / | |
+% kill lvl up
+
+start_link(Name, Opts) ->
+ gen_fsm:start_link(?MODULE, {Name, Opts}, []).
+
+init({Name, Opts}) ->
+ %% Properly seeding stuff. If not doing this, the random module will
+ %% seed it based on a slightly unique time value. However, when starting
+ %% many processes at about the same time, the seeds can be very close
+ %% and give barely random results. The crypto:rand_bytes/1 function
+ %% allows for much better seeding.
+ <<A:32, B:32, C:32>> = crypto:rand_bytes(12),
+ random:seed({A,B,C}),
+ %% The first event, to start the FSM
+ gen_fsm:send_event(self(), kill),
+ case regis:register(Name, self()) of
+ {error, _} ->
+ {stop, name_taken};
+ ok ->
+ %% Use proplists with default values to let the user configure
+ %% all parts of the FSM's state by using the Opts proplist.
+ S = #state{
+ name=Name,
+ stats=proplists:get_value(stats, Opts, pq_stats:initial_roll()),
+ exp=proplists:get_value(exp, Opts, 0),
+ lvlexp=proplists:get_value(lvlexp, Opts, 1000),
+ lvl=proplists:get_value(lvl, Opts, 1),
+ equip=proplists:get_value(equip, Opts, []),
+ money=proplists:get_value(money, Opts, 0),
+ loot=proplists:get_value(loot, Opts, []),
+ bought=proplists:get_value(bought, Opts, []),
+ time=proplists:get_value(time, Opts, 0)
+ },
+ {ok, market, S}
+ end.
+
+%% Done selling. Switch to the event where we head to the killing fields
+market(sell, S = #state{loot=[]}) ->
+ gen_fsm:send_event(self(), buy),
+ {next_state, market, S};
+%% Selling an Item we have looted to the market, for whatever value it has
+market(sell, S = #state{loot=[H={_X,Val}|T], money=M}) ->
+ pq_events:sell(S#state.name, H, S#state.time),
+ gen_fsm:send_event(self(), sell),
+ {next_state, market, S#state{loot=T, money=M+Val}};
+%% When done selling, buy items with your money
+market(buy, S = #state{equip=Equip, money=Money, bought=Bought}) ->
+ %% we have slots of equipment. It's useless to buy the same
+ %% kind of item time after time, so we must select one we haven't observed yet
+ case next_slot(Equip, Bought) of
+ undefined ->
+ %% when no slot is found, go to the killing field
+ gen_fsm:send_event(self(), kill),
+ {next_state, market, S#state{bought=[]}};
+ OldItem = {Slot, {_Name, Modifier, Lvl, _Price}} ->
+ %% Replace the item by a slightly better one if possible.
+ case pq_market:Slot(Modifier+Lvl, Money) of
+ undefined ->
+ market(buy, S#state{bought=[Slot|Bought]});
+ NewItem = {_, _, _, Price} ->
+ pq_events:buy(S#state.name, Slot, NewItem, S#state.time),
+ gen_fsm:send_event(self(), buy),
+ NewEquip = [{Slot, NewItem} | Equip -- [OldItem]],
+ {next_state, market, S#state{equip=NewEquip,
+ money=Money-Price,
+ bought=[Slot|Bought]}}
+ end
+ end;
+%% Heading to the killing field. State only useful as a state transition.
+market(kill, S) ->
+ pq_events:location(S#state.name, killing, S#state.time),
+ gen_fsm:send_event(self(), kill),
+ {next_state, killing, S}.
+
+%% Killing an enemy on the killing field. Taking its drop and keeping it
+%% in our loot.
+killing(kill, S = #state{loot=Loot, stats=Stats, exp=Exp, lvlexp=LvlExp}) ->
+ MaxSize = proplists:get_value(strength, Stats)*2,
+ {EnemyName, Props} = pq_enemy:fetch(),
+ pq_events:killed(S#state.name, {EnemyName, Props}, S#state.time),
+ Drop = {_N, _V} = proplists:get_value(drop, Props),
+ KillExp = proplists:get_value(experience, Props),
+ NewLoot = [Drop|Loot],
+ if length(NewLoot) =:= MaxSize ->
+ gen_fsm:send_event(self(), market);
+ Exp+KillExp >= LvlExp ->
+ gen_fsm:send_event(self(), lvl_up);
+ true ->
+ gen_fsm:send_event(self(), kill)
+ end,
+ {next_state, killing, S#state{loot=NewLoot, exp=Exp+KillExp}};
+%% If we just leveled up, the stats get updated before we keep killing.
+killing(lvl_up, S = #state{stats=Stats, lvl=Lvl, lvlexp=LvlExp}) ->
+ NewStats = [{charisma, proplists:get_value(charisma, Stats)+pq_stats:roll()},
+ {constitution, proplists:get_value(constitution, Stats)+pq_stats:roll()},
+ {dexterity, proplists:get_value(dexterity, Stats)+pq_stats:roll()},
+ {intelligence, proplists:get_value(intelligence, Stats)+pq_stats:roll()},
+ {strength, proplists:get_value(strength, Stats)+pq_stats:roll()},
+ {wisdom, proplists:get_value(wisdom, Stats)+pq_stats:roll()}],
+ gen_fsm:send_event(self(), kill),
+ pq_events:lvl_up(S#state.name, NewStats, Lvl+1, LvlExp*2, S#state.time),
+ {next_state, killing, S#state{stats=NewStats, lvl=Lvl+1, lvlexp=LvlExp*2}};
+%% Heading to the market state transition
+killing(market, S) ->
+ pq_events:location(S#state.name, market, S#state.time),
+ gen_fsm:send_event(self(), sell),
+ {next_state, market, S}.
+
+handle_event(_Event, StateName, State) ->
+ {next_state, StateName, State}.
+
+handle_sync_event(_Event, _From, StateName, State) ->
+ {next_state, StateName, State}.
+
+handle_info(_Event, StateName, State) ->
+ {next_state, StateName, State}.
+
+terminate(_Reason, _StateName, _State) ->
+ ok.
+
+code_change(_OldVsn, StateName, State, _Extra) ->
+ {next_state, StateName, State}.
+
+%%%%%%%%%%%%%%%
+%%% PRIVATE %%%
+%%%%%%%%%%%%%%%
+
+%% Picks a slot based on what has been seen so far, combined with the
+%% current weakest item.
+next_slot(Equip, Bought) ->
+ L = expand(Equip),
+ case lists:sort([{Mod+Lvl, Entry} || Entry = {Slot, {_, Mod, Lvl, _}} <- L,
+ not lists:member(Slot, Bought)]) of
+ [] -> undefined;
+ [{_, Entry}|_] -> Entry
+ end.
+
+expand(L) ->
+ [expand_field(armor, L),
+ expand_field(helmet, L),
+ expand_field(shield, L),
+ expand_field(weapon, L)].
+
+expand_field(F, L) ->
+ {F, proplists:get_value(F, L, {undefined,0,0,0})}.
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_stats.erl b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_stats.erl
new file mode 100644
index 0000000..379f5e9
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_stats.erl
@@ -0,0 +1,19 @@
+%%% Rolls dice to generate statistics or level increases for a character
+-module(pq_stats).
+-export([initial_roll/0, roll/0]).
+
+%% First roll, when setting the stats up for the first time
+initial_roll() ->
+ [{charisma, roll(3)},
+ {constitution, roll(3)},
+ {dexterity, roll(3)},
+ {intelligence, roll(3)},
+ {strength, roll(3)},
+ {wisdom, roll(3)}].
+
+%% Rolls a single die. Used when leveling up
+roll() -> roll(1).
+
+%% Rolls Num 6-faced dice
+roll(Num) ->
+ lists:sum([random:uniform(6) || _ <- lists:seq(1,Num)]).
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_sup.erl b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_sup.erl
new file mode 100644
index 0000000..de89a9a
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_sup.erl
@@ -0,0 +1,31 @@
+%%% Supervisor for each player. Goes over a pair of a
+%%% gen_fsm (pq_player) and gen_event (pq_events).
+-module(pq_sup).
+-behaviour(supervisor).
+-export([start_link/2]).
+-export([init/1]).
+
+
+start_link(Name, Info) ->
+ supervisor:start_link(?MODULE, {Name,Info}).
+
+%% The name is passed to the events so that
+%% it can register itself as {events, Name} into the
+%% 'regis' regsitry app.
+%% Same for pq_player, which also gets the info.
+%%
+%% It is important that pq_events is started before
+%% pq_player, otherwise we might create race conditions
+%% when starting a player and then quickly generating events to
+%% an event manager that doesn't exist.
+init({Name, Info}) ->
+ {ok,
+ {{one_for_all, 2, 3600},
+ [{events,
+ {pq_events, start_link, [Name]},
+ permanent, 5000, worker, [dynamic]},
+ {player,
+ {pq_player, start_link, [Name, Info]},
+ permanent, 2000, worker, [pq_player]}]}}.
+
+
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_supersup.erl b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_supersup.erl
new file mode 100644
index 0000000..577bbed
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_supersup.erl
@@ -0,0 +1,28 @@
+%%% pq_supersup is the ProcessQuest top-level supervisor.
+%%% It sits over many pq_sup instances, allowing to have
+%%% a truckload of different players running at once.
+-module(pq_supersup).
+-behaviour(supervisor).
+-export([start_link/0, start_player/2, stop_player/1]).
+-export([init/1]).
+
+%% We register it so that it's guaranteed to be unique
+start_link() ->
+ supervisor:start_link({local, ?MODULE}, ?MODULE, []).
+
+%% Using a SOFO strategy because we get to have many
+%% supervisees of the same type.
+init([]) ->
+ {ok,
+ {{simple_one_for_one, 1, 60000},
+ [{sup,
+ {pq_sup, start_link, []},
+ permanent, infinity, supervisor, [pq_sup]}]}}.
+
+%% Starts an individual player
+start_player(Name, Info) ->
+ supervisor:start_child(?MODULE, [Name, Info]).
+
+%% Stops a player.
+stop_player(Name) ->
+ supervisor:terminate_child(?MODULE, regis:whereis(Name)).
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/processquest.erl b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/processquest.erl
new file mode 100644
index 0000000..469dcb0
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/processquest.erl
@@ -0,0 +1,39 @@
+%%% ProcessQuest's main wrapping module.
+%%% Start ProcessQuest by calling application:start(processquest).
+%%% Create a player by calling processquest:start_player(Name, Info).
+%%% - Name is any term to identify the player
+%%% - Info is additional information to configure the player. Consult
+%%% the pq_player module for more info.
+%%%
+%%% You can subscribe to the player events by calling
+%%% processquest:subscribe(Name, Handler, Args).
+%%% The handler is a regular event handler. See test/pq_events_handler.erl.
+-module(processquest).
+-behaviour(application).
+-export([start/2, stop/1]).
+-export([start_player/2, stop_player/1, subscribe/3]).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%% APPLICATION CALLBACKS %%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+start(normal, []) ->
+ pq_supersup:start_link().
+
+stop(_) -> ok.
+
+%%%%%%%%%%%%%%%%%%%%%%
+%%% USER INTERFACE %%%
+%%%%%%%%%%%%%%%%%%%%%%
+
+%% Starts a player
+start_player(Name, Info) ->
+ pq_supersup:start_player(Name, Info).
+
+%% Stops a player
+stop_player(Name) ->
+ pq_supersup:stop_player(Name).
+
+%% Subscribe to user events
+subscribe(Name, Handler, Args) ->
+ pq_events:add_handler(Name, Handler, Args).
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/pq_enemy_tests.erl b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/pq_enemy_tests.erl
new file mode 100644
index 0000000..14742fa
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/pq_enemy_tests.erl
@@ -0,0 +1,33 @@
+-module(pq_enemy_tests).
+-include_lib("eunit/include/eunit.hrl").
+
+is_random_test_() ->
+ F = fun(Parent, Ref) -> fun() ->
+ <<A:32,B:32,C:32>> = crypto:rand_bytes(12),
+ random:seed({A,B,C}),
+ Entries = [pq_enemy:fetch() || _ <- lists:seq(1,100)],
+ Parent ! {Ref, Entries}
+ end end,
+ Refs = [begin
+ Ref = make_ref(),
+ spawn_link(F(self(), Ref)),
+ Ref
+ end || _ <- lists:seq(1,3)],
+ [A,B,C] = [receive
+ {Ref, X} -> X
+ end || Ref <- Refs],
+ [?_assert(A =/= B),
+ ?_assert(A =/= C),
+ ?_assert(B =/= C)].
+
+format_test_() ->
+ [[?_assertMatch({_Name, [{drop, {_DropName, _DropVal}},
+ {experience, _Exp}]}, pq_enemy:fetch())
+ || _ <- lists:seq(1,10)],
+ begin
+ {Name, [{drop, {Drop, Val}}, {experience, Exp}]} = pq_enemy:fetch(),
+ [?_assert(is_binary(Name)),
+ ?_assert(is_binary(Drop)),
+ ?_assert(is_integer(Val)),
+ ?_assert(is_integer(Exp))]
+ end].
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/pq_events_handler.erl b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/pq_events_handler.erl
new file mode 100644
index 0000000..a02df4c
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/pq_events_handler.erl
@@ -0,0 +1,24 @@
+%% A fake event handler used for tests
+-module(pq_events_handler).
+-behaviour(gen_event).
+-export([init/1, handle_event/2, handle_call/2, handle_info/2,
+ terminate/2, code_change/3]).
+
+init(Parent) -> {ok, Parent}.
+
+handle_event(E, Pid) ->
+ Pid ! E,
+ {ok, Pid}.
+
+handle_call(Req, Pid) ->
+ Pid ! Req,
+ {ok, ok, Pid}.
+
+handle_info(E, Pid) ->
+ Pid ! E,
+ {ok, Pid}.
+
+terminate(_, _) -> ok.
+
+code_change(_OldVsn, Pid, _Extra) ->
+ {ok, Pid}.
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/pq_events_tests.erl b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/pq_events_tests.erl
new file mode 100644
index 0000000..3a327b7
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/pq_events_tests.erl
@@ -0,0 +1,55 @@
+-module(pq_events_tests).
+-include_lib("eunit/include/eunit.hrl").
+
+-define(setup(Name, T), {setup, fun() -> start(Name) end, fun stop/1, fun T/1}).
+-define(setup(T), ?setup(make_ref(), T)).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%% TESTS DESCRIPTIONS %%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%
+events_start_stop_reg_test_() ->
+ {"The event handler can be reached, started and stopped by using the "
+ "player's name",
+ ?setup(can_contact)}.
+
+%%%%%%%%%%%%%%%%%%%%%%%
+%%% SETUP FUNCTIONS %%%
+%%%%%%%%%%%%%%%%%%%%%%%
+start(Name) ->
+ application:start(regis),
+ {ok, Pid} = pq_events:start_link(Name),
+ unlink(Pid),
+ Name.
+
+stop(Name) ->
+ pq_events:stop(Name).
+
+%%%%%%%%%%%%%%%%%%%%
+%%% ACTUAL TESTS %%%
+%%%%%%%%%%%%%%%%%%%%
+can_contact(Name) ->
+ ok = pq_events:add_handler(Name, pq_events_handler, self()),
+ pq_events:notify(Name, hello),
+ L1 = flush(),
+ pq_events:delete_handler(Name, pq_events_handler, []),
+ pq_events:notify(Name, hello),
+ L2 = flush(),
+ [?_assertEqual([hello], L1),
+ ?_assertEqual([], L2)].
+
+%%%%%%%%%%%%%%%%%%%%%%%%
+%%% HELPER FUNCTIONS %%%
+%%%%%%%%%%%%%%%%%%%%%%%%
+flush() ->
+ receive
+ X -> [X | flush1()]
+ after 300 ->
+ []
+ end.
+
+flush1() ->
+ receive
+ X -> [X | flush1()]
+ after 0 ->
+ []
+ end.
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/pq_market_tests.erl b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/pq_market_tests.erl
new file mode 100644
index 0000000..d689f67
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/pq_market_tests.erl
@@ -0,0 +1,15 @@
+-module(pq_market_tests).
+-include_lib("eunit/include/eunit.hrl").
+
+best_smallest_weapon_test_() ->
+ [?_assertMatch({<<"plastic knife">>, 0, 1, 3}, pq_market:weapon(0, 5)),
+ ?_assertMatch({<<"plastic knife">>, 1, 1, 5}, pq_market:weapon(1, 100)),
+ ?_assertMatch(undefined, pq_market:weapon(0,0)),
+ ?_assertMatch(undefined, pq_market:weapon(50000,100000000000000000000))].
+
+best_smallest_gear_test_() ->
+ [[?_assertMatch({<<"wool">>, 0, 1, 25}, pq_market:F(0, 35)),
+ ?_assertMatch({<<"pleather">>, 0, 2, 45}, pq_market:F(1, 100)),
+ ?_assertMatch(undefined, pq_market:F(0,0)),
+ ?_assertMatch(undefined, pq_market:F(50000,100000000000000000000))]
+ || F <- [helmet, shield, armor]].
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/pq_player_tests.erl b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/pq_player_tests.erl
new file mode 100644
index 0000000..bbdbf3d
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/pq_player_tests.erl
@@ -0,0 +1,303 @@
+-module(pq_player_tests).
+-include_lib("eunit/include/eunit.hrl").
+-record(state, {name, stats, exp=0, lvlexp=1000, lvl=1, % copied from pq_player.erl
+ equip=[], money=0, loot=[], bought=[], time=0}).
+
+-define(setup(Name, T), {setup, fun() -> start(Name) end, fun stop/1, fun T/1}).
+-define(setup(T), ?setup(make_ref(), T)).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%% TESTS DESCRIPTIONS %%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%
+new_player_test_() ->
+ [{"A player holds its own name in its state",
+ ?setup(initial_name)},
+ {"A new player has stats randomly rolled",
+ ?setup(initial_roll)},
+ {"A new player becomes a registered process",
+ ?setup(initial_register)},
+ {"A player has a counter for experience and a counter for "
+ "the next level (which is higher than the current exp)",
+ ?setup(initial_lvl)},
+ {"A player has a basic equipment (empty) when first going",
+ ?setup(initial_equipment)},
+ {"A new player has no money",
+ ?setup(initial_money)},
+ {"A new player has no loot",
+ ?setup(initial_loot)},
+ {"The state of a player can be overriden using the info "
+ "arguments to the init function",
+ ?setup(override_init)}].
+
+market_test_() ->
+ [{"A player with N items will sell all of them to the market "
+ "and end with equivalent money, before switching to the "
+ "buying state",
+ ?setup(sell_all)},
+ {"A player with nearly infinite money will buy items available "
+ "for his money, higher than his level",
+ ?setup(buy_items)},
+ {"A player with no money or no items available for the price "
+ "range leaves for the killing fields.",
+ ?setup(buy_none)},
+ {"Receiving the kill message just forwards to the killing state",
+ ?setup(to_killing)}].
+
+killing_fields_test_() ->
+ [{"Kill enemies until the loot limit is hit. Loot is 2x Strength",
+ ?setup(loot_limit)},
+ {"Killing enemies raises XP until someone levels up",
+ ?setup(kill_xp)},
+ {"Leveling up boosts stats. The sum is higher by at least as "
+ "many points as there are fields, but not more than 6 times. "
+ "Moreover, the rolling is random.",
+ ?setup(lvl_stats)},
+ {"receiving the market message just forwards to the market state",
+ ?setup(to_market)}].
+
+%%%%%%%%%%%%%%%%%%%%%%%
+%%% SETUP FUNCTIONS %%%
+%%%%%%%%%%%%%%%%%%%%%%%
+start(Name) ->
+ application:start(crypto),
+ application:start(regis),
+ Pid = spawn(fun() -> timer:sleep(infinity) end),
+ regis:register({events, Name}, Pid),
+ Name.
+
+stop(Name) ->
+ exit(regis:whereis({events, Name}), kill),
+ application:stop(regis),
+ application:stop(crypto).
+
+%%%%%%%%%%%%%%%%%%%%
+%%% ACTUAL TESTS %%%
+%%%%%%%%%%%%%%%%%%%%
+
+%% Initial State
+initial_name(Name) ->
+ {ok, market, S} = pq_player:init({Name, []}),
+ M = read_event(),
+ [?_assertEqual(Name, S#state.name),
+ ?_assertEqual(M, kill)].
+
+initial_roll(Name) ->
+ {ok, _, S} = pq_player:init({Name, []}),
+ _ = read_event(),
+ ?_assertMatch([{charisma,_}, {constitution, _}, {dexterity, _},
+ {intelligence, _}, {strength, _}, {wisdom, _}],
+ lists:sort(S#state.stats)).
+
+initial_register(Ref) ->
+ {ok, _, _} = pq_player:init({Ref, []}),
+ _ = read_event(),
+ Pid = regis:whereis(Ref),
+ Ret = pq_player:init({Ref, []}),
+ _ = read_event(),
+ [?_assert(undefined =/= Pid),
+ ?_assert(is_pid(Pid)),
+ ?_assertEqual({stop, name_taken}, Ret)].
+
+initial_lvl(Name) ->
+ {ok, _, S} = pq_player:init({Name, []}),
+ _ = read_event(),
+ [?_assert(is_integer(S#state.lvlexp)),
+ ?_assert(S#state.lvlexp > 0),
+ ?_assert(S#state.exp =:= 0)]. % start at 0 exp
+
+initial_equipment(Name) ->
+ {ok, _, S} = pq_player:init({Name, []}),
+ _ = read_event(),
+ [?_assert(is_list(S#state.equip)),
+ ?_assertEqual(undefined, proplists:get_value(armor, S#state.equip)),
+ ?_assertEqual(undefined, proplists:get_value(helmet, S#state.equip)),
+ ?_assertEqual(undefined, proplists:get_value(weapon, S#state.equip)),
+ ?_assertEqual(undefined, proplists:get_value(shield, S#state.equip))].
+
+initial_money(Name) ->
+ {ok, _, S} = pq_player:init({Name, []}),
+ _ = read_event(),
+ ?_assertEqual(0, S#state.money).
+
+initial_loot(Name) ->
+ {ok, _, S} = pq_player:init({Name, []}),
+ _ = read_event(),
+ ?_assertEqual([], S#state.loot).
+
+override_init(Name) ->
+ {ok, _, Partial} = pq_player:init({Name, [
+ {stats, [{charisma,1}, {constitution,1}, {dexterity,1},
+ {intelligence,1}, {strength,1}, {wisdom,1}]},
+ {lvlexp,1}]}),
+ regis:unregister(Name),
+ _ = read_event(),
+ {ok, _, Complete} = pq_player:init({Name, [
+ {stats, [{charisma,1}, {constitution,1}, {dexterity,1},
+ {intelligence,1}, {strength,1}, {wisdom,1}]},
+ {exp, 1}, {lvlexp,1}, {lvl,9},
+ {equip, [{weapon,{<<"plastic knife">>, -1, 1, 2}}]},
+ {money,1}, {loot, [{<<"Bacon">>, 1}]}, {bought, [helmet]}
+ ]}),
+ _ = read_event(),
+ [?_assertMatch(#state{stats=[{_,1},{_,1},{_,1},{_,1},{_,1},{_,1}],
+ lvlexp=1, lvl=1, exp=0},
+ Partial),
+ ?_assertMatch(#state{stats=[{_,1},{_,1},{_,1},{_,1},{_,1},{_,1}],
+ exp=1, lvlexp=1, lvl=9, equip=[{weapon,_}],
+ money=1, loot=[{_,_}], bought=[_]},
+ Complete)].
+
+%% Market
+sell_all(Name) ->
+ undefined = read_event(),
+ Loot = [proplists:get_value(drop, element(2, pq_enemy:fetch()))
+ || _ <- lists:seq(1,5)],
+ {[Sum1, Sum2, Sum3, Sum4, Sum5], _} = lists:mapfoldl(
+ fun(X, Sum) -> {X+Sum, X+Sum} end,
+ 0,
+ [Val || {_, Val} <- Loot]
+ ),
+ %undefined = read_event(),
+ S0 = #state{name=Name, loot=Loot, money=0, lvl=1},
+ {next_state, market, S1} = pq_player:market(sell, S0),
+ M1 = read_event(),
+ {next_state, market, S2} = pq_player:market(sell, S1),
+ M2 = read_event(),
+ {next_state, market, S3} = pq_player:market(sell, S2),
+ M3 = read_event(),
+ {next_state, market, S4} = pq_player:market(sell, S3),
+ M4 = read_event(),
+ {next_state, market, S5} = pq_player:market(sell, S4),
+ M5 = read_event(),
+ {next_state, market, S6} = pq_player:market(sell, S5),
+ M6 = read_event(),
+ [?_assertMatch(#state{money=Sum1, loot=[_,_,_,_]}, S1),
+ ?_assertMatch(#state{money=Sum2, loot=[_,_,_]}, S2),
+ ?_assertMatch(#state{money=Sum3, loot=[_,_]}, S3),
+ ?_assertMatch(#state{money=Sum4, loot=[_]}, S4),
+ ?_assertMatch(#state{money=Sum5, loot=[]}, S5),
+ ?_assertMatch(#state{money=Sum5, loot=[]}, S6),
+ ?_assertEqual([sell, sell, sell, sell, sell, buy],
+ [M1,M2,M3,M4,M5,M6])].
+
+buy_items(Name) ->
+ %% 4 different pieces of equipment to buy
+ S0 = #state{name=Name, equip=[], money=999999999999},
+ {next_state, market, S1} = pq_player:market(buy, S0),
+ M1 = read_event(),
+ {next_state, market, S2} = pq_player:market(buy, S1),
+ M2 = read_event(),
+ {next_state, market, S3} = pq_player:market(buy, S2),
+ M3 = read_event(),
+ {next_state, market, S4} = pq_player:market(buy, S3),
+ M4 = read_event(),
+ %% All slots bought. Implicit requirement: not buying for the
+ %% same slot twice.
+ {next_state, market, S5} = pq_player:market(buy, S4),
+ M5 = read_event(),
+ [?_assertEqual([S5#state.money, S4#state.money, S3#state.money,
+ S2#state.money, S1#state.money, S0#state.money],
+ lists:sort([S5#state.money, S4#state.money, S3#state.money,
+ S2#state.money, S1#state.money, S0#state.money])),
+ ?_assertEqual([1,2,3,4,4],
+ [length(L) || L <- [S1#state.equip, S2#state.equip,
+ S3#state.equip, S4#state.equip,
+ S5#state.equip]]),
+ ?_assertEqual([buy, buy, buy, buy, kill],
+ [M1, M2, M3, M4, M5])].
+
+buy_none(Name) ->
+ S0 = #state{name=Name, equip=[], money=0},
+ %% one try per part of the equipment
+ {next_state, market, S1} = pq_player:market(buy, S0),
+ _ = read_event(),
+ {next_state, market, S2} = pq_player:market(buy, S1),
+ _ = read_event(),
+ {next_state, market, S3} = pq_player:market(buy, S2),
+ _ = read_event(),
+ {next_state, market, S4} = pq_player:market(buy, S3),
+ M = read_event(),
+ [?_assertEqual(S0, S4),
+ ?_assertEqual(kill, M)].
+
+to_killing(Name) ->
+ S = #state{name=Name},
+ Res = pq_player:market(kill, S),
+ M = read_event(),
+ [?_assertMatch({next_state, killing, S}, Res),
+ ?_assertEqual(kill, M)].
+
+%% Killing fields tests
+loot_limit(Name) ->
+ S0 = #state{name=Name, stats=[{strength, 2}], loot=[]},
+ {next_state, killing, S1 = #state{loot=L1}} = pq_player:killing(kill, S0),
+ M1 = read_event(),
+ {next_state, killing, S2 = #state{loot=L2}} = pq_player:killing(kill, S1),
+ M2 = read_event(),
+ {next_state, killing, S3 = #state{loot=L3}} = pq_player:killing(kill, S2),
+ M3 = read_event(),
+ {next_state, killing, #state{loot=L4}} = pq_player:killing(kill, S3),
+ M4 = read_event(),
+ %% Group identical drops with a counter?
+ [?_assertEqual([1,2,3,4], [length(L) || L <- [L1, L2, L3, L4]]),
+ ?_assertEqual([kill, kill, kill, market], [M1, M2, M3, M4])].
+
+kill_xp(Name) ->
+ S0 = #state{name=Name, stats=[{strength, 999}|pq_stats:initial_roll()],
+ lvl=1, exp=0, lvlexp=5},
+ %% between 1 and 5 kills required to lvl up.
+ {next_state, NS1, S1} = pq_player:killing(kill, S0),
+ M1 = read_event(),
+ {next_state, NS2, S2} = pq_player:NS1(M1, S1),
+ M2 = read_event(),
+ {next_state, NS3, S3} = pq_player:NS2(M2, S2),
+ M3 = read_event(),
+ {next_state, NS4, S4} = pq_player:NS3(M3, S3),
+ M4 = read_event(),
+ {next_state, NS5, S5} = pq_player:NS4(M4, S4),
+ M5 = read_event(),
+ {next_state, NS6, S6} = pq_player:NS5(M5, S5),
+ M6 = read_event(),
+ [?_assert(lists:any(fun(#state{lvl=L}) -> L > 1 end, [S1,S2,S3,S4,S5,S6])),
+ ?_assert(lists:any(fun(#state{lvlexp=L}) -> L >= 10 end, [S1,S2,S3,S4,S5,S6])),
+ ?_assert(lists:any(fun(#state{exp=E}) -> E >= 5 end, [S1,S2,S3,S4,S5,S6])),
+ ?_assert(lists:any(fun(FSMState) -> FSMState =:= killing end,
+ [NS1, NS2, NS3, NS4, NS5, NS6])),
+ ?_assert(lists:any(fun(Msg) -> Msg =:= kill end,
+ [M1, M2, M3, M4, M5, M6])),
+ ?_assert(lists:any(fun(Msg) -> Msg =:= lvl_up end,
+ [M1, M2, M3, M4, M5, M6]))].
+
+lvl_stats(Name) ->
+ {ok, _, S0} = pq_player:init({Name, []}),
+ _ = read_event(),
+ TotalStats = length(S0#state.stats),
+ {next_state, killing, S1} = pq_player:killing(lvl_up, S0),
+ _ = read_event(),
+ {next_state, killing, S2} = pq_player:killing(lvl_up, S0),
+ _ = read_event(),
+ SumInit = lists:sum([Pts || {_,Pts} <- S0#state.stats]),
+ SumS1 = lists:sum([Pts || {_,Pts} <- S1#state.stats]),
+ SumS2 = lists:sum([Pts || {_,Pts} <- S2#state.stats]),
+ [?_assert(SumS1 >= TotalStats+SumInit),
+ ?_assert(SumS2 >= TotalStats+SumInit),
+ ?_assert(SumS1 =< TotalStats*6 + SumInit),
+ ?_assert(SumS2 =< TotalStats*6 + SumInit),
+ ?_assert(S1#state.stats =/= S2#state.stats)].
+
+to_market(Name) ->
+ S = #state{name=Name},
+ Res = pq_player:killing(market, S),
+ M = read_event(),
+ [?_assertMatch({next_state, market, S}, Res),
+ ?_assertEqual(sell, M)].
+
+%%%%%%%%%%%%%%%%%%%%%%%%
+%%% HELPER FUNCTIONS %%%
+%%%%%%%%%%%%%%%%%%%%%%%%
+read_event() ->
+ receive
+ {'$gen_event', Msg} -> Msg
+ after 0 ->
+ undefined
+ end.
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/pq_stats_tests.erl b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/pq_stats_tests.erl
new file mode 100644
index 0000000..da9daa5
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/pq_stats_tests.erl
@@ -0,0 +1,28 @@
+-module(pq_stats_tests).
+-include_lib("eunit/include/eunit.hrl").
+
+all_stats_test_() ->
+ Stats = pq_stats:initial_roll(),
+ {"Checks whether all stats are returned",
+ [?_assertEqual([charisma, constitution, dexterity,
+ intelligence, strength, wisdom],
+ lists:sort(proplists:get_keys(Stats)))]}.
+
+initial_roll_test_() ->
+ Rolls = [pq_stats:initial_roll() || _ <- lists:seq(1,100)],
+ {"All die rolls are made out of 3 d6 dice",
+ %% 6 == number of stats
+ [?_assertEqual(6, length([S || {_,S} <- Stats, S >= 3, S =< 18]))
+ || Stats <- Rolls]}.
+
+initial_random_roll_test_() ->
+ Stats = [pq_stats:initial_roll() || _ <- lists:seq(1,100)],
+ {"All die rolls are random",
+ ?_assertEqual(lists:sort(Stats),
+ lists:sort(sets:to_list(sets:from_list(Stats))))}.
+
+single_die_roll_test_() ->
+ Rolls = [pq_stats:roll() || _ <- lists:seq(1,100)],
+ [?_assertEqual(100, length([N || N <- Rolls, N >= 1, N =< 6])),
+ ?_assert(1 =/= length(sets:to_list(sets:from_list(Rolls))))].
+
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/processquest_tests.erl b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/processquest_tests.erl
new file mode 100644
index 0000000..fa118b1
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/processquest_tests.erl
@@ -0,0 +1,49 @@
+-module(processquest_tests).
+-include_lib("eunit/include/eunit.hrl").
+
+%%% Integration tests verifying the whole app.
+-define(setup(Name, T), {setup, fun() -> start(Name) end, fun stop/1, fun T/1}).
+-define(setup(T), ?setup(make_ref(), T)).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%% TESTS DESCRIPTIONS %%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%
+integration_test_() ->
+ [{"A player can be started from the processquest module and monitored",
+ ?setup(subscribe)}].
+
+%%%%%%%%%%%%%%%%%%%%%%%
+%%% SETUP FUNCTIONS %%%
+%%%%%%%%%%%%%%%%%%%%%%%
+start(Name) ->
+ application:start(crypto),
+ application:start(regis),
+ application:start(processquest),
+ processquest:start_player(Name, [{time,1100}]),
+ Name.
+
+stop(Name) ->
+ processquest:stop_player(Name),
+ application:stop(processquest),
+ application:stop(regis).
+
+%%%%%%%%%%%%%%%%%%%%
+%%% ACTUAL TESTS %%%
+%%%%%%%%%%%%%%%%%%%%
+subscribe(Name) ->
+ ok = processquest:subscribe(Name, pq_events_handler, self()),
+ timer:sleep(4000),
+ Msgs = flush(),
+ [?_assertMatch([{Name, killed, _Time1, {_EnemyName1, _Props1}},
+ {Name, killed, _Time2, {_EnemyName2, _Props2}},
+ {Name, killed, _Time3, {_EnemyName3, _Props3}}],
+ Msgs)].
+
+%%%%%%%%%%%%%%%%%%%%%%%%
+%%% HELPER FUNCTIONS %%%
+%%%%%%%%%%%%%%%%%%%%%%%%
+flush() ->
+ receive
+ X -> [X | flush()]
+ after 0 -> []
+ end.
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.1.0/Emakefile b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/Emakefile
new file mode 100644
index 0000000..b203ea3
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/Emakefile
@@ -0,0 +1,2 @@
+{["src/*", "test/*"],
+ [{i,"include"}, {outdir, "ebin"}]}.
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.1.0/ebin/.this-file-intentionally-left-blank b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/ebin/.this-file-intentionally-left-blank
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/ebin/.this-file-intentionally-left-blank
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.1.0/ebin/processquest.app b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/ebin/processquest.app
new file mode 100644
index 0000000..d923479
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/ebin/processquest.app
@@ -0,0 +1,8 @@
+{application, processquest,
+ [{description, "Game inspired by the Progress Quest game (http://progressquest.com)"},
+ {vsn, "1.1.0"},
+ {mod, {processquest, []}},
+ {registered, [pq_supersup]},
+ {modules, [processquest, pq_stats, pq_enemy, pq_quest, pq_events,
+ pq_player]},
+ {applications, [stdlib, kernel, regis, crypto]}]}.
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.1.0/ebin/processquest.appup b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/ebin/processquest.appup
new file mode 100644
index 0000000..41d305c
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/ebin/processquest.appup
@@ -0,0 +1,9 @@
+{"1.1.0",
+ [{"1.0.0", [{add_module, pq_quest},
+ {load_module, pq_enemy},
+ {load_module, pq_events},
+ {update, pq_player, {advanced, []}, [pq_quest, pq_events]}]}],
+ [{"1.0.0", [{update, pq_player, {advanced, []}},
+ {delete_module, pq_quest},
+ {load_module, pq_enemy},
+ {load_module, pq_events}]}]}.
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.1.0/include/.this-file-intentionally-left-blank b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/include/.this-file-intentionally-left-blank
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/include/.this-file-intentionally-left-blank
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.1.0/priv/.this-file-intentionally-left-blank b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/priv/.this-file-intentionally-left-blank
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/priv/.this-file-intentionally-left-blank
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_enemy.erl b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_enemy.erl
new file mode 100644
index 0000000..e1c2332
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_enemy.erl
@@ -0,0 +1,32 @@
+%% Gives random enemies
+-module(pq_enemy).
+-export([fetch/0]).
+
+fetch() ->
+ L = enemies(),
+ lists:nth(random:uniform(length(L)), L).
+
+enemies() ->
+ [{<<"Spider">>, [{drop, {<<"Spider Egg">>, 1}}, {experience, 1}]},
+ {<<"Wildcat">>, [{drop, {<<"Pelt">>, 1}}, {experience, 1}]},
+ {<<"Pig">>, [{drop, {<<"Bacon">>, 1}}, {experience, 1}]},
+ {<<"Wild Pig">>, [{drop, {<<"Tasty Ribs">>, 2}}, {experience, 1}]},
+ {<<"Goblin">>, [{drop, {<<"Goblin hair">>, 1}}, {experience, 2}]},
+ {<<"Robot">>, [{drop, {<<"Chunks of Metal">>, 3}}, {experience, 2}]},
+ {<<"Factory Worker">>, [{drop, {<<"Wrench">>,2}}, {experience,1}]},
+ {<<"Carnie">>, [{drop, {<<"Cotton Candy">>,1}}, {experience,1}]},
+ {<<"Mad Beaver">>, [{drop, {<<"Wood chips">>, 2}}, {experience, 1}]},
+ {<<"Silent magpie">>, [{drop, {<<"Shiny things">>, 3}}, {experience, 1}]},
+ {<<"Great Lizard">>, [{drop, {<<"Lizard tail">>, 1}}, {experience, 1}]},
+ {<<"Cheetah">>, [{drop, {<<"Fur">>, 3}}, {experience, 4}]},
+ {<<"Radish Horse">>, [{drop, {<<"Horseradish">>,1}}, {experience, 2}]},
+ {<<"Sand Worm">>, [{drop, {<<"Spices">>,10}}, {experience, 25}]},
+ {<<"Mule">>, [{drop, {<<"Map">>, 2}}, {experience, 12}]},
+ {<<"Man Tree">>, [{drop, {<<"branch">>,1}}, {experience, 2}]},
+ {<<"Penguin Lord">>, [{drop, {<<"Penguin Egg">>,1}}, {experience, 3}]},
+ {<<"Cursed Priest">>, [{drop, {<<"Grail">>, 3}}, {experience, 5}]},
+ {<<"Bearded cow">>, [{drop, {<<"Hairy milk">>, 1}}, {experience, 6}]},
+ {<<"Hellish crow">>, [{drop, {<<"Black feather">>, 1}}, {experience, 1}]},
+ {<<"Wolverine">>, [{drop, {<<"Puddle of blood">>, 1}}, {experience, 2}]},
+ {<<"Gangsta Bear">>, [{drop, {<<"Bear Grylls">>, 3}}, {experience, 4}]}].
+
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_events.erl b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_events.erl
new file mode 100644
index 0000000..faea13e
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_events.erl
@@ -0,0 +1,53 @@
+%%% Wrapper module for the event manager of ProgressQuest players.
+%%% It adds a few functions to wrap the events and sleep on the right
+%%% scale on behalf of the pq_player process.
+-module(pq_events).
+-export([killed/3, location/3, lvl_up/5, buy/4, sell/3, quest/4]).
+-export([start_link/1, stop/1, add_handler/3, delete_handler/3, notify/2]).
+
+start_link(Name) ->
+ {ok, Pid} = gen_event:start_link(),
+ ok = regis:register({events, Name}, Pid),
+ {ok, Pid}.
+
+stop(Name) ->
+ ManagerPid = regis:whereis({events, Name}),
+ gen_event:stop(ManagerPid).
+
+add_handler(Name, Handler, Args) ->
+ ManagerPid = regis:whereis({events, Name}),
+ gen_event:add_handler(ManagerPid, Handler, Args).
+
+delete_handler(Name, Handler, Args) ->
+ ManagerPid = regis:whereis({events, Name}),
+ gen_event:delete_handler(ManagerPid, Handler, Args).
+
+notify(Name, Msg) ->
+ ManagerPid = regis:whereis({events, Name}),
+ gen_event:notify(ManagerPid, Msg).
+
+killed(Name, Enemy = {_EnemyName, _Props}, Time) ->
+ notify(Name, {Name, killed, Time*2, Enemy}),
+ timer:sleep(Time*2).
+
+location(Name, Place, Time) ->
+ notify(Name, {Name, heading, Time, Place}),
+ timer:sleep(Time).
+
+lvl_up(Name, NewStats, NewLvl, NewExp, _Time) ->
+ notify(Name, {Name, lvl_up, 0, NewStats, NewLvl, NewExp}),
+ ok.
+
+buy(Name, Slot, Item, Time) ->
+ T = round(Time/2),
+ notify(Name, {Name, buy, T, Slot, Item}),
+ timer:sleep(T).
+
+sell(Name, Item, Time) ->
+ T = round(Time/5),
+ notify(Name, {Name, sell, T, Item}),
+ timer:sleep(Time).
+
+quest(Name, {Old, _}, {New, _}, _Time) ->
+ notify(Name, {Name, quest, 0, Old, New}),
+ ok.
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_market.erl b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_market.erl
new file mode 100644
index 0000000..241e9cd
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_market.erl
@@ -0,0 +1,77 @@
+%%% Can be used to obtain weapons and pieces of equipment of various types
+%%% to be equipped by the hero. The standard format is:
+%%% {Name, LevelModifier, Level, Price}.
+-module(pq_market).
+-export([helmet/2, weapon/2, shield/2, armor/2]).
+
+weapon(CombinedLvl, Money) ->
+ L = [
+ {<<"plastic knife">>, -1, 1, 2},
+ {<<"plastic knife">>, 0, 1, 3},
+ {<<"plastic knife">>, 1, 1, 5},
+ {<<"metal spoon">>, -1, 4, 3},
+ {<<"metal spoon">>, 0, 4, 4},
+ {<<"butter knife">>, -1, 6, 5},
+ {<<"butter knife">>, 0, 6, 7},
+ {<<"butter knife">>, 1, 6, 9},
+ {<<"machete">>, -1, 9, 15},
+ {<<"machete">>, 0, 9, 20},
+ {<<"machete">>, 1, 9, 25},
+ {<<"broad sword">>, -1, 12, 23},
+ {<<"broad sword">>, 0, 12, 30},
+ {<<"broad sword">>, 1, 12, 38},
+ {<<"lance">>, -1, 15, 32},
+ {<<"lance">>, 0, 15, 44},
+ {<<"lance">>, 1, 15, 57},
+ {<<"pistol">>, -1, 25, 95},
+ {<<"pistol">>, 0, 25, 105},
+ {<<"pistol">>, 1, 25, 155},
+ {<<"submachine gun">>, -1, 40, 200},
+ {<<"submachine gun">>, 0, 40, 245},
+ {<<"submachine gun">>, 1, 40, 365}
+ ],
+ first_match(fun(W = {_, Modifier, Lvl, Price}) ->
+ if Modifier+Lvl > CombinedLvl, Price =< Money -> W;
+ true -> continue
+ end
+ end, L).
+
+helmet(CombinedLvl, Money) -> pick_material(CombinedLvl, Money).
+shield(CombinedLvl, Money) -> pick_material(CombinedLvl, Money).
+armor(CombinedLvl, Money) -> pick_material(CombinedLvl, Money).
+
+pick_material(CombinedLvl, Money) ->
+ L = materials(),
+ first_match(fun(W = {_, Modifier, Lvl, Price}) ->
+ if Modifier+Lvl > CombinedLvl, Price =< Money -> W;
+ true -> continue
+ end
+ end, L).
+
+
+first_match(_, []) -> undefined;
+first_match(F, [H|T]) ->
+ case F(H) of
+ continue -> first_match(F,T);
+ Val -> Val
+ end.
+
+materials() ->
+ [{<<"wool">>, 0, 1, 25},
+ {<<"pleather">>, 0, 2, 45},
+ {<<"pleather">>, 1, 2, 50},
+ {<<"pleather">>, 2, 2, 65},
+ {<<"leather">>, -2, 7, 30},
+ {<<"leather">>, -1, 7, 35},
+ {<<"leather">>, 0, 7, 45},
+ {<<"leather">>, 2, 7, 65},
+ {<<"chain mail">>, -2, 12, 70},
+ {<<"chain mail">>, 0, 12, 85},
+ {<<"chain mail">>, 1, 12, 95},
+ {<<"chain mail">>, 2, 12, 105},
+ {<<"plate mail">>, -2, 17, 90},
+ {<<"plate mail">>, -1, 17, 95},
+ {<<"plate mail">>, 0, 17, 105},
+ {<<"plate mail">>, 1, 17, 115},
+ {<<"plate mail">>, 2, 17, 135}].
+
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_player.erl b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_player.erl
new file mode 100644
index 0000000..9cca7e0
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_player.erl
@@ -0,0 +1,216 @@
+%%% The core of ProcessQuest -- the player FSM,
+%%% acting for each of the players in the game.
+%%%
+%%% The FSM actually depends on no external events and only sends messages
+%%% to itself to prompt state changes. This is somewhat unusual as far as
+%%% gen_fsm usages go, but it's pretty useful when needing to work with
+%%% a standalone process.
+-module(pq_player).
+-behaviour(gen_fsm).
+-export([start_link/2]).
+-export([init/1, market/2, killing/2, handle_event/3, handle_sync_event/4,
+ handle_info/3, terminate/3, code_change/4]).
+
+-record(state, {name, stats, exp=0, lvlexp=1000, lvl=1,
+ equip=[], money=0, loot=[], bought=[],
+ time=0, quest}).
+
+
+%%% Possible states & events
+%%
+% sell buy
+% / | | \
+% \ ^ ^ /
+% [market]<--,
+% | |
+% done buying |
+% | bag full
+% v /
+% [killing fields]
+% / V V |
+% \ / | |
+% kill lvl up
+
+start_link(Name, Opts) ->
+ gen_fsm:start_link(?MODULE, {Name, Opts}, []).
+
+init({Name, Opts}) ->
+ %% Properly seeding stuff. If not doing this, the random module will
+ %% seed it based on a slightly unique time value. However, when starting
+ %% many processes at about the same time, the seeds can be very close
+ %% and give barely random results. The crypto:rand_bytes/1 function
+ %% allows for much better seeding.
+ <<A:32, B:32, C:32>> = crypto:rand_bytes(12),
+ random:seed({A,B,C}),
+ %% The first event, to start the FSM
+ gen_fsm:send_event(self(), kill),
+ case regis:register(Name, self()) of
+ {error, _} ->
+ {stop, name_taken};
+ ok ->
+ %% Use proplists with default values to let the user configure
+ %% all parts of the FSM's state by using the Opts proplist.
+ S = #state{
+ name=Name,
+ stats=proplists:get_value(stats, Opts, pq_stats:initial_roll()),
+ exp=proplists:get_value(exp, Opts, 0),
+ lvlexp=proplists:get_value(lvlexp, Opts, 1000),
+ lvl=proplists:get_value(lvl, Opts, 1),
+ equip=proplists:get_value(equip, Opts, []),
+ money=proplists:get_value(money, Opts, 0),
+ loot=proplists:get_value(loot, Opts, []),
+ bought=proplists:get_value(bought, Opts, []),
+ time=proplists:get_value(time, Opts, 0),
+ quest=proplists:get_value(quest, Opts, pq_quest:fetch())
+ },
+ {ok, market, S}
+ end.
+
+%% Done selling. Switch to the event where we head to the killing fields
+market(sell, S = #state{loot=[]}) ->
+ gen_fsm:send_event(self(), buy),
+ {next_state, market, S};
+%% Selling an Item we have looted to the market, for whatever value it has
+market(sell, S = #state{loot=[{Drop,Val}|T], money=M, lvl=Lvl}) ->
+ pq_events:sell(S#state.name, {Drop, Val*Lvl}, S#state.time),
+ gen_fsm:send_event(self(), sell),
+ {next_state, market, S#state{loot=T, money=M+Val*Lvl}};
+%% When done selling, buy items with your money
+market(buy, S = #state{equip=Equip, money=Money, bought=Bought}) ->
+ %% we have slots of equipment. It's useless to buy the same
+ %% kind of item time after time, so we must select one we haven't observed yet
+ case next_slot(Equip, Bought) of
+ undefined ->
+ %% when no slot is found, go to the killing field
+ gen_fsm:send_event(self(), kill),
+ {next_state, market, S#state{bought=[]}};
+ OldItem = {Slot, {_Name, Modifier, Lvl, _Price}} ->
+ %% Replace the item by a slightly better one if possible.
+ case pq_market:Slot(Modifier+Lvl, Money) of
+ undefined ->
+ market(buy, S#state{bought=[Slot|Bought]});
+ NewItem = {_, _, _, Price} ->
+ pq_events:buy(S#state.name, Slot, NewItem, S#state.time),
+ gen_fsm:send_event(self(), buy),
+ NewEquip = [{Slot, NewItem} | Equip -- [OldItem]],
+ {next_state, market, S#state{equip=NewEquip,
+ money=Money-Price,
+ bought=[Slot|Bought]}}
+ end
+ end;
+%% Heading to the killing field. State only useful as a state transition.
+market(kill, S) ->
+ pq_events:location(S#state.name, killing, S#state.time),
+ gen_fsm:send_event(self(), kill),
+ {next_state, killing, S}.
+
+%% Killing an enemy on the killing field. Taking its drop and keeping it
+%% in our loot.
+killing(kill, S = #state{loot=Loot, stats=Stats, exp=Exp, lvlexp=LvlExp,
+ quest=Quest}) ->
+ MaxSize = proplists:get_value(strength, Stats)*2,
+ {EnemyName, Props} = pq_enemy:fetch(),
+ pq_events:killed(S#state.name, {EnemyName, Props}, S#state.time),
+ Drop = {_N, _V} = proplists:get_value(drop, Props),
+ KillExp = proplists:get_value(experience, Props),
+ NewLoot = [Drop|Loot],
+ {QuestExp, NewQuest} = case check_quest(Quest) of
+ UpdatedQuest = {0, _} -> UpdatedQuest;
+ QuestBeaten = {_, NewQuest0} ->
+ pq_events:quest(S#state.name, Quest, NewQuest0, S#state.time),
+ QuestBeaten
+ end,
+ if length(NewLoot) =:= MaxSize ->
+ gen_fsm:send_event(self(), market);
+ Exp+KillExp+QuestExp >= LvlExp ->
+ gen_fsm:send_event(self(), lvl_up);
+ true ->
+ gen_fsm:send_event(self(), kill)
+ end,
+ {next_state, killing, S#state{loot=NewLoot,
+ exp=Exp+KillExp+QuestExp,
+ quest=NewQuest}};
+%% If we just leveled up, the stats get updated before we keep killing.
+killing(lvl_up, S = #state{stats=Stats, lvl=Lvl, lvlexp=LvlExp}) ->
+ NewStats = [{charisma, proplists:get_value(charisma, Stats)+pq_stats:roll()},
+ {constitution, proplists:get_value(constitution, Stats)+pq_stats:roll()},
+ {dexterity, proplists:get_value(dexterity, Stats)+pq_stats:roll()},
+ {intelligence, proplists:get_value(intelligence, Stats)+pq_stats:roll()},
+ {strength, proplists:get_value(strength, Stats)+pq_stats:roll()},
+ {wisdom, proplists:get_value(wisdom, Stats)+pq_stats:roll()}],
+ gen_fsm:send_event(self(), kill),
+ pq_events:lvl_up(S#state.name, NewStats, Lvl+1, LvlExp*2, S#state.time),
+ {next_state, killing, S#state{stats=NewStats, lvl=Lvl+1, lvlexp=LvlExp*2}};
+%% Heading to the market state transition
+killing(market, S) ->
+ pq_events:location(S#state.name, market, S#state.time),
+ gen_fsm:send_event(self(), sell),
+ {next_state, market, S}.
+
+handle_event(_Event, StateName, State) ->
+ {next_state, StateName, State}.
+
+handle_sync_event(_Event, _From, StateName, State) ->
+ {next_state, StateName, State}.
+
+handle_info(_Event, StateName, State) ->
+ {next_state, StateName, State}.
+
+terminate(_Reason, _StateName, _State) ->
+ ok.
+
+code_change({down, _},
+ StateName,
+ #state{name=N, stats=S, exp=E, lvlexp=LE, lvl=L, equip=Eq,
+ money=M, loot=Lo, bought=B, time=T},
+ _Extra) ->
+ Old = {state, N, S, E, LE, L, Eq, M, Lo, B, T},
+ {ok, StateName, Old};
+code_change(_OldVsn,
+ StateName,
+ {state, Name, Stats, Exp, LvlExp, Lvl, Equip, Money, Loot,
+ Bought, Time},
+ _Extra) ->
+ State = #state{
+ name=Name, stats=Stats, exp=Exp, lvlexp=LvlExp, lvl=Lvl, equip=Equip,
+ money=Money, loot=Loot, bought=Bought, time=Time, quest=pq_quest:fetch()
+ },
+ {ok, StateName, State}.
+
+%%%%%%%%%%%%%%%
+%%% PRIVATE %%%
+%%%%%%%%%%%%%%%
+
+%% Picks a slot based on what has been seen so far, combined with the
+%% current weakest item.
+next_slot(Equip, Bought) ->
+ L = expand(Equip),
+ case lists:sort([{Mod+Lvl, Entry} || Entry = {Slot, {_, Mod, Lvl, _}} <- L,
+ not lists:member(Slot, Bought)]) of
+ [] -> undefined;
+ [{_, Entry}|_] -> Entry
+ end.
+
+expand(L) ->
+ [expand_field(armor, L),
+ expand_field(helmet, L),
+ expand_field(shield, L),
+ expand_field(weapon, L)].
+
+expand_field(F, L) ->
+ {F, proplists:get_value(F, L, {undefined,0,0,0})}.
+
+%% Checks quests, if they are ready for the next level or not
+check_quest({Name, Props}) ->
+ case proplists:get_value(kills, Props) of
+ 1 ->
+ case pq_quest:fetch() of
+ %% Same name, we want new stuff!
+ {Name, _} -> check_quest({Name, Props});
+ NewQuest ->
+ Exp = proplists:get_value(experience, Props),
+ {Exp, NewQuest}
+ end;
+ Q ->
+ {0, {Name, [{kills,Q-1} | Props--[{kills,Q}]]}}
+ end.
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_quest.erl b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_quest.erl
new file mode 100644
index 0000000..b8294fb
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_quest.erl
@@ -0,0 +1,15 @@
+-module(pq_quest).
+-export([fetch/0]).
+
+fetch() ->
+ L = quests(),
+ lists:nth(random:uniform(length(L)), L).
+
+quests() ->
+ [{<<"Fetch me a nut">>, [{experience, 150}, {kills, 20}]},
+ {<<"Cancel the festival">>, [{experience, 65}, {kills, 8}]},
+ {<<"Summon the dragon">>, [{experience, 1000}, {kills, 100}]},
+ {<<"Meet the invisible man">>, [{experience, 200}, {kills, 25}]},
+ {<<"Find quest ideas">>, [{experience, 340}, {kills, 32}]},
+ {<<"Invent maple syrup">>, [{experience, 1500}, {kills, 175}]},
+ {<<"Slay the Bieber">>, [{experience, 500}, {kills, 45}]}].
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_stats.erl b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_stats.erl
new file mode 100644
index 0000000..379f5e9
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_stats.erl
@@ -0,0 +1,19 @@
+%%% Rolls dice to generate statistics or level increases for a character
+-module(pq_stats).
+-export([initial_roll/0, roll/0]).
+
+%% First roll, when setting the stats up for the first time
+initial_roll() ->
+ [{charisma, roll(3)},
+ {constitution, roll(3)},
+ {dexterity, roll(3)},
+ {intelligence, roll(3)},
+ {strength, roll(3)},
+ {wisdom, roll(3)}].
+
+%% Rolls a single die. Used when leveling up
+roll() -> roll(1).
+
+%% Rolls Num 6-faced dice
+roll(Num) ->
+ lists:sum([random:uniform(6) || _ <- lists:seq(1,Num)]).
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_sup.erl b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_sup.erl
new file mode 100644
index 0000000..de89a9a
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_sup.erl
@@ -0,0 +1,31 @@
+%%% Supervisor for each player. Goes over a pair of a
+%%% gen_fsm (pq_player) and gen_event (pq_events).
+-module(pq_sup).
+-behaviour(supervisor).
+-export([start_link/2]).
+-export([init/1]).
+
+
+start_link(Name, Info) ->
+ supervisor:start_link(?MODULE, {Name,Info}).
+
+%% The name is passed to the events so that
+%% it can register itself as {events, Name} into the
+%% 'regis' regsitry app.
+%% Same for pq_player, which also gets the info.
+%%
+%% It is important that pq_events is started before
+%% pq_player, otherwise we might create race conditions
+%% when starting a player and then quickly generating events to
+%% an event manager that doesn't exist.
+init({Name, Info}) ->
+ {ok,
+ {{one_for_all, 2, 3600},
+ [{events,
+ {pq_events, start_link, [Name]},
+ permanent, 5000, worker, [dynamic]},
+ {player,
+ {pq_player, start_link, [Name, Info]},
+ permanent, 2000, worker, [pq_player]}]}}.
+
+
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_supersup.erl b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_supersup.erl
new file mode 100644
index 0000000..577bbed
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_supersup.erl
@@ -0,0 +1,28 @@
+%%% pq_supersup is the ProcessQuest top-level supervisor.
+%%% It sits over many pq_sup instances, allowing to have
+%%% a truckload of different players running at once.
+-module(pq_supersup).
+-behaviour(supervisor).
+-export([start_link/0, start_player/2, stop_player/1]).
+-export([init/1]).
+
+%% We register it so that it's guaranteed to be unique
+start_link() ->
+ supervisor:start_link({local, ?MODULE}, ?MODULE, []).
+
+%% Using a SOFO strategy because we get to have many
+%% supervisees of the same type.
+init([]) ->
+ {ok,
+ {{simple_one_for_one, 1, 60000},
+ [{sup,
+ {pq_sup, start_link, []},
+ permanent, infinity, supervisor, [pq_sup]}]}}.
+
+%% Starts an individual player
+start_player(Name, Info) ->
+ supervisor:start_child(?MODULE, [Name, Info]).
+
+%% Stops a player.
+stop_player(Name) ->
+ supervisor:terminate_child(?MODULE, regis:whereis(Name)).
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/processquest.erl b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/processquest.erl
new file mode 100644
index 0000000..469dcb0
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/processquest.erl
@@ -0,0 +1,39 @@
+%%% ProcessQuest's main wrapping module.
+%%% Start ProcessQuest by calling application:start(processquest).
+%%% Create a player by calling processquest:start_player(Name, Info).
+%%% - Name is any term to identify the player
+%%% - Info is additional information to configure the player. Consult
+%%% the pq_player module for more info.
+%%%
+%%% You can subscribe to the player events by calling
+%%% processquest:subscribe(Name, Handler, Args).
+%%% The handler is a regular event handler. See test/pq_events_handler.erl.
+-module(processquest).
+-behaviour(application).
+-export([start/2, stop/1]).
+-export([start_player/2, stop_player/1, subscribe/3]).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%% APPLICATION CALLBACKS %%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+start(normal, []) ->
+ pq_supersup:start_link().
+
+stop(_) -> ok.
+
+%%%%%%%%%%%%%%%%%%%%%%
+%%% USER INTERFACE %%%
+%%%%%%%%%%%%%%%%%%%%%%
+
+%% Starts a player
+start_player(Name, Info) ->
+ pq_supersup:start_player(Name, Info).
+
+%% Stops a player
+stop_player(Name) ->
+ pq_supersup:stop_player(Name).
+
+%% Subscribe to user events
+subscribe(Name, Handler, Args) ->
+ pq_events:add_handler(Name, Handler, Args).
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_enemy_tests.erl b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_enemy_tests.erl
new file mode 100644
index 0000000..14742fa
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_enemy_tests.erl
@@ -0,0 +1,33 @@
+-module(pq_enemy_tests).
+-include_lib("eunit/include/eunit.hrl").
+
+is_random_test_() ->
+ F = fun(Parent, Ref) -> fun() ->
+ <<A:32,B:32,C:32>> = crypto:rand_bytes(12),
+ random:seed({A,B,C}),
+ Entries = [pq_enemy:fetch() || _ <- lists:seq(1,100)],
+ Parent ! {Ref, Entries}
+ end end,
+ Refs = [begin
+ Ref = make_ref(),
+ spawn_link(F(self(), Ref)),
+ Ref
+ end || _ <- lists:seq(1,3)],
+ [A,B,C] = [receive
+ {Ref, X} -> X
+ end || Ref <- Refs],
+ [?_assert(A =/= B),
+ ?_assert(A =/= C),
+ ?_assert(B =/= C)].
+
+format_test_() ->
+ [[?_assertMatch({_Name, [{drop, {_DropName, _DropVal}},
+ {experience, _Exp}]}, pq_enemy:fetch())
+ || _ <- lists:seq(1,10)],
+ begin
+ {Name, [{drop, {Drop, Val}}, {experience, Exp}]} = pq_enemy:fetch(),
+ [?_assert(is_binary(Name)),
+ ?_assert(is_binary(Drop)),
+ ?_assert(is_integer(Val)),
+ ?_assert(is_integer(Exp))]
+ end].
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_events_handler.erl b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_events_handler.erl
new file mode 100644
index 0000000..a02df4c
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_events_handler.erl
@@ -0,0 +1,24 @@
+%% A fake event handler used for tests
+-module(pq_events_handler).
+-behaviour(gen_event).
+-export([init/1, handle_event/2, handle_call/2, handle_info/2,
+ terminate/2, code_change/3]).
+
+init(Parent) -> {ok, Parent}.
+
+handle_event(E, Pid) ->
+ Pid ! E,
+ {ok, Pid}.
+
+handle_call(Req, Pid) ->
+ Pid ! Req,
+ {ok, ok, Pid}.
+
+handle_info(E, Pid) ->
+ Pid ! E,
+ {ok, Pid}.
+
+terminate(_, _) -> ok.
+
+code_change(_OldVsn, Pid, _Extra) ->
+ {ok, Pid}.
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_events_tests.erl b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_events_tests.erl
new file mode 100644
index 0000000..3a327b7
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_events_tests.erl
@@ -0,0 +1,55 @@
+-module(pq_events_tests).
+-include_lib("eunit/include/eunit.hrl").
+
+-define(setup(Name, T), {setup, fun() -> start(Name) end, fun stop/1, fun T/1}).
+-define(setup(T), ?setup(make_ref(), T)).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%% TESTS DESCRIPTIONS %%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%
+events_start_stop_reg_test_() ->
+ {"The event handler can be reached, started and stopped by using the "
+ "player's name",
+ ?setup(can_contact)}.
+
+%%%%%%%%%%%%%%%%%%%%%%%
+%%% SETUP FUNCTIONS %%%
+%%%%%%%%%%%%%%%%%%%%%%%
+start(Name) ->
+ application:start(regis),
+ {ok, Pid} = pq_events:start_link(Name),
+ unlink(Pid),
+ Name.
+
+stop(Name) ->
+ pq_events:stop(Name).
+
+%%%%%%%%%%%%%%%%%%%%
+%%% ACTUAL TESTS %%%
+%%%%%%%%%%%%%%%%%%%%
+can_contact(Name) ->
+ ok = pq_events:add_handler(Name, pq_events_handler, self()),
+ pq_events:notify(Name, hello),
+ L1 = flush(),
+ pq_events:delete_handler(Name, pq_events_handler, []),
+ pq_events:notify(Name, hello),
+ L2 = flush(),
+ [?_assertEqual([hello], L1),
+ ?_assertEqual([], L2)].
+
+%%%%%%%%%%%%%%%%%%%%%%%%
+%%% HELPER FUNCTIONS %%%
+%%%%%%%%%%%%%%%%%%%%%%%%
+flush() ->
+ receive
+ X -> [X | flush1()]
+ after 300 ->
+ []
+ end.
+
+flush1() ->
+ receive
+ X -> [X | flush1()]
+ after 0 ->
+ []
+ end.
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_market_tests.erl b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_market_tests.erl
new file mode 100644
index 0000000..d689f67
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_market_tests.erl
@@ -0,0 +1,15 @@
+-module(pq_market_tests).
+-include_lib("eunit/include/eunit.hrl").
+
+best_smallest_weapon_test_() ->
+ [?_assertMatch({<<"plastic knife">>, 0, 1, 3}, pq_market:weapon(0, 5)),
+ ?_assertMatch({<<"plastic knife">>, 1, 1, 5}, pq_market:weapon(1, 100)),
+ ?_assertMatch(undefined, pq_market:weapon(0,0)),
+ ?_assertMatch(undefined, pq_market:weapon(50000,100000000000000000000))].
+
+best_smallest_gear_test_() ->
+ [[?_assertMatch({<<"wool">>, 0, 1, 25}, pq_market:F(0, 35)),
+ ?_assertMatch({<<"pleather">>, 0, 2, 45}, pq_market:F(1, 100)),
+ ?_assertMatch(undefined, pq_market:F(0,0)),
+ ?_assertMatch(undefined, pq_market:F(50000,100000000000000000000))]
+ || F <- [helmet, shield, armor]].
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_player_tests.erl b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_player_tests.erl
new file mode 100644
index 0000000..d0f9c62
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_player_tests.erl
@@ -0,0 +1,395 @@
+-module(pq_player_tests).
+-include_lib("eunit/include/eunit.hrl").
+-record(state, {name, stats, exp=0, lvlexp=1000, lvl=1, % copied from pq_player.erl
+ equip=[], money=0, loot=[], bought=[],
+ time=0, quest}).
+
+-define(setup(Name, T), {setup, fun() -> start(Name) end, fun stop/1, fun T/1}).
+-define(setup(T), ?setup(make_ref(), T)).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%% TESTS DESCRIPTIONS %%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%
+new_player_test_() ->
+ [{"A player holds its own name in its state",
+ ?setup(initial_name)},
+ {"A new player has stats randomly rolled",
+ ?setup(initial_roll)},
+ {"A new player becomes a registered process",
+ ?setup(initial_register)},
+ {"A player has a counter for experience and a counter for "
+ "the next level (which is higher than the current exp)",
+ ?setup(initial_lvl)},
+ {"A player has a basic equipment (empty) when first going",
+ ?setup(initial_equipment)},
+ {"A new player has no money",
+ ?setup(initial_money)},
+ {"A new player has no loot",
+ ?setup(initial_loot)},
+ {"A new player has a quest given to him",
+ ?setup(initial_quest)},
+ {"The state of a player can be overriden using the info "
+ "arguments to the init function",
+ ?setup(override_init)}].
+
+market_test_() ->
+ [{"A player with N items will sell all of them to the market "
+ "and end with equivalent money, before switching to the "
+ "buying state",
+ ?setup(sell_all)},
+ {"A player with nearly infinite money will buy items available "
+ "for his money, higher than his level",
+ ?setup(buy_items)},
+ {"A player with no money or no items available for the price "
+ "range leaves for the killing fields.",
+ ?setup(buy_none)},
+ {"Receiving the kill message just forwards to the killing state",
+ ?setup(to_killing)}].
+
+killing_fields_test_() ->
+ [{"Kill enemies until the loot limit is hit. Loot is 2x Strength",
+ ?setup(loot_limit)},
+ {"Killing enemies raises XP until someone levels up",
+ ?setup(kill_xp)},
+ {"Leveling up boosts stats. The sum is higher by at least as "
+ "many points as there are fields, but not more than 6 times. "
+ "Moreover, the rolling is random.",
+ ?setup(lvl_stats)},
+ {"receiving the market message just forwards to the market state",
+ ?setup(to_market)}].
+
+quest_test_() ->
+ [{"A player who kills enough enemies to complete a quest obtains "
+ "a new, different one",
+ ?setup(quest_change)},
+ {"A player who kills an enemy sees the quest kill count change",
+ ?setup(quest_kill_count)},
+ {"A player who completes a quest obtains XP in exchange",
+ ?setup(quest_exp)}].
+
+code_change_test_() ->
+ [{"Updating", update()},
+ {"Downgrading", downgrade()}].
+
+%%%%%%%%%%%%%%%%%%%%%%%
+%%% SETUP FUNCTIONS %%%
+%%%%%%%%%%%%%%%%%%%%%%%
+start(Name) ->
+ application:start(crypto),
+ application:start(regis),
+ Pid = spawn(fun() -> timer:sleep(infinity) end),
+ regis:register({events, Name}, Pid),
+ Name.
+
+stop(Name) ->
+ exit(regis:whereis({events, Name}), kill),
+ application:stop(regis),
+ application:stop(crypto).
+
+%%%%%%%%%%%%%%%%%%%%
+%%% ACTUAL TESTS %%%
+%%%%%%%%%%%%%%%%%%%%
+
+%% Initial State
+initial_name(Name) ->
+ {ok, market, S} = pq_player:init({Name, []}),
+ M = read_event(),
+ [?_assertEqual(Name, S#state.name),
+ ?_assertEqual(M, kill)].
+
+initial_roll(Name) ->
+ {ok, _, S} = pq_player:init({Name, []}),
+ _ = read_event(),
+ ?_assertMatch([{charisma,_}, {constitution, _}, {dexterity, _},
+ {intelligence, _}, {strength, _}, {wisdom, _}],
+ lists:sort(S#state.stats)).
+
+initial_register(Ref) ->
+ {ok, _, _} = pq_player:init({Ref, []}),
+ _ = read_event(),
+ Pid = regis:whereis(Ref),
+ Ret = pq_player:init({Ref, []}),
+ _ = read_event(),
+ [?_assert(undefined =/= Pid),
+ ?_assert(is_pid(Pid)),
+ ?_assertEqual({stop, name_taken}, Ret)].
+
+initial_lvl(Name) ->
+ {ok, _, S} = pq_player:init({Name, []}),
+ _ = read_event(),
+ [?_assert(is_integer(S#state.lvlexp)),
+ ?_assert(S#state.lvlexp > 0),
+ ?_assert(S#state.exp =:= 0)]. % start at 0 exp
+
+initial_equipment(Name) ->
+ {ok, _, S} = pq_player:init({Name, []}),
+ _ = read_event(),
+ [?_assert(is_list(S#state.equip)),
+ ?_assertEqual(undefined, proplists:get_value(armor, S#state.equip)),
+ ?_assertEqual(undefined, proplists:get_value(helmet, S#state.equip)),
+ ?_assertEqual(undefined, proplists:get_value(weapon, S#state.equip)),
+ ?_assertEqual(undefined, proplists:get_value(shield, S#state.equip))].
+
+initial_money(Name) ->
+ {ok, _, S} = pq_player:init({Name, []}),
+ _ = read_event(),
+ ?_assertEqual(0, S#state.money).
+
+initial_loot(Name) ->
+ {ok, _, S} = pq_player:init({Name, []}),
+ _ = read_event(),
+ ?_assertEqual([], S#state.loot).
+
+initial_quest(Name) ->
+ {ok, _, S} = pq_player:init({Name, []}),
+ _ = read_event(),
+ ?_assertMatch({_QuestName, _Props}, S#state.quest).
+
+override_init(Name) ->
+ {ok, _, Partial} = pq_player:init({Name, [
+ {stats, [{charisma,1}, {constitution,1}, {dexterity,1},
+ {intelligence,1}, {strength,1}, {wisdom,1}]},
+ {lvlexp,1}]}),
+ regis:unregister(Name),
+ _ = read_event(),
+ {ok, _, Complete} = pq_player:init({Name, [
+ {stats, [{charisma,1}, {constitution,1}, {dexterity,1},
+ {intelligence,1}, {strength,1}, {wisdom,1}]},
+ {exp, 1}, {lvlexp,1}, {lvl,9},
+ {equip, [{weapon,{<<"plastic knife">>, -1, 1, 2}}]},
+ {money,1}, {loot, [{<<"Bacon">>, 1}]}, {bought, [helmet]},
+ {time, 1},
+ {quest, {<<"A">>, [{experience, 1},{kills,1}]}}
+ ]}),
+ _ = read_event(),
+ [?_assertMatch(#state{stats=[{_,1},{_,1},{_,1},{_,1},{_,1},{_,1}],
+ lvlexp=1, lvl=1, exp=0},
+ Partial),
+ ?_assertMatch(#state{stats=[{_,1},{_,1},{_,1},{_,1},{_,1},{_,1}],
+ exp=1, lvlexp=1, lvl=9, equip=[{weapon,_}],
+ money=1, loot=[{_,_}], bought=[_], time=1,
+ quest={<<"A">>, [_|_]}},
+ Complete)].
+
+%% Market
+sell_all(Name) ->
+ Loot = [proplists:get_value(drop, element(2, pq_enemy:fetch()))
+ || _ <- lists:seq(1,5)],
+ [{_, LV1}, {_, LV2}, {_, LV3}, {_, LV4}, {_, LV5}] = Loot,
+ LootVals = [LV1, LV2*2, LV3*3, LV4*4, LV5*5],
+ {[Sum1, Sum2, Sum3, Sum4, Sum5], _} = lists:mapfoldl(
+ fun(X, Sum) -> {X+Sum, X+Sum} end,
+ 0,
+ LootVals
+ ),
+ undefined = read_event(),
+ S0 = #state{name=Name, loot=Loot, money=0},
+ {next_state, market, S1} = pq_player:market(sell, S0),
+ M1 = read_event(),
+ {next_state, market, S2} = pq_player:market(sell, S1#state{lvl=2}),
+ M2 = read_event(),
+ {next_state, market, S3} = pq_player:market(sell, S2#state{lvl=3}),
+ M3 = read_event(),
+ {next_state, market, S4} = pq_player:market(sell, S3#state{lvl=4}),
+ M4 = read_event(),
+ {next_state, market, S5} = pq_player:market(sell, S4#state{lvl=5}),
+ M5 = read_event(),
+ {next_state, market, S6} = pq_player:market(sell, S5#state{lvl=6}),
+ M6 = read_event(),
+ [?_assertMatch(#state{money=Sum1, loot=[_,_,_,_]}, S1),
+ ?_assertMatch(#state{money=Sum2, loot=[_,_,_]}, S2),
+ ?_assertMatch(#state{money=Sum3, loot=[_,_]}, S3),
+ ?_assertMatch(#state{money=Sum4, loot=[_]}, S4),
+ ?_assertMatch(#state{money=Sum5, loot=[]}, S5),
+ ?_assertMatch(#state{money=Sum5, loot=[]}, S6),
+ ?_assertEqual([sell, sell, sell, sell, sell, buy],
+ [M1,M2,M3,M4,M5,M6])].
+
+buy_items(Name) ->
+ %% 4 different pieces of equipment to buy
+ S0 = #state{name=Name, equip=[], money=999999999999},
+ {next_state, market, S1} = pq_player:market(buy, S0),
+ M1 = read_event(),
+ {next_state, market, S2} = pq_player:market(buy, S1),
+ M2 = read_event(),
+ {next_state, market, S3} = pq_player:market(buy, S2),
+ M3 = read_event(),
+ {next_state, market, S4} = pq_player:market(buy, S3),
+ M4 = read_event(),
+ %% All slots bought. Implicit requirement: not buying for the
+ %% same slot twice.
+ {next_state, market, S5} = pq_player:market(buy, S4),
+ M5 = read_event(),
+ [?_assertEqual([S5#state.money, S4#state.money, S3#state.money,
+ S2#state.money, S1#state.money, S0#state.money],
+ lists:sort([S5#state.money, S4#state.money, S3#state.money,
+ S2#state.money, S1#state.money, S0#state.money])),
+ ?_assertEqual([1,2,3,4,4],
+ [length(L) || L <- [S1#state.equip, S2#state.equip,
+ S3#state.equip, S4#state.equip,
+ S5#state.equip]]),
+ ?_assertEqual([buy, buy, buy, buy, kill],
+ [M1, M2, M3, M4, M5])].
+
+buy_none(Name) ->
+ S0 = #state{name=Name, equip=[], money=0},
+ %% one try per part of the equipment
+ {next_state, market, S1} = pq_player:market(buy, S0),
+ _ = read_event(),
+ {next_state, market, S2} = pq_player:market(buy, S1),
+ _ = read_event(),
+ {next_state, market, S3} = pq_player:market(buy, S2),
+ _ = read_event(),
+ {next_state, market, S4} = pq_player:market(buy, S3),
+ M = read_event(),
+ [?_assertEqual(S0, S4),
+ ?_assertEqual(kill, M)].
+
+to_killing(Name) ->
+ S = #state{name=Name},
+ Res = pq_player:market(kill, S),
+ M = read_event(),
+ [?_assertMatch({next_state, killing, S}, Res),
+ ?_assertEqual(kill, M)].
+
+%% Killing fields tests
+loot_limit(Name) ->
+ S0 = #state{name=Name, stats=[{strength, 2}], loot=[],
+ quest={<<1>>,[{experience,0},{kills,10000}]}},
+ {next_state, killing, S1 = #state{loot=L1}} = pq_player:killing(kill, S0),
+ M1 = read_event(),
+ {next_state, killing, S2 = #state{loot=L2}} = pq_player:killing(kill, S1),
+ M2 = read_event(),
+ {next_state, killing, S3 = #state{loot=L3}} = pq_player:killing(kill, S2),
+ M3 = read_event(),
+ {next_state, killing, #state{loot=L4}} = pq_player:killing(kill, S3),
+ M4 = read_event(),
+ %% Group identical drops with a counter?
+ [?_assertEqual([1,2,3,4], [length(L) || L <- [L1, L2, L3, L4]]),
+ ?_assertEqual([kill, kill, kill, market], [M1, M2, M3, M4])].
+
+kill_xp(Name) ->
+ S0 = #state{name=Name, stats=[{strength, 999}|pq_stats:initial_roll()],
+ lvl=1, exp=0, lvlexp=5,
+ quest={<<1>>,[{experience,0},{kills,10000}]}},
+ %% between 1 and 5 kills required to lvl up.
+ {next_state, NS1, S1} = pq_player:killing(kill, S0),
+ M1 = read_event(),
+ {next_state, NS2, S2} = pq_player:NS1(M1, S1),
+ M2 = read_event(),
+ {next_state, NS3, S3} = pq_player:NS2(M2, S2),
+ M3 = read_event(),
+ {next_state, NS4, S4} = pq_player:NS3(M3, S3),
+ M4 = read_event(),
+ {next_state, NS5, S5} = pq_player:NS4(M4, S4),
+ M5 = read_event(),
+ {next_state, NS6, S6} = pq_player:NS5(M5, S5),
+ M6 = read_event(),
+ [?_assert(lists:any(fun(#state{lvl=L}) -> L > 1 end, [S1,S2,S3,S4,S5,S6])),
+ ?_assert(lists:any(fun(#state{lvlexp=L}) -> L >= 10 end, [S1,S2,S3,S4,S5,S6])),
+ ?_assert(lists:any(fun(#state{exp=E}) -> E >= 5 end, [S1,S2,S3,S4,S5,S6])),
+ ?_assert(lists:any(fun(FSMState) -> FSMState =:= killing end,
+ [NS1, NS2, NS3, NS4, NS5, NS6])),
+ ?_assert(lists:any(fun(Msg) -> Msg =:= kill end,
+ [M1, M2, M3, M4, M5, M6])),
+ ?_assert(lists:any(fun(Msg) -> Msg =:= lvl_up end,
+ [M1, M2, M3, M4, M5, M6]))].
+
+lvl_stats(Name) ->
+ {ok, _, S0} = pq_player:init({Name, []}),
+ _ = read_event(),
+ TotalStats = length(S0#state.stats),
+ {next_state, killing, S1} = pq_player:killing(lvl_up, S0),
+ _ = read_event(),
+ {next_state, killing, S2} = pq_player:killing(lvl_up, S0),
+ _ = read_event(),
+ SumInit = lists:sum([Pts || {_,Pts} <- S0#state.stats]),
+ SumS1 = lists:sum([Pts || {_,Pts} <- S1#state.stats]),
+ SumS2 = lists:sum([Pts || {_,Pts} <- S2#state.stats]),
+ [?_assert(SumS1 >= TotalStats+SumInit),
+ ?_assert(SumS2 >= TotalStats+SumInit),
+ ?_assert(SumS1 =< TotalStats*6 + SumInit),
+ ?_assert(SumS2 =< TotalStats*6 + SumInit),
+ ?_assert(S1#state.stats =/= S2#state.stats)].
+
+to_market(Name) ->
+ S = #state{name=Name},
+ Res = pq_player:killing(market, S),
+ M = read_event(),
+ [?_assertMatch({next_state, market, S}, Res),
+ ?_assertEqual(sell, M)].
+
+%% Quests
+quest_change(Name) ->
+ {ok, _, S0=#state{quest={N, Props}}} = pq_player:init({Name, []}),
+ _ = read_event(),
+ S = S0#state{quest={N, [{kills,1}|Props]}},
+ States = [element(3, pq_player:killing(kill, S)) || _ <- lists:seq(1,10)],
+ [read_event() || _ <- lists:seq(1,10)],
+ [?_assert(lists:all(fun(#state{quest={QName, _}}) -> QName =/= N end,
+ States))].
+
+quest_kill_count(Name) ->
+ {ok, _, S0=#state{quest={N, Props}}} = pq_player:init({Name, []}),
+ _ = read_event(),
+ S1 = S0#state{quest={N, [{kills,3}|Props]}},
+ {next_state, _, S2} = pq_player:killing(kill, S1),
+ _ = read_event(),
+ [?_assertEqual(2, proplists:get_value(kills,element(2,S2#state.quest)))].
+
+quest_exp(Name) ->
+ {ok, _, S0=#state{quest={N, _Props}}} = pq_player:init({Name, [{lvl,0},{exp,0}]}),
+ _ = read_event(),
+ S1 = S0#state{quest={N, [{kills,2},{experience, 100000}]}},
+ {next_state, _, S2 = #state{exp=E1}} = pq_player:killing(kill, S1),
+ _ = read_event(),
+ {next_state, _, #state{exp=E2}} = pq_player:killing(kill, S2),
+ _ = read_event(),
+ [?_assert(E1 < 100000),
+ ?_assert(E2 > 100000)].
+
+%% Update/Downgrade
+
+update() ->
+ Stats = [{charisma,1}, {constitution,1}, {dexterity,1},
+ {intelligence,1}, {strength,1}, {wisdom,1}],
+ OldState = {state, joe,
+ Stats,
+ 1, % exp
+ 2, % lvlexp
+ 3, % lvl
+ [{weapon,{<<"a">>, -1, 1, 2}}], % equip
+ 4, % money
+ [{<<"b">>, 1}], % loot
+ [helmet], % bought
+ 5 % time
+ },
+ ?_assertMatch({ok,
+ market,
+ #state{name=joe, stats=Stats, exp=1, lvlexp=2,
+ lvl=3, equip=[{weapon,{<<"a">>,-1,1,2}}],
+ money=4, loot=[{<<"b">>,1}], bought=[helmet],
+ quest={_, [_|_]}}},
+ pq_player:code_change("some version hash", market, OldState, none)).
+
+downgrade() ->
+ Stats = [{charisma,1}, {constitution,1}, {dexterity,1},
+ {intelligence,1}, {strength,1}, {wisdom,1}],
+ State = #state{name=joe, stats=Stats, exp=1, lvlexp=2,
+ lvl=3, equip=[{weapon,{<<"a">>,-1,1,2}}],
+ money=4, loot=[{<<"b">>,1}], bought=[helmet],
+ time=5, quest={<<"this goes">>, []}},
+ NewState = {state, joe, Stats, 1, 2, 3, [{weapon,{<<"a">>, -1, 1, 2}}],
+ 4, [{<<"b">>, 1}], [helmet], 5},
+ ?_assertMatch({ok, market, NewState},
+ pq_player:code_change({down, "old vsn hash"}, market, State, none)).
+
+%%%%%%%%%%%%%%%%%%%%%%%%
+%%% HELPER FUNCTIONS %%%
+%%%%%%%%%%%%%%%%%%%%%%%%
+read_event() ->
+ receive
+ {'$gen_event', Msg} -> Msg
+ after 0 ->
+ undefined
+ end.
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_quest_tests.erl b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_quest_tests.erl
new file mode 100644
index 0000000..4e4f98c
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_quest_tests.erl
@@ -0,0 +1,30 @@
+-module(pq_quest_tests).
+-include_lib("eunit/include/eunit.hrl").
+
+format_test_() ->
+ Quest = pq_quest:fetch(),
+ [?_assertMatch({_Name, _Props}, Quest),
+ ?_assert(is_binary(element(1,Quest))),
+ ?_assert(is_integer(proplists:get_value(kills, element(2,Quest)))),
+ ?_assert(0 < proplists:get_value(kills, element(2,Quest))),
+ ?_assert(is_integer(proplists:get_value(experience, element(2,Quest)))),
+ ?_assert(0 < proplists:get_value(experience, element(2,Quest)))].
+
+is_random_test_() ->
+ F = fun(Parent, Ref) -> fun() ->
+ <<A:32,B:32,C:32>> = crypto:rand_bytes(12),
+ random:seed({A,B,C}),
+ Entries = [pq_quest:fetch() || _ <- lists:seq(1,100)],
+ Parent ! {Ref, Entries}
+ end end,
+ Refs = [begin
+ Ref = make_ref(),
+ spawn_link(F(self(), Ref)),
+ Ref
+ end || _ <- lists:seq(1,3)],
+ [A,B,C] = [receive
+ {Ref, X} -> X
+ end || Ref <- Refs],
+ [?_assert(A =/= B),
+ ?_assert(A =/= C),
+ ?_assert(B =/= C)].
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_stats_tests.erl b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_stats_tests.erl
new file mode 100644
index 0000000..da9daa5
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_stats_tests.erl
@@ -0,0 +1,28 @@
+-module(pq_stats_tests).
+-include_lib("eunit/include/eunit.hrl").
+
+all_stats_test_() ->
+ Stats = pq_stats:initial_roll(),
+ {"Checks whether all stats are returned",
+ [?_assertEqual([charisma, constitution, dexterity,
+ intelligence, strength, wisdom],
+ lists:sort(proplists:get_keys(Stats)))]}.
+
+initial_roll_test_() ->
+ Rolls = [pq_stats:initial_roll() || _ <- lists:seq(1,100)],
+ {"All die rolls are made out of 3 d6 dice",
+ %% 6 == number of stats
+ [?_assertEqual(6, length([S || {_,S} <- Stats, S >= 3, S =< 18]))
+ || Stats <- Rolls]}.
+
+initial_random_roll_test_() ->
+ Stats = [pq_stats:initial_roll() || _ <- lists:seq(1,100)],
+ {"All die rolls are random",
+ ?_assertEqual(lists:sort(Stats),
+ lists:sort(sets:to_list(sets:from_list(Stats))))}.
+
+single_die_roll_test_() ->
+ Rolls = [pq_stats:roll() || _ <- lists:seq(1,100)],
+ [?_assertEqual(100, length([N || N <- Rolls, N >= 1, N =< 6])),
+ ?_assert(1 =/= length(sets:to_list(sets:from_list(Rolls))))].
+
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/processquest_tests.erl b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/processquest_tests.erl
new file mode 100644
index 0000000..fa118b1
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/processquest_tests.erl
@@ -0,0 +1,49 @@
+-module(processquest_tests).
+-include_lib("eunit/include/eunit.hrl").
+
+%%% Integration tests verifying the whole app.
+-define(setup(Name, T), {setup, fun() -> start(Name) end, fun stop/1, fun T/1}).
+-define(setup(T), ?setup(make_ref(), T)).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%% TESTS DESCRIPTIONS %%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%
+integration_test_() ->
+ [{"A player can be started from the processquest module and monitored",
+ ?setup(subscribe)}].
+
+%%%%%%%%%%%%%%%%%%%%%%%
+%%% SETUP FUNCTIONS %%%
+%%%%%%%%%%%%%%%%%%%%%%%
+start(Name) ->
+ application:start(crypto),
+ application:start(regis),
+ application:start(processquest),
+ processquest:start_player(Name, [{time,1100}]),
+ Name.
+
+stop(Name) ->
+ processquest:stop_player(Name),
+ application:stop(processquest),
+ application:stop(regis).
+
+%%%%%%%%%%%%%%%%%%%%
+%%% ACTUAL TESTS %%%
+%%%%%%%%%%%%%%%%%%%%
+subscribe(Name) ->
+ ok = processquest:subscribe(Name, pq_events_handler, self()),
+ timer:sleep(4000),
+ Msgs = flush(),
+ [?_assertMatch([{Name, killed, _Time1, {_EnemyName1, _Props1}},
+ {Name, killed, _Time2, {_EnemyName2, _Props2}},
+ {Name, killed, _Time3, {_EnemyName3, _Props3}}],
+ Msgs)].
+
+%%%%%%%%%%%%%%%%%%%%%%%%
+%%% HELPER FUNCTIONS %%%
+%%%%%%%%%%%%%%%%%%%%%%%%
+flush() ->
+ receive
+ X -> [X | flush()]
+ after 0 -> []
+ end.
diff --git a/learn-you-some-erlang/processquest/apps/regis-1.0.0/Emakefile b/learn-you-some-erlang/processquest/apps/regis-1.0.0/Emakefile
new file mode 100644
index 0000000..b8be313
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/regis-1.0.0/Emakefile
@@ -0,0 +1,2 @@
+{["src/*", "test/*"], [{outdir, "ebin"}]}.
+
diff --git a/learn-you-some-erlang/processquest/apps/regis-1.0.0/ebin/regis.app b/learn-you-some-erlang/processquest/apps/regis-1.0.0/ebin/regis.app
new file mode 100644
index 0000000..169f6d2
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/regis-1.0.0/ebin/regis.app
@@ -0,0 +1,7 @@
+{application, regis,
+ [{description, "A non-distributed process registry"},
+ {vsn, "1.0.0"},
+ {mod, {regis, []}},
+ {registered, [regis_server]},
+ {modules, [regis, regis_sup, regis_server]},
+ {applications, [stdlib, kernel]}]}.
diff --git a/learn-you-some-erlang/processquest/apps/regis-1.0.0/src/regis.erl b/learn-you-some-erlang/processquest/apps/regis-1.0.0/src/regis.erl
new file mode 100644
index 0000000..0b37c7c
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/regis-1.0.0/src/regis.erl
@@ -0,0 +1,29 @@
+%%% Application wrapper module for regis,
+%%% a process registration application.
+%%%
+%%% This was added because the standard process registry has a precise
+%%% meaning of representing VM-global, non-dynamic processes.
+%%% However, for this, we needed dynamic names and so we had to write
+%%% one ourselves. Of course we could have used 'global' (but we
+%%% didn't see distributed Erlang yet) or 'gproc' (I don't want to
+%%% depend on external libs for this guide), so checkthem out
+%%% if you're writing your own app.
+-module(regis).
+-behaviour(application).
+-export([start/2, stop/1]).
+-export([register/2, unregister/1, whereis/1, get_names/0]).
+
+
+start(normal, []) ->
+ regis_sup:start_link().
+
+stop(_) ->
+ ok.
+
+register(Name, Pid) -> regis_server:register(Name, Pid).
+
+unregister(Name) -> regis_server:unregister(Name).
+
+whereis(Name) -> regis_server:whereis(Name).
+
+get_names() -> regis_server:get_names().
diff --git a/learn-you-some-erlang/processquest/apps/regis-1.0.0/src/regis_server.erl b/learn-you-some-erlang/processquest/apps/regis-1.0.0/src/regis_server.erl
new file mode 100644
index 0000000..a668b02
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/regis-1.0.0/src/regis_server.erl
@@ -0,0 +1,98 @@
+%%% The core of the app: the server in charge of tracking processes.
+-module(regis_server).
+-behaviour(gen_server).
+
+-export([start_link/0, stop/0, register/2, unregister/1, whereis/1,
+ get_names/0]).
+-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
+ code_change/3, terminate/2]).
+
+%% We have two indexes: one by name and one by pid, for
+%% MAXIMUM SPEED (not actually measured).
+-record(state, {pid, name}).
+
+%%%%%%%%%%%%%%%%%
+%%% INTERFACE %%%
+%%%%%%%%%%%%%%%%%
+start_link() ->
+ gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
+
+stop() ->
+ gen_server:call(?MODULE, stop).
+
+%% Give a name to a process
+register(Name, Pid) when is_pid(Pid) ->
+ gen_server:call(?MODULE, {register, Name, Pid}).
+
+%% Remove the name from a process
+unregister(Name) ->
+ gen_server:call(?MODULE, {unregister, Name}).
+
+%% Find the pid associated with a process
+whereis(Name) ->
+ gen_server:call(?MODULE, {whereis, Name}).
+
+%% Find all the names currently registered.
+get_names() ->
+ gen_server:call(?MODULE, get_names).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%% GEN_SERVER CALLBACKS %%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+init([]) ->
+ %% Using gb_trees to store items. gb_trees generally have
+ %% good overall performance.
+ {ok, #state{pid = gb_trees:empty(),
+ name = gb_trees:empty()}}.
+
+handle_call({register, Name, Pid}, _From, S = #state{pid=P, name=N}) ->
+ case {gb_trees:is_defined(Pid, P), gb_trees:is_defined(Name, N)} of
+ {true, _} ->
+ {reply, {error, already_named}, S};
+ {_, true} ->
+ {reply, {error, name_taken}, S};
+ {false, false} ->
+ Ref = erlang:monitor(process, Pid),
+ {reply, ok, S#state{pid=gb_trees:insert(Pid, {Name,Ref}, P),
+ name=gb_trees:insert(Name, {Pid,Ref}, N)}}
+ end;
+handle_call({unregister, Name}, _From, S = #state{pid=P, name=N}) ->
+ case gb_trees:lookup(Name, N) of
+ {value, {Pid,Ref}} ->
+ erlang:demonitor(Ref, [flush]),
+ {reply, ok, S#state{pid=gb_trees:delete(Pid, P),
+ name=gb_trees:delete(Name, N)}};
+ none ->
+ {reply, ok, S}
+ end;
+handle_call({whereis, Name}, _From, S = #state{name=N}) ->
+ case gb_trees:lookup(Name, N) of
+ {value, {Pid,_}} ->
+ {reply, Pid, S};
+ none ->
+ {reply, undefined, S}
+ end;
+handle_call(get_names, _From, S = #state{name=N}) ->
+ {reply, gb_trees:keys(N), S};
+handle_call(stop, _From, State) ->
+ {stop, normal, ok, State};
+handle_call(_Event, _From, State) ->
+ {noreply, State}.
+
+handle_cast(_Event, State) ->
+ {noreply, State}.
+
+handle_info({'DOWN', Ref, process, Pid, _Reason}, S = #state{pid=P,name=N}) ->
+ {value, {Name, Ref}} = gb_trees:lookup(Pid, P),
+ {noreply, S#state{pid = gb_trees:delete(Pid, P),
+ name = gb_trees:delete(Name, N)}};
+handle_info(_Event, State) ->
+ {noreply, State}.
+
+code_change(_OldVsn, State, _Extra) ->
+ {ok, State}.
+
+terminate(_Reason, _State) ->
+ ok.
+
+
diff --git a/learn-you-some-erlang/processquest/apps/regis-1.0.0/src/regis_sup.erl b/learn-you-some-erlang/processquest/apps/regis-1.0.0/src/regis_sup.erl
new file mode 100644
index 0000000..be333e6
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/regis-1.0.0/src/regis_sup.erl
@@ -0,0 +1,18 @@
+%%% The top-level supervisor of the registration
+%%% server.
+-module(regis_sup).
+-behaviour(supervisor).
+-export([start_link/0]).
+-export([init/1]).
+
+start_link() ->
+ supervisor:start_link(?MODULE, []).
+
+init([]) ->
+ {ok, {{one_for_one, 1, 3600},
+ [{server,
+ {regis_server, start_link, []},
+ permanent,
+ 500,
+ worker,
+ [regis_server]}]}}.
diff --git a/learn-you-some-erlang/processquest/apps/regis-1.0.0/test/regis_server_tests.erl b/learn-you-some-erlang/processquest/apps/regis-1.0.0/test/regis_server_tests.erl
new file mode 100644
index 0000000..40e1af0
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/regis-1.0.0/test/regis_server_tests.erl
@@ -0,0 +1,120 @@
+-module(regis_server_tests).
+-include_lib("eunit/include/eunit.hrl").
+
+-define(setup(F), {setup, fun start/0, fun stop/1, F}).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%% TESTS DESCRIPTIONS %%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+start_stop_test_() ->
+ {"The server can be started, stopped and has a registered name",
+ ?setup(fun is_registered/1)}.
+
+register_test_() ->
+ [{"A process can be registered and contacted",
+ ?setup(fun register_contact/1)},
+ {"A list of registered processes can be obtained",
+ ?setup(fun registered_list/1)},
+ {"An undefined name should return 'undefined' to crash calls",
+ ?setup(fun noregister/1)},
+ {"A process can not have two names",
+ ?setup(fun two_names_one_pid/1)},
+ {"Two processes cannot share the same name",
+ ?setup(fun two_pids_one_name/1)}].
+
+unregister_test_() ->
+ [{"A process that was registered can be registered again iff it was "
+ "unregistered between both calls",
+ ?setup(fun re_un_register/1)},
+ {"Unregistering never crashes",
+ ?setup(fun unregister_nocrash/1)},
+ {"A crash unregisters a process",
+ ?setup(fun crash_unregisters/1)}].
+
+%%%%%%%%%%%%%%%%%%%%%%%
+%%% SETUP FUNCTIONS %%%
+%%%%%%%%%%%%%%%%%%%%%%%
+start() ->
+ {ok, Pid} = regis_server:start_link(),
+ Pid.
+
+stop(_) ->
+ regis_server:stop().
+
+%%%%%%%%%%%%%%%%%%%%
+%%% ACTUAL TESTS %%%
+%%%%%%%%%%%%%%%%%%%%
+is_registered(Pid) ->
+ [?_assert(erlang:is_process_alive(Pid)),
+ ?_assertEqual(Pid, whereis(regis_server))].
+
+register_contact(_) ->
+ Pid = proc_lib:spawn_link(fun() -> callback(regcontact) end),
+ timer:sleep(15),
+ Ref = make_ref(),
+ WherePid = regis_server:whereis(regcontact),
+ regis_server:whereis(regcontact) ! {self(), Ref, hi},
+ Rec = receive
+ {Ref, hi} -> true
+ after 2000 -> false
+ end,
+ [?_assertEqual(Pid, WherePid),
+ ?_assert(Rec)].
+
+noregister(_) ->
+ [?_assertError(badarg, regis_server:whereis(make_ref()) ! hi),
+ ?_assertEqual(undefined, regis_server:whereis(make_ref()))].
+
+two_names_one_pid(_) ->
+ ok = regis_server:register(make_ref(), self()),
+ Res = regis_server:register(make_ref(), self()),
+ [?_assertEqual({error, already_named}, Res)].
+
+two_pids_one_name(_) ->
+ Pid = proc_lib:spawn(fun() -> callback(myname) end),
+ timer:sleep(15),
+ Res = regis_server:register(myname, self()),
+ exit(Pid, kill),
+ [?_assertEqual({error, name_taken}, Res)].
+
+registered_list(_) ->
+ L1 = regis_server:get_names(),
+ Pids = [spawn(fun() -> callback(N) end) || N <- lists:seq(1,15)],
+ timer:sleep(200),
+ L2 = regis_server:get_names(),
+ [exit(Pid, kill) || Pid <- Pids],
+ [?_assertEqual([], L1),
+ ?_assertEqual(lists:sort(lists:seq(1,15)), lists:sort(L2))].
+
+re_un_register(_) ->
+ Ref = make_ref(),
+ L = [regis_server:register(Ref, self()),
+ regis_server:register(make_ref(), self()),
+ regis_server:unregister(Ref),
+ regis_server:register(make_ref(), self())],
+ [?_assertEqual([ok, {error, already_named}, ok, ok], L)].
+
+unregister_nocrash(_) ->
+ ?_assertEqual(ok, regis_server:unregister(make_ref())).
+
+crash_unregisters(_) ->
+ Ref = make_ref(),
+ Pid = spawn(fun() -> callback(Ref) end),
+ timer:sleep(150),
+ Pid = regis_server:whereis(Ref),
+ exit(Pid, kill),
+ timer:sleep(95),
+ regis_server:register(Ref, self()),
+ S = regis_server:whereis(Ref),
+ Self = self(),
+ ?_assertEqual(Self, S).
+
+%%%%%%%%%%%%%%%%%%%%%%%%
+%%% HELPER FUNCTIONS %%%
+%%%%%%%%%%%%%%%%%%%%%%%%
+callback(Name) ->
+ ok = regis_server:register(Name, self()),
+ receive
+ {From, Ref, Msg} -> From ! {Ref, Msg}
+ end.
diff --git a/learn-you-some-erlang/processquest/apps/regis-1.0.0/test/regis_tests.erl b/learn-you-some-erlang/processquest/apps/regis-1.0.0/test/regis_tests.erl
new file mode 100644
index 0000000..61f5f16
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/regis-1.0.0/test/regis_tests.erl
@@ -0,0 +1,18 @@
+-module(regis_tests).
+-include_lib("eunit/include/eunit.hrl").
+
+app_test_() ->
+ {inorder,
+ [?_assert(try application:start(regis) of
+ ok -> true;
+ {error, {already_started, regis}} -> true;
+ _ -> false
+ catch
+ _:_ -> false
+ end),
+ ?_assert(try application:stop(regis) of
+ ok -> true;
+ _ -> false
+ catch
+ _:_ -> false
+ end)]}.
diff --git a/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/Emakefile b/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/Emakefile
new file mode 100644
index 0000000..b203ea3
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/Emakefile
@@ -0,0 +1,2 @@
+{["src/*", "test/*"],
+ [{i,"include"}, {outdir, "ebin"}]}.
diff --git a/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/ebin/sockserv.app b/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/ebin/sockserv.app
new file mode 100644
index 0000000..dd6d8c7
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/ebin/sockserv.app
@@ -0,0 +1,11 @@
+{application, sockserv,
+ [{description, "Socket server to forward ProcessQuest messages to a client"},
+ {vsn, "1.0.0"},
+ {mod, {sockserv, []}},
+ {registered, [sockserv_sup]},
+ {modules, [sockserv, sockserv_sup, sockserv_serv, sockserv_trans,
+ sockserv_pq_events]},
+ {applications, [stdlib, kernel, processquest]},
+ {env,
+ [{port, 8082}]}
+ ]}.
diff --git a/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/src/sockserv.erl b/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/src/sockserv.erl
new file mode 100644
index 0000000..bd82b6d
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/src/sockserv.erl
@@ -0,0 +1,13 @@
+%%% Starting the sockserv application.
+%%% The sockserv application is a lightweight
+%%% Raw socket server that can be used with telnet
+%%% to follow updates on the process quest game.
+%%% The port is defined in the app's env
+-module(sockserv).
+-behaviour(application).
+-export([start/2, stop/1]).
+
+start(normal, []) ->
+ sockserv_sup:start_link().
+
+stop(_) -> ok.
diff --git a/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/src/sockserv_pq_events.erl b/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/src/sockserv_pq_events.erl
new file mode 100644
index 0000000..c3c6705
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/src/sockserv_pq_events.erl
@@ -0,0 +1,25 @@
+%%% Converts events from a player's event manager into a
+%%% cast sent to the sockserv socket gen_server.
+-module(sockserv_pq_events).
+-behaviour(gen_event).
+-export([init/1, handle_event/2, handle_call/2, handle_info/2,
+ terminate/2, code_change/3]).
+
+init(Parent) -> {ok, Parent}.
+
+handle_event(E, Pid) ->
+ gen_server:cast(Pid, E),
+ {ok, Pid}.
+
+handle_call(Req, Pid) ->
+ Pid ! Req,
+ {ok, ok, Pid}.
+
+handle_info(E, Pid) ->
+ Pid ! E,
+ {ok, Pid}.
+
+terminate(_, _) -> ok.
+
+code_change(_OldVsn, Pid, _Extra) ->
+ {ok, Pid}.
diff --git a/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/src/sockserv_serv.erl b/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/src/sockserv_serv.erl
new file mode 100644
index 0000000..cabc608
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/src/sockserv_serv.erl
@@ -0,0 +1,153 @@
+%%% Handles socket connections, and bridges a remote server
+%%% With a progressquest game.
+-module(sockserv_serv).
+-behaviour(gen_server).
+
+-record(state, {name, % player's name
+ next, % next step, used when initializing
+ socket}). % the current socket
+
+-export([start_link/1]).
+-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
+ code_change/3, terminate/2]).
+
+-define(SOCK(Msg), {tcp, _Port, Msg}).
+-define(TIME, 800).
+-define(EXP, 50).
+
+%% The socket is passed in from sockserv_sup.
+%% It's a listen socket, as started by gen_tcp:listen/2.
+%%
+%% In Erlang, a TCP socket must be started as a listening socket first.
+%% The listening socket can then be used to listen for a connection,
+%% meant to be accepted. To do so, use gen_tcp:accept/1-2, as it is done
+%% later in this module.
+%%
+%% A single listen socket can be used by many processes, each accepting
+%% a communication. When a communication is accepted with accept/1-2,
+%% a new socket, called accept socket, is returned. This accept socket
+%% is the one that may be used to communicate with a client.
+start_link(Socket) ->
+ gen_server:start_link(?MODULE, Socket, []).
+
+init(Socket) ->
+ %% properly seeding the process
+ <<A:32, B:32, C:32>> = crypto:rand_bytes(12),
+ random:seed({A,B,C}),
+ %% Because accepting a connection is a blocking function call,
+ %% we can not do it in here. Forward to the server loop!
+ gen_server:cast(self(), accept),
+ {ok, #state{socket=Socket}}.
+
+handle_call(_E, _From, State) ->
+ {noreply, State}.
+
+%% Accepting a connection
+handle_cast(accept, S = #state{socket=ListenSocket}) ->
+ %% this is the socket acceptance mentioned earlier
+ {ok, AcceptSocket} = gen_tcp:accept(ListenSocket),
+ %% Remember that thou art dust, and to dust thou shalt return.
+ %% We want to always keep a given number of children in this app.
+ sockserv_sup:start_socket(), % a new acceptor is born, praise the lord
+ send(AcceptSocket, "What's your character's name?", []),
+ {noreply, S#state{socket=AcceptSocket, next=name}};
+%% The player has given us his name (in handle_info)
+%% so we now roll stats that might or might not satisfy
+%% said player.
+handle_cast(roll_stats, S = #state{socket=Socket}) ->
+ Roll = pq_stats:initial_roll(),
+ send(Socket,
+ "Stats for your character:~n"
+ " Charisma: ~B~n"
+ " Constitution: ~B~n"
+ " Dexterity: ~B~n"
+ " Intelligence: ~B~n"
+ " Strength: ~B~n"
+ " Wisdom: ~B~n~n"
+ "Do you agree to these? y/n~n",
+ [Points || {_Name, Points} <- lists:sort(Roll)]),
+ {noreply, S#state{next={stats, Roll}}};
+%% The player has accepted the stats! Start the game!
+handle_cast(stats_accepted, S = #state{name=Name, next={stats, Stats}}) ->
+ processquest:start_player(Name, [{stats,Stats},{time,?TIME},
+ {lvlexp, ?EXP}]),
+ processquest:subscribe(Name, sockserv_pq_events, self()),
+ {noreply, S#state{next=playing}};
+%% Events coming in from process quest
+%% We know this because all these events' tuples start with the
+%% name of the player.
+handle_cast(Event, S = #state{name=N, socket=Sock}) when element(1, Event) =:= N ->
+ [case E of
+ {wait, Time} -> timer:sleep(Time);
+ IoList -> send(Sock, IoList, [])
+ end || E <- sockserv_trans:to_str(Event)], % translate to a string
+ {noreply, S}.
+
+%% The TCP client sends the string "quit". We close the connection.
+handle_info(?SOCK("quit"++_), S) ->
+ processquest:stop_player(S#state.name),
+ {stop, normal, S};
+%% We receive a string while looking for a name -- we assume that hte
+%% string is the name.
+handle_info(?SOCK(Str), S = #state{next=name}) ->
+ Name = line(Str),
+ gen_server:cast(self(), roll_stats),
+ {noreply, S#state{name=Name, next=stats}};
+%% The user might or might not accept the stats we rolled in handle_cast
+handle_info(?SOCK(Str), S = #state{socket=Socket, next={stats, _}}) ->
+ case line(Str) of
+ "y" ->
+ gen_server:cast(self(), stats_accepted);
+ "n" ->
+ gen_server:cast(self(), roll_stats);
+ _ -> % ask again because we didn't get what we wanted
+ send(Socket, "Answer with y (yes) or n (no)", [])
+ end,
+ {noreply, S};
+handle_info(?SOCK(E), S = #state{socket=Socket}) ->
+ send(Socket, "Unexpected input: ~p~n", [E]),
+ {noreply, S};
+handle_info({tcp_closed, _Socket}, S) ->
+ {stop, normal, S};
+handle_info({tcp_error, _Socket, _}, S) ->
+ {stop, normal, S};
+handle_info(E, S) ->
+ io:format("unexpected: ~p~n", [E]),
+ {noreply, S}.
+
+code_change(_OldVsn, State, _Extra) ->
+ {ok, State}.
+
+terminate(normal, #state{socket=S}) ->
+ gen_tcp:close(S);
+terminate(_Reason, _State) ->
+ io:format("terminate reason: ~p~n", [_Reason]).
+
+%% Send a message through a socket, then make it active again.
+%% The difference between an active and a passive socket is that
+%% an active socket will send incoming data as Erlang messages, while
+%% passive sockets will require to be polled with gen_tcp:recv/2-3.
+%%
+%% Depending on the context, you might want one or the other. I chose
+%% to have active sockets because they feel somewhat easier to work
+%% with. However, one problem with active sockets is that all input
+%% is blindly changed into messages and makes it so the Erlang VM
+%% is somewhat more subject to overload. Passive sockets push this
+%% responsibility to the underlying implementation and the OS and are
+%% somewhat safer.
+%%
+%% A middle ground exists, with sockets that are 'active once'.
+%% The {active, once} option (can be set with inet:setopts or
+%% when creating the listen socket) makes it so only *one* message
+%% will be sent in active mode, and then the socket is automatically
+%% turned back to passive mode. On each message reception, we turn
+%% the socket back to {active once} as to achieve rate limiting.
+send(Socket, Str, Args) ->
+ ok = gen_tcp:send(Socket, io_lib:format(Str++"~n", Args)),
+ ok = inet:setopts(Socket, [{active, once}]),
+ ok.
+
+%% Let's get rid of the whitespace and ignore whatever's after.
+%% makes it simpler to deal with telnet.
+line(Str) ->
+ hd(string:tokens(Str, "\r\n ")).
diff --git a/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/src/sockserv_sup.erl b/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/src/sockserv_sup.erl
new file mode 100644
index 0000000..0e8cbdd
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/src/sockserv_sup.erl
@@ -0,0 +1,32 @@
+%%% The supervisor in charge of all the socket acceptors.
+-module(sockserv_sup).
+-behaviour(supervisor).
+
+-export([start_link/0, start_socket/0]).
+-export([init/1]).
+
+start_link() ->
+ supervisor:start_link({local, ?MODULE}, ?MODULE, []).
+
+init([]) ->
+ {ok, Port} = application:get_env(port),
+ %% Set the socket into {active_once} mode.
+ %% See sockserv_serv comments for more details
+ {ok, ListenSocket} = gen_tcp:listen(Port, [{active,once}, {packet,line}]),
+ spawn_link(fun empty_listeners/0),
+ {ok, {{simple_one_for_one, 60, 3600},
+ [{socket,
+ {sockserv_serv, start_link, [ListenSocket]}, % pass the socket!
+ temporary, 1000, worker, [sockserv_serv]}
+ ]}}.
+
+start_socket() ->
+ supervisor:start_child(?MODULE, []).
+
+%% Start with 20 listeners so that many multiple connections can
+%% be started at once, without serialization. In best circumstances,
+%% a process would keep the count active at all times to insure nothing
+%% bad happens over time when processes get killed too much.
+empty_listeners() ->
+ [start_socket() || _ <- lists:seq(1,20)],
+ ok.
diff --git a/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/src/sockserv_trans.erl b/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/src/sockserv_trans.erl
new file mode 100644
index 0000000..23a70c0
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/src/sockserv_trans.erl
@@ -0,0 +1,58 @@
+%%% Translates the process quest events to iolists
+%%% that can be sent over a socket.
+%%%
+%%% IO lists are lists of bytes (0..255, ASCII),
+%%% binaries and other iolists. They allow to append,
+%%% prepend and insert data in strings without re-writing
+%%% the fragments that compose them. Erlang's drivers and
+%%% IO modules accept them without an issue and are a quick,
+%%% somewhat elegant solution to immutable data structures
+%%% requiring many changes.
+-module(sockserv_trans).
+-export([to_str/1]).
+
+%% The player killed something
+to_str({_User, killed, Time, {EnemyName, Props}}) ->
+ {Drop, _} = proplists:get_value(drop, Props),
+ [["Executing a ",EnemyName, "..."],
+ {wait, Time}, % take a pause between the output values
+ ["Obtained ", Drop, "."]];
+%% Changing locations
+to_str({_Name, heading, _Time, Loc}) ->
+ [["Heading to ",
+ case Loc of
+ market -> "the marketplace to sell loot...";
+ killing -> "the killing fields..."
+ end]];
+%% Leveling up
+to_str({_Name, lvl_up, _, NewStats, NewLvl, _NewExp}) ->
+ [["Leveled up to level ", integer_to_list(NewLvl),
+ " Here are your new stats:", $\n,
+ io_lib:format(
+ " Charisma: ~B~n"
+ " Constitution: ~B~n"
+ " Dexterity: ~B~n"
+ " Intelligence: ~B~n"
+ " Strength: ~B~n"
+ " Wisdom: ~B~n~n",
+ [Points || {_, Points} <- lists:sort(NewStats)])]];
+%% Bought an item
+to_str({_Name, buy, Time, Slot, {Item, _, _, _}}) ->
+ SlotTxt = case Slot of
+ armor -> " armor";
+ weapon -> "";
+ helmet -> " helmet";
+ shield -> " shield"
+ end,
+ [["Negotiating purchase of better equipment..."],
+ {wait, Time},
+ ["Bought a ", Item, SlotTxt]];
+%% Sold an item
+to_str({_Name, sell, Time, {Item, Val}}) ->
+ [["Selling ", Item],
+ {wait, Time},
+ ["Got ", integer_to_list(Val), " bucks."]];
+%% Completed a quest
+to_str({_Name, quest, 0, Completed, New}) ->
+ [["Completed quest: ", Completed, "..."],
+ ["Obtained new quest: ", New, "."]].
diff --git a/learn-you-some-erlang/processquest/apps/sockserv-1.0.1/Emakefile b/learn-you-some-erlang/processquest/apps/sockserv-1.0.1/Emakefile
new file mode 100644
index 0000000..b203ea3
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/sockserv-1.0.1/Emakefile
@@ -0,0 +1,2 @@
+{["src/*", "test/*"],
+ [{i,"include"}, {outdir, "ebin"}]}.
diff --git a/learn-you-some-erlang/processquest/apps/sockserv-1.0.1/ebin/sockserv.app b/learn-you-some-erlang/processquest/apps/sockserv-1.0.1/ebin/sockserv.app
new file mode 100644
index 0000000..63618d0
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/sockserv-1.0.1/ebin/sockserv.app
@@ -0,0 +1,11 @@
+{application, sockserv,
+ [{description, "Socket server to forward ProcessQuest messages to a client"},
+ {vsn, "1.0.1"},
+ {mod, {sockserv, []}},
+ {registered, [sockserv_sup]},
+ {modules, [sockserv, sockserv_sup, sockserv_serv, sockserv_trans,
+ sockserv_pq_events]},
+ {applications, [stdlib, kernel, processquest]},
+ {env,
+ [{port, 8082}]}
+ ]}.
diff --git a/learn-you-some-erlang/processquest/apps/sockserv-1.0.1/ebin/sockserv.appup b/learn-you-some-erlang/processquest/apps/sockserv-1.0.1/ebin/sockserv.appup
new file mode 100644
index 0000000..5462e65
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/sockserv-1.0.1/ebin/sockserv.appup
@@ -0,0 +1,4 @@
+{"1.0.1",
+ [{"1.0.0", [{load_module, sockserv_serv}]}],
+ [{"1.0.0", [{load_module, sockserv_serv}]}]}.
+
diff --git a/learn-you-some-erlang/processquest/apps/sockserv-1.0.1/src/sockserv.erl b/learn-you-some-erlang/processquest/apps/sockserv-1.0.1/src/sockserv.erl
new file mode 100644
index 0000000..bd82b6d
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/sockserv-1.0.1/src/sockserv.erl
@@ -0,0 +1,13 @@
+%%% Starting the sockserv application.
+%%% The sockserv application is a lightweight
+%%% Raw socket server that can be used with telnet
+%%% to follow updates on the process quest game.
+%%% The port is defined in the app's env
+-module(sockserv).
+-behaviour(application).
+-export([start/2, stop/1]).
+
+start(normal, []) ->
+ sockserv_sup:start_link().
+
+stop(_) -> ok.
diff --git a/learn-you-some-erlang/processquest/apps/sockserv-1.0.1/src/sockserv_pq_events.erl b/learn-you-some-erlang/processquest/apps/sockserv-1.0.1/src/sockserv_pq_events.erl
new file mode 100644
index 0000000..c3c6705
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/sockserv-1.0.1/src/sockserv_pq_events.erl
@@ -0,0 +1,25 @@
+%%% Converts events from a player's event manager into a
+%%% cast sent to the sockserv socket gen_server.
+-module(sockserv_pq_events).
+-behaviour(gen_event).
+-export([init/1, handle_event/2, handle_call/2, handle_info/2,
+ terminate/2, code_change/3]).
+
+init(Parent) -> {ok, Parent}.
+
+handle_event(E, Pid) ->
+ gen_server:cast(Pid, E),
+ {ok, Pid}.
+
+handle_call(Req, Pid) ->
+ Pid ! Req,
+ {ok, ok, Pid}.
+
+handle_info(E, Pid) ->
+ Pid ! E,
+ {ok, Pid}.
+
+terminate(_, _) -> ok.
+
+code_change(_OldVsn, Pid, _Extra) ->
+ {ok, Pid}.
diff --git a/learn-you-some-erlang/processquest/apps/sockserv-1.0.1/src/sockserv_serv.erl b/learn-you-some-erlang/processquest/apps/sockserv-1.0.1/src/sockserv_serv.erl
new file mode 100644
index 0000000..24aed16
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/sockserv-1.0.1/src/sockserv_serv.erl
@@ -0,0 +1,154 @@
+%%% Handles socket connections, and bridges a remote server
+%%% With a progressquest game.
+-module(sockserv_serv).
+-behaviour(gen_server).
+
+-record(state, {name, % player's name
+ next, % next step, used when initializing
+ socket}). % the current socket
+
+-export([start_link/1]).
+-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
+ code_change/3, terminate/2]).
+
+-define(SOCK(Msg), {tcp, _Port, Msg}).
+-define(TIME, 800).
+-define(EXP, 50).
+
+%% The socket is passed in from sockserv_sup.
+%% It's a listen socket, as started by gen_tcp:listen/2.
+%%
+%% In Erlang, a TCP socket must be started as a listening socket first.
+%% The listening socket can then be used to listen for a connection,
+%% meant to be accepted. To do so, use gen_tcp:accept/1-2, as it is done
+%% later in this module.
+%%
+%% A single listen socket can be used by many processes, each accepting
+%% a communication. When a communication is accepted with accept/1-2,
+%% a new socket, called accept socket, is returned. This accept socket
+%% is the one that may be used to communicate with a client.
+start_link(Socket) ->
+ gen_server:start_link(?MODULE, Socket, []).
+
+init(Socket) ->
+ %% properly seeding the process
+ <<A:32, B:32, C:32>> = crypto:rand_bytes(12),
+ random:seed({A,B,C}),
+ %% Because accepting a connection is a blocking function call,
+ %% we can not do it in here. Forward to the server loop!
+ gen_server:cast(self(), accept),
+ {ok, #state{socket=Socket}}.
+
+handle_call(_E, _From, State) ->
+ {noreply, State}.
+
+%% Accepting a connection
+handle_cast(accept, S = #state{socket=ListenSocket}) ->
+ %% this is the socket acceptance mentioned earlier
+ {ok, AcceptSocket} = gen_tcp:accept(ListenSocket),
+ %% Remember that thou art dust, and to dust thou shalt return.
+ %% We want to always keep a given number of children in this app.
+ sockserv_sup:start_socket(), % a new acceptor is born, praise the lord
+ send(AcceptSocket, "What's your character's name?", []),
+ {noreply, S#state{socket=AcceptSocket, next=name}};
+%% The player has given us his name (in handle_info)
+%% so we now roll stats that might or might not satisfy
+%% said player.
+handle_cast(roll_stats, S = #state{socket=Socket}) ->
+ Roll = pq_stats:initial_roll(),
+ send(Socket,
+ "Stats for your character:~n"
+ " Charisma: ~B~n"
+ " Constitution: ~B~n"
+ " Dexterity: ~B~n"
+ " Intelligence: ~B~n"
+ " Strength: ~B~n"
+ " Wisdom: ~B~n~n"
+ "Do you agree to these? y/n~n",
+ [Points || {_Name, Points} <- lists:sort(Roll)]),
+ {noreply, S#state{next={stats, Roll}}};
+%% The player has accepted the stats! Start the game!
+handle_cast(stats_accepted, S = #state{name=Name, next={stats, Stats}}) ->
+ processquest:start_player(Name, [{stats,Stats},{time,?TIME},
+ {lvlexp, ?EXP}]),
+ processquest:subscribe(Name, sockserv_pq_events, self()),
+ {noreply, S#state{next=playing}};
+%% Events coming in from process quest
+%% We know this because all these events' tuples start with the
+%% name of the player.
+handle_cast(Event, S = #state{name=N, socket=Sock}) when element(1, Event) =:= N ->
+ [case E of
+ {wait, Time} -> timer:sleep(Time);
+ IoList -> send(Sock, IoList, [])
+ end || E <- sockserv_trans:to_str(Event)], % translate to a string
+ {noreply, S}.
+
+%% The TCP client sends the string "quit". We close the connection.
+handle_info(?SOCK("quit"++_), S) ->
+ processquest:stop_player(S#state.name),
+ gen_tcp:close(S#state.socket),
+ {stop, normal, S};
+%% We receive a string while looking for a name -- we assume that hte
+%% string is the name.
+handle_info(?SOCK(Str), S = #state{next=name}) ->
+ Name = line(Str),
+ gen_server:cast(self(), roll_stats),
+ {noreply, S#state{name=Name, next=stats}};
+%% The user might or might not accept the stats we rolled in handle_cast
+handle_info(?SOCK(Str), S = #state{socket=Socket, next={stats, _}}) ->
+ case line(Str) of
+ "y" ->
+ gen_server:cast(self(), stats_accepted);
+ "n" ->
+ gen_server:cast(self(), roll_stats);
+ _ -> % ask again because we didn't get what we wanted
+ send(Socket, "Answer with y (yes) or n (no)", [])
+ end,
+ {noreply, S};
+handle_info(?SOCK(E), S = #state{socket=Socket}) ->
+ send(Socket, "Unexpected input: ~p~n", [E]),
+ {noreply, S};
+handle_info({tcp_closed, _Socket}, S) ->
+ {stop, normal, S};
+handle_info({tcp_error, _Socket, _}, S) ->
+ {stop, normal, S};
+handle_info(E, S) ->
+ io:format("unexpected: ~p~n", [E]),
+ {noreply, S}.
+
+code_change(_OldVsn, State, _Extra) ->
+ {ok, State}.
+
+terminate(normal, _State) ->
+ ok;
+terminate(_Reason, _State) ->
+ io:format("terminate reason: ~p~n", [_Reason]).
+
+%% Send a message through a socket, then make it active again.
+%% The difference between an active and a passive socket is that
+%% an active socket will send incoming data as Erlang messages, while
+%% passive sockets will require to be polled with gen_tcp:recv/2-3.
+%%
+%% Depending on the context, you might want one or the other. I chose
+%% to have active sockets because they feel somewhat easier to work
+%% with. However, one problem with active sockets is that all input
+%% is blindly changed into messages and makes it so the Erlang VM
+%% is somewhat more subject to overload. Passive sockets push this
+%% responsibility to the underlying implementation and the OS and are
+%% somewhat safer.
+%%
+%% A middle ground exists, with sockets that are 'active once'.
+%% The {active, once} option (can be set with inet:setopts or
+%% when creating the listen socket) makes it so only *one* message
+%% will be sent in active mode, and then the socket is automatically
+%% turned back to passive mode. On each message reception, we turn
+%% the socket back to {active once} as to achieve rate limiting.
+send(Socket, Str, Args) ->
+ ok = gen_tcp:send(Socket, io_lib:format(Str++"~n", Args)),
+ ok = inet:setopts(Socket, [{active, once}]),
+ ok.
+
+%% Let's get rid of the whitespace and ignore whatever's after.
+%% makes it simpler to deal with telnet.
+line(Str) ->
+ hd(string:tokens(Str, "\r\n ")).
diff --git a/learn-you-some-erlang/processquest/apps/sockserv-1.0.1/src/sockserv_sup.erl b/learn-you-some-erlang/processquest/apps/sockserv-1.0.1/src/sockserv_sup.erl
new file mode 100644
index 0000000..0e8cbdd
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/sockserv-1.0.1/src/sockserv_sup.erl
@@ -0,0 +1,32 @@
+%%% The supervisor in charge of all the socket acceptors.
+-module(sockserv_sup).
+-behaviour(supervisor).
+
+-export([start_link/0, start_socket/0]).
+-export([init/1]).
+
+start_link() ->
+ supervisor:start_link({local, ?MODULE}, ?MODULE, []).
+
+init([]) ->
+ {ok, Port} = application:get_env(port),
+ %% Set the socket into {active_once} mode.
+ %% See sockserv_serv comments for more details
+ {ok, ListenSocket} = gen_tcp:listen(Port, [{active,once}, {packet,line}]),
+ spawn_link(fun empty_listeners/0),
+ {ok, {{simple_one_for_one, 60, 3600},
+ [{socket,
+ {sockserv_serv, start_link, [ListenSocket]}, % pass the socket!
+ temporary, 1000, worker, [sockserv_serv]}
+ ]}}.
+
+start_socket() ->
+ supervisor:start_child(?MODULE, []).
+
+%% Start with 20 listeners so that many multiple connections can
+%% be started at once, without serialization. In best circumstances,
+%% a process would keep the count active at all times to insure nothing
+%% bad happens over time when processes get killed too much.
+empty_listeners() ->
+ [start_socket() || _ <- lists:seq(1,20)],
+ ok.
diff --git a/learn-you-some-erlang/processquest/apps/sockserv-1.0.1/src/sockserv_trans.erl b/learn-you-some-erlang/processquest/apps/sockserv-1.0.1/src/sockserv_trans.erl
new file mode 100644
index 0000000..23a70c0
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/sockserv-1.0.1/src/sockserv_trans.erl
@@ -0,0 +1,58 @@
+%%% Translates the process quest events to iolists
+%%% that can be sent over a socket.
+%%%
+%%% IO lists are lists of bytes (0..255, ASCII),
+%%% binaries and other iolists. They allow to append,
+%%% prepend and insert data in strings without re-writing
+%%% the fragments that compose them. Erlang's drivers and
+%%% IO modules accept them without an issue and are a quick,
+%%% somewhat elegant solution to immutable data structures
+%%% requiring many changes.
+-module(sockserv_trans).
+-export([to_str/1]).
+
+%% The player killed something
+to_str({_User, killed, Time, {EnemyName, Props}}) ->
+ {Drop, _} = proplists:get_value(drop, Props),
+ [["Executing a ",EnemyName, "..."],
+ {wait, Time}, % take a pause between the output values
+ ["Obtained ", Drop, "."]];
+%% Changing locations
+to_str({_Name, heading, _Time, Loc}) ->
+ [["Heading to ",
+ case Loc of
+ market -> "the marketplace to sell loot...";
+ killing -> "the killing fields..."
+ end]];
+%% Leveling up
+to_str({_Name, lvl_up, _, NewStats, NewLvl, _NewExp}) ->
+ [["Leveled up to level ", integer_to_list(NewLvl),
+ " Here are your new stats:", $\n,
+ io_lib:format(
+ " Charisma: ~B~n"
+ " Constitution: ~B~n"
+ " Dexterity: ~B~n"
+ " Intelligence: ~B~n"
+ " Strength: ~B~n"
+ " Wisdom: ~B~n~n",
+ [Points || {_, Points} <- lists:sort(NewStats)])]];
+%% Bought an item
+to_str({_Name, buy, Time, Slot, {Item, _, _, _}}) ->
+ SlotTxt = case Slot of
+ armor -> " armor";
+ weapon -> "";
+ helmet -> " helmet";
+ shield -> " shield"
+ end,
+ [["Negotiating purchase of better equipment..."],
+ {wait, Time},
+ ["Bought a ", Item, SlotTxt]];
+%% Sold an item
+to_str({_Name, sell, Time, {Item, Val}}) ->
+ [["Selling ", Item],
+ {wait, Time},
+ ["Got ", integer_to_list(Val), " bucks."]];
+%% Completed a quest
+to_str({_Name, quest, 0, Completed, New}) ->
+ [["Completed quest: ", Completed, "..."],
+ ["Obtained new quest: ", New, "."]].
diff --git a/learn-you-some-erlang/processquest/processquest-1.0.0.config b/learn-you-some-erlang/processquest/processquest-1.0.0.config
new file mode 100644
index 0000000..2e62f5d
--- /dev/null
+++ b/learn-you-some-erlang/processquest/processquest-1.0.0.config
@@ -0,0 +1,21 @@
+{sys, [
+ {lib_dirs, ["/Users/ferd/code/learn-you-some-erlang/processquest/apps"]},
+ {erts, [{mod_cond, derived},
+ {app_file, strip}]},
+ {rel, "processquest", "1.0.0",
+ [kernel, stdlib, sasl, crypto, regis, sockserv, processquest]},
+ {boot_rel, "processquest"},
+ {relocatable, true},
+ {profile, embedded},
+ {app_file, strip},
+ {incl_cond, exclude},
+ {excl_app_filters, ["_tests.beam"]},
+ {excl_archive_filters, [".*"]},
+ {app, stdlib, [{incl_cond, include}]},
+ {app, kernel, [{incl_cond, include}]},
+ {app, sasl, [{incl_cond, include}]},
+ {app, crypto, [{incl_cond, include}]},
+ {app, regis, [{vsn, "1.0.0"}, {incl_cond, include}]},
+ {app, sockserv, [{vsn, "1.0.0"}, {incl_cond, include}]},
+ {app, processquest, [{vsn, "1.0.0"}, {incl_cond, include}]}
+]}.
diff --git a/learn-you-some-erlang/processquest/processquest-1.1.0.config b/learn-you-some-erlang/processquest/processquest-1.1.0.config
new file mode 100644
index 0000000..38a55f4
--- /dev/null
+++ b/learn-you-some-erlang/processquest/processquest-1.1.0.config
@@ -0,0 +1,21 @@
+{sys, [
+ {lib_dirs, ["/Users/ferd/code/learn-you-some-erlang/processquest/apps"]},
+ {erts, [{mod_cond, derived},
+ {app_file, strip}]},
+ {rel, "processquest", "1.1.0",
+ [kernel, stdlib, sasl, crypto, regis, processquest, sockserv]},
+ {boot_rel, "processquest"},
+ {relocatable, true},
+ {profile, embedded},
+ {app_file, strip},
+ {incl_cond, exclude},
+ {excl_app_filters, ["_tests.beam"]},
+ {excl_archive_filters, [".*"]},
+ {app, stdlib, [{incl_cond, include}]},
+ {app, kernel, [{incl_cond, include}]},
+ {app, sasl, [{incl_cond, include}]},
+ {app, crypto, [{incl_cond, include}]},
+ {app, regis, [{vsn, "1.0.0"}, {incl_cond, include}]},
+ {app, sockserv, [{vsn, "1.0.1"}, {incl_cond, include}]},
+ {app, processquest, [{vsn, "1.1.0"}, {incl_cond, include}]}
+]}.
diff --git a/learn-you-some-erlang/processquest/rel/.this-directory-must-be-tracked b/learn-you-some-erlang/processquest/rel/.this-directory-must-be-tracked
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/learn-you-some-erlang/processquest/rel/.this-directory-must-be-tracked
diff --git a/learn-you-some-erlang/records.erl b/learn-you-some-erlang/records.erl
new file mode 100644
index 0000000..f5a65b1
--- /dev/null
+++ b/learn-you-some-erlang/records.erl
@@ -0,0 +1,39 @@
+-module(records).
+-compile(export_all).
+-include("records.hrl").
+
+-record(robot, {name,
+ type=industrial,
+ hobbies,
+ details=[]}).
+-record(user, {id, name, group, age}).
+
+first_robot() ->
+ #robot{name="Mechatron",
+ type=handmade,
+ details=["Moved by a small man inside"]}.
+
+car_factory(CorpName) ->
+ #robot{name=CorpName, hobbies="building cars"}.
+
+%% use pattern matching to filter
+admin_panel(#user{name=Name, group=admin}) ->
+ Name ++ " is allowed!";
+admin_panel(#user{name=Name}) ->
+ Name ++ " is not allowed".
+
+%% can extend user without problem
+adult_section(U = #user{}) when U#user.age >= 18 ->
+ %% Show stuff that can't be written in such a text
+ allowed;
+adult_section(_) ->
+ %% redirect to sesame street site
+ forbidden.
+
+repairman(Rob) ->
+ Details = Rob#robot.details,
+ NewRob = Rob#robot{details=["Repaired by repairman"|Details]},
+ {repaired, NewRob}.
+
+
+included() -> #included{some_field="Some value"}.
diff --git a/learn-you-some-erlang/records.hrl b/learn-you-some-erlang/records.hrl
new file mode 100644
index 0000000..c9a0eae
--- /dev/null
+++ b/learn-you-some-erlang/records.hrl
@@ -0,0 +1,4 @@
+%% this is a .hrl (header) file.
+-record(included, {some_field,
+ some_default = "yeah!",
+ unimaginative_name}).
diff --git a/learn-you-some-erlang/recursive.erl b/learn-you-some-erlang/recursive.erl
new file mode 100644
index 0000000..96a046c
--- /dev/null
+++ b/learn-you-some-erlang/recursive.erl
@@ -0,0 +1,135 @@
+-module(recursive).
+-export([fac/1, tail_fac/1, len/1, tail_len/1, duplicate/2,
+ tail_duplicate/2, reverse/1, tail_reverse/1, sublist/2,
+ tail_sublist/2, zip/2, lenient_zip/2, tail_zip/2,
+ tail_lenient_zip/2]).
+-export([quicksort/1, lc_quicksort/1, bestest_qsort/1]).
+
+%% basic recursive factorial function
+fac(0) -> 1;
+fac(N) when N > 0 -> N*fac(N-1).
+
+%% tail recursive version of fac/1
+tail_fac(N) -> tail_fac(N,1).
+
+tail_fac(0,Acc) -> Acc;
+tail_fac(N,Acc) when N > 0 -> tail_fac(N-1,N*Acc).
+
+%% finds the len of a list
+len([]) -> 0;
+len([_|T]) -> 1 + len(T).
+
+%% tail recursive version of len/1
+tail_len(L) -> tail_len(L,0).
+
+tail_len([], Acc) -> Acc;
+tail_len([_|T], Acc) -> tail_len(T,Acc+1).
+
+%% duplicates Term N times
+duplicate(0,_) ->
+ [];
+duplicate(N,Term) when N > 0 ->
+ [Term|duplicate(N-1,Term)].
+
+%% tail recursive version of duplicate/2
+tail_duplicate(N,Term) ->
+ tail_duplicate(N,Term,[]).
+
+tail_duplicate(0,_,List) ->
+ List;
+tail_duplicate(N,Term,List) when N > 0 ->
+ tail_duplicate(N-1, Term, [Term|List]).
+
+%% reverses a list (a truly descriptive function name!)
+reverse([]) -> [];
+reverse([H|T]) -> reverse(T)++[H].
+
+
+%% tail recursive version of reverse/1
+tail_reverse(L) -> tail_reverse(L,[]).
+
+tail_reverse([],Acc) -> Acc;
+tail_reverse([H|T],Acc) -> tail_reverse(T, [H|Acc]).
+
+
+%% returns the N first elements of a list
+sublist(_,0) -> [];
+sublist([],_) -> [];
+sublist([H|T],N) when N > 0 -> [H|sublist(T,N-1)].
+
+%% tail recursive version of sublist/2
+tail_sublist(L, N) -> reverse(tail_sublist(L, N, [])).
+
+tail_sublist(_, 0, SubList) -> SubList;
+tail_sublist([], _, SubList) -> SubList;
+tail_sublist([H|T], N, SubList) when N > 0 ->
+ tail_sublist(T, N-1, [H|SubList]).
+
+%% Takes two lists [A] and [B] and returns a list of tuples
+%% with the form [{A,B}]. Both lists need to be of same lenght.
+zip([],[]) -> [];
+zip([X|Xs],[Y|Ys]) -> [{X,Y}|zip(Xs,Ys)].
+
+%% Same as zip/2, but lists can vary in lenght
+lenient_zip([],_) -> [];
+lenient_zip(_,[]) -> [];
+lenient_zip([X|Xs],[Y|Ys]) -> [{X,Y}|lenient_zip(Xs,Ys)].
+
+%% tail recursive version of zip/2
+tail_zip(X,Y) -> reverse(tail_zip(X,Y,[])).
+
+tail_zip([],[],Acc) -> Acc;
+tail_zip([X|Xs],[Y|Ys], Acc) ->
+ tail_zip(Xs,Ys, [{X,Y}|Acc]).
+
+%% tail recursive version of lenient-zip/2
+tail_lenient_zip(X,Y) -> reverse(tail_lenient_zip(X,Y,[])).
+
+tail_lenient_zip([],_,Acc) -> Acc;
+tail_lenient_zip(_,[],Acc) -> Acc;
+tail_lenient_zip([X|Xs],[Y|Ys], Acc) ->
+ tail_lenient_zip(Xs,Ys,[{X,Y}|Acc]).
+
+%% basic quicksort function.
+quicksort([]) -> [];
+quicksort([Pivot|Rest]) ->
+ {Smaller, Larger} = partition(Pivot,Rest,[],[]),
+ quicksort(Smaller) ++ [Pivot] ++ quicksort(Larger).
+
+partition(_,[], Smaller, Larger) -> {Smaller, Larger};
+partition(Pivot, [H|T], Smaller, Larger) ->
+ if H =< Pivot -> partition(Pivot, T, [H|Smaller], Larger);
+ H > Pivot -> partition(Pivot, T, Smaller, [H|Larger])
+ end.
+
+%% quicksort built with list comprehensions rather than with a
+%% partition function.
+lc_quicksort([]) -> [];
+lc_quicksort([Pivot|Rest]) ->
+ lc_quicksort([Smaller || Smaller <- Rest, Smaller =< Pivot])
+ ++ [Pivot] ++
+ lc_quicksort([Larger || Larger <- Rest, Larger > Pivot]).
+
+%% BESTEST QUICKSORT, YEAH!
+%% (This is not really the bestest quicksort, because we do not do
+%% adequate pivot selection. It is the bestest of this book, alright?
+%% Thanks to literateprograms.org for this example. Give them a visit!
+%% http://en.literateprograms.org/Quicksort_(Erlang) )
+bestest_qsort([]) -> [];
+bestest_qsort(L=[_|_]) ->
+ bestest_qsort(L, []).
+
+bestest_qsort([], Acc) -> Acc;
+bestest_qsort([Pivot|Rest], Acc) ->
+ bestest_partition(Pivot, Rest, {[], [Pivot], []}, Acc).
+
+bestest_partition(_, [], {Smaller, Equal, Larger}, Acc) ->
+ bestest_qsort(Smaller, Equal ++ bestest_qsort(Larger, Acc));
+bestest_partition(Pivot, [H|T], {Smaller, Equal, Larger}, Acc) ->
+ if H < Pivot ->
+ bestest_partition(Pivot, T, {[H|Smaller], Equal, Larger}, Acc);
+ H > Pivot ->
+ bestest_partition(Pivot, T, {Smaller, Equal, [H|Larger]}, Acc);
+ H == Pivot ->
+ bestest_partition(Pivot, T, {Smaller, [H|Equal], Larger}, Acc)
+ end.
diff --git a/learn-you-some-erlang/release/erlcount-1.0.config b/learn-you-some-erlang/release/erlcount-1.0.config
new file mode 100644
index 0000000..235ab23
--- /dev/null
+++ b/learn-you-some-erlang/release/erlcount-1.0.config
@@ -0,0 +1,20 @@
+{sys, [
+ {lib_dirs, ["/home/ferd/code/learn-you-some-erlang/release/"]},
+ {erts, [{vsn, "5.8.4"}]},
+ {rel, "erlcount", "1.0.0",
+ [kernel,
+ stdlib,
+ {ppool, permanent},
+ {erlcount, transient}
+ ]},
+ {boot_rel, "erlcount"},
+ {relocatable, true},
+ {profile, embedded},
+ {app, ppool, [{vsn, "1.0.0"},
+ {app_file, all},
+ {debug_info, keep}]},
+ {app, erlcount, [{vsn, "1.0.0"},
+ {incl_cond, include},
+ {app_file, strip},
+ {debug_info, strip}]}
+]}.
diff --git a/learn-you-some-erlang/release/erlcount-1.0.rel b/learn-you-some-erlang/release/erlcount-1.0.rel
new file mode 100644
index 0000000..aa2ce44
--- /dev/null
+++ b/learn-you-some-erlang/release/erlcount-1.0.rel
@@ -0,0 +1,7 @@
+{release,
+ {"erlcount", "1.0.0"},
+ {erts, "5.8.4"},
+ [{kernel, "2.14.4"},
+ {stdlib, "1.17.4"},
+ {ppool, "1.0.0", permanent},
+ {erlcount, "1.0.0", transient}]}.
diff --git a/learn-you-some-erlang/release/erlcount-1.0/Emakefile b/learn-you-some-erlang/release/erlcount-1.0/Emakefile
new file mode 100644
index 0000000..76e98fc
--- /dev/null
+++ b/learn-you-some-erlang/release/erlcount-1.0/Emakefile
@@ -0,0 +1,4 @@
+{"src/*", [debug_info, {i,"include/"}, {outdir, "ebin/"}]}.
+%% The TESTDIR macro assumes the file is running from the 'erlcount-1.0'
+%% directory, sitting within 'learn-you-some-erlang/'.
+{"test/*", [debug_info, {i,"include/"}, {outdir, "ebin/"}, {d, 'TESTDIR', ".."}]}.
diff --git a/learn-you-some-erlang/release/erlcount-1.0/ebin/erlcount.app b/learn-you-some-erlang/release/erlcount-1.0/ebin/erlcount.app
new file mode 100644
index 0000000..22f0f0a
--- /dev/null
+++ b/learn-you-some-erlang/release/erlcount-1.0/ebin/erlcount.app
@@ -0,0 +1,14 @@
+{application, erlcount,
+ [{vsn, "1.0.0"},
+ {description, "Run regular expressions on Erlang source files"},
+ {modules, [erlcount, erlcount_sup, erlcount_lib,
+ erlcount_dispatch, erlcount_counter]},
+ {applications, [stdlib, kernel, ppool]},
+ {registered, [erlcount]},
+ {mod, {erlcount, []}},
+ {env,
+ [{directory, "."},
+ {regex, ["if\\s.+->", "case\\s.+\\sof"]},
+ {max_files, 10}]}
+ ]}.
+
diff --git a/learn-you-some-erlang/release/erlcount-1.0/src/erlcount.erl b/learn-you-some-erlang/release/erlcount-1.0/src/erlcount.erl
new file mode 100644
index 0000000..16a9f23
--- /dev/null
+++ b/learn-you-some-erlang/release/erlcount-1.0/src/erlcount.erl
@@ -0,0 +1,9 @@
+-module(erlcount).
+-behaviour(application).
+-export([start/2, stop/1]).
+
+start(normal, _Args) ->
+ erlcount_sup:start_link().
+
+stop(_State) ->
+ ok.
diff --git a/learn-you-some-erlang/release/erlcount-1.0/src/erlcount_counter.erl b/learn-you-some-erlang/release/erlcount-1.0/src/erlcount_counter.erl
new file mode 100644
index 0000000..c42fd4d
--- /dev/null
+++ b/learn-you-some-erlang/release/erlcount-1.0/src/erlcount_counter.erl
@@ -0,0 +1,35 @@
+-module(erlcount_counter).
+-behaviour(gen_server).
+-export([start_link/4]).
+-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
+ terminate/2, code_change/3]).
+
+-record(state, {dispatcher, ref, file, re}).
+
+start_link(DispatcherPid, Ref, FileName, Regex) ->
+ gen_server:start_link(?MODULE, [DispatcherPid, Ref, FileName, Regex], []).
+
+init([DispatcherPid, Ref, FileName, Regex]) ->
+ self() ! start,
+ {ok, #state{dispatcher=DispatcherPid,
+ ref = Ref,
+ file = FileName,
+ re = Regex}}.
+
+handle_call(_Msg, _From, State) ->
+ {noreply, State}.
+
+handle_cast(_Msg, State) ->
+ {noreply, State}.
+
+handle_info(start, S = #state{re=Re, ref=Ref}) ->
+ {ok, Bin} = file:read_file(S#state.file),
+ Count = erlcount_lib:regex_count(Re, Bin),
+ erlcount_dispatch:complete(S#state.dispatcher, Re, Ref, Count),
+ {stop, normal, S}.
+
+terminate(_Reason, _State) ->
+ ok.
+
+code_change(_OldVsn, State, _Extra) ->
+ {ok, State}.
diff --git a/learn-you-some-erlang/release/erlcount-1.0/src/erlcount_dispatch.erl b/learn-you-some-erlang/release/erlcount-1.0/src/erlcount_dispatch.erl
new file mode 100644
index 0000000..c125da3
--- /dev/null
+++ b/learn-you-some-erlang/release/erlcount-1.0/src/erlcount_dispatch.erl
@@ -0,0 +1,86 @@
+-module(erlcount_dispatch).
+-behaviour(gen_fsm).
+-export([start_link/0, complete/4]).
+-export([init/1, dispatching/2, listening/2, handle_event/3,
+ handle_sync_event/4, handle_info/3, terminate/3, code_change/4]).
+
+-define(POOL, erlcount).
+-record(data, {regex=[], refs=[]}).
+
+%%% PUBLIC API
+start_link() ->
+ gen_fsm:start_link(?MODULE, [], []).
+
+complete(Pid, Regex, Ref, Count) ->
+ gen_fsm:send_all_state_event(Pid, {complete, Regex, Ref, Count}).
+
+%%% GEN_FSM
+%% Two states: dispatching and listening
+init([]) ->
+ %% Move the get_env stuff to the supervisor's init.
+ {ok, Re} = application:get_env(regex),
+ {ok, Dir} = application:get_env(directory),
+ {ok, MaxFiles} = application:get_env(max_files),
+ ppool:start_pool(?POOL, MaxFiles, {erlcount_counter, start_link, []}),
+ case lists:all(fun valid_regex/1, Re) of
+ true ->
+ %% creates a regex entry of the form [{Re, Count}]
+ self() ! {start, Dir},
+ {ok, dispatching, #data{regex=[{R,0} || R <- Re]}};
+ false ->
+ {stop, invalid_regex}
+ end.
+
+dispatching({continue, File, Continuation}, Data = #data{regex=Re, refs=Refs}) ->
+ F = fun({Regex,_Count}, NewRefs) ->
+ Ref = make_ref(),
+ ppool:async_queue(?POOL, [self(), Ref, File, Regex]),
+ [Ref|NewRefs]
+ end,
+ NewRefs = lists:foldl(F, Refs, Re),
+ gen_fsm:send_event(self(), Continuation()),
+ {next_state, dispatching, Data#data{refs = NewRefs}};
+dispatching(done, Data) ->
+ %% This is a special case. We can not assume that all messages have NOT
+ %% been received by the time we hit 'done'. As such, we directly move to
+ %% listening/2 without waiting for an external event.
+ listening(done, Data).
+
+listening(done, #data{regex=Re, refs=[]}) -> % all received!
+ [io:format("Regex ~s has ~p results~n", [R,C]) || {R, C} <- Re],
+ {stop, normal, done};
+listening(done, Data) -> % entries still missing
+ {next_state, listening, Data}.
+
+handle_event({complete, Regex, Ref, Count}, State, Data = #data{regex=Re, refs=Refs}) ->
+ {Regex, OldCount} = lists:keyfind(Regex, 1, Re),
+ NewRe = lists:keyreplace(Regex, 1, Re, {Regex, OldCount+Count}),
+ NewData = Data#data{regex=NewRe, refs=Refs--[Ref]},
+ case State of
+ dispatching ->
+ {next_state, dispatching, NewData};
+ listening ->
+ listening(done, NewData)
+ end.
+
+handle_sync_event(Event, _From, State, Data) ->
+ io:format("Unexpected event: ~p~n", [Event]),
+ {next_state, State, Data}.
+
+handle_info({start, Dir}, State, Data) ->
+ gen_fsm:send_event(self(), erlcount_lib:find_erl(Dir)),
+ {next_state, State, Data}.
+
+terminate(_Reason, _State, _Data) ->
+ init:stop().
+
+code_change(_OldVsn, State, Data, _Extra) ->
+ {ok, State, Data}.
+
+%%% PRIVATE
+valid_regex(Re) ->
+ try re:run("", Re) of
+ _ -> true
+ catch
+ error:badarg -> false
+ end.
diff --git a/learn-you-some-erlang/release/erlcount-1.0/src/erlcount_lib.erl b/learn-you-some-erlang/release/erlcount-1.0/src/erlcount_lib.erl
new file mode 100644
index 0000000..70c062f
--- /dev/null
+++ b/learn-you-some-erlang/release/erlcount-1.0/src/erlcount_lib.erl
@@ -0,0 +1,55 @@
+-module(erlcount_lib).
+-export([find_erl/1, regex_count/2]).
+-include_lib("kernel/include/file.hrl").
+
+%% Finds all files ending in .erl
+find_erl(Directory) ->
+ find_erl(Directory, queue:new()).
+
+regex_count(Re, Str) ->
+ case re:run(Str, Re, [global]) of
+ nomatch -> 0;
+ {match, List} -> length(List)
+ end.
+
+%%% Private
+%% Dispatches based on file type
+find_erl(Name, Queue) ->
+ {ok, F = #file_info{}} = file:read_file_info(Name),
+ case F#file_info.type of
+ directory -> handle_directory(Name, Queue);
+ regular -> handle_regular_file(Name, Queue);
+ _Other -> dequeue_and_run(Queue)
+ end.
+
+%% Opens directories and enqueues files in there
+handle_directory(Dir, Queue) ->
+ case file:list_dir(Dir) of
+ {ok, []} ->
+ dequeue_and_run(Queue);
+ {ok, Files} ->
+ dequeue_and_run(enqueue_many(Dir, Files, Queue))
+ end.
+
+%% Checks if the file finishes in .erl
+handle_regular_file(Name, Queue) ->
+ case filename:extension(Name) of
+ ".erl" ->
+ {continue, Name, fun() -> dequeue_and_run(Queue) end};
+ _NonErl ->
+ dequeue_and_run(Queue)
+ end.
+
+%% Pops an item from the queue and runs it.
+dequeue_and_run(Queue) ->
+ case queue:out(Queue) of
+ {empty, _} -> done;
+ {{value, File}, NewQueue} -> find_erl(File, NewQueue)
+ end.
+
+%% Adds a bunch of items to the queue.
+enqueue_many(Path, Files, Queue) ->
+ F = fun(File, Q) -> queue:in(filename:join(Path,File), Q) end,
+ lists:foldl(F, Queue, Files).
+
+
diff --git a/learn-you-some-erlang/release/erlcount-1.0/src/erlcount_sup.erl b/learn-you-some-erlang/release/erlcount-1.0/src/erlcount_sup.erl
new file mode 100644
index 0000000..b8633a3
--- /dev/null
+++ b/learn-you-some-erlang/release/erlcount-1.0/src/erlcount_sup.erl
@@ -0,0 +1,17 @@
+-module(erlcount_sup).
+-behaviour(supervisor).
+-export([start_link/0, init/1]).
+
+start_link() ->
+ supervisor:start_link(?MODULE, []).
+
+init([]) ->
+ MaxRestart = 5,
+ MaxTime = 100,
+ {ok, {{one_for_one, MaxRestart, MaxTime},
+ [{dispatch,
+ {erlcount_dispatch, start_link, []},
+ transient,
+ 60000,
+ worker,
+ [erlcount_dispatch]}]}}.
diff --git a/learn-you-some-erlang/release/erlcount-1.0/test/erlcount_tests.erl b/learn-you-some-erlang/release/erlcount-1.0/test/erlcount_tests.erl
new file mode 100644
index 0000000..7459a39
--- /dev/null
+++ b/learn-you-some-erlang/release/erlcount-1.0/test/erlcount_tests.erl
@@ -0,0 +1,35 @@
+-module(erlcount_tests).
+-include_lib("eunit/include/eunit.hrl").
+-ifndef(TESTDIR).
+%% Assumes we're running from the app's directory. We want to target the
+%% 'learn-you-some-erlang' directory.
+-define(TESTDIR, "..").
+-endif.
+
+%% NOTE:
+%% Because we do not want the tests to be bound to a specific snapshot in time
+%% of our app, we will instead compare it to an oracle built with unix
+%% commands. Users running windows sadly won't be able to run these tests.
+
+%% We'll be forcing the design to be continuation-based when it comes to
+%% reading files. This will require some explaining to the user, but will
+%% allow to show how we can read files and schedule them at the same time,
+%% but without breaking functional principles of referential transparency
+%% and while allowing specialised functions to be written in a testable manner.
+find_erl_test_() ->
+ ?_assertEqual(lists:sort(string:tokens(os:cmd("find "++?TESTDIR++" -name *.erl"), "\n")),
+ lists:sort(build_list(erlcount_lib:find_erl(?TESTDIR)))).
+
+build_list(Term) -> build_list(Term, []).
+
+build_list(done, List) -> List;
+build_list({continue, Entry, Fun}, List) ->
+ build_list(Fun(), [Entry|List]).
+
+regex_count_test_() ->
+ [?_assertEqual(5, erlcount_lib:regex_count("a", "a a a a a")),
+ ?_assertEqual(0, erlcount_lib:regex_count("o", "a a a a a")),
+ ?_assertEqual(2, erlcount_lib:regex_count("a.*", "a a a\na a a")),
+ ?_assertEqual(3, erlcount_lib:regex_count("if", "myiffun() ->\n if 1 < \" if \" -> ok;\n true -> other\n end.\n")),
+ ?_assertEqual(1, erlcount_lib:regex_count("if[\\s]{1}(?:.+)->", "myiffun() ->\n if 1 < \" if \" -> ok;\n true -> other\n end.\n")),
+ ?_assertEqual(2, erlcount_lib:regex_count("if[\\s]{1}(?:.+)->", "myiffun() ->\n if 1 < \" if \" -> ok;\n true -> other\n end,\n if true -> ok end.\n"))].
diff --git a/learn-you-some-erlang/release/erlcount-sm.config b/learn-you-some-erlang/release/erlcount-sm.config
new file mode 100644
index 0000000..6e22bf1
--- /dev/null
+++ b/learn-you-some-erlang/release/erlcount-sm.config
@@ -0,0 +1,17 @@
+{sys, [
+ {lib_dirs, ["/home/ferd/code/learn-you-some-erlang/release/"]},
+ {erts, [{mod_cond, derived},
+ {app_file, strip}]},
+ {rel, "erlcount", "1.0.0", [kernel, stdlib, ppool, erlcount]},
+ {boot_rel, "erlcount"},
+ {relocatable, true},
+ {profile, embedded},
+ {app_file, strip},
+ {debug_info, strip},
+ {incl_cond, exclude},
+ {excl_app_filters, ["_tests.beam"]},
+ {app, stdlib, [{incl_cond, include}]},
+ {app, kernel, [{incl_cond, include}]},
+ {app, ppool, [{vsn, "1.0.0"}, {incl_cond, include}]},
+ {app, erlcount, [{vsn, "1.0.0"}, {incl_cond, include}]}
+]}.
diff --git a/learn-you-some-erlang/release/ppool-1.0/Emakefile b/learn-you-some-erlang/release/ppool-1.0/Emakefile
new file mode 100644
index 0000000..8e1f951
--- /dev/null
+++ b/learn-you-some-erlang/release/ppool-1.0/Emakefile
@@ -0,0 +1,2 @@
+{"src/*", [debug_info, {i,"include/"}, {outdir, "ebin/"}]}.
+{"test/*", [debug_info, {i,"include/"}, {outdir, "ebin/"}]}.
diff --git a/learn-you-some-erlang/release/ppool-1.0/ebin/ppool.app b/learn-you-some-erlang/release/ppool-1.0/ebin/ppool.app
new file mode 100644
index 0000000..f806b7c
--- /dev/null
+++ b/learn-you-some-erlang/release/ppool-1.0/ebin/ppool.app
@@ -0,0 +1,8 @@
+{application, ppool,
+ [{vsn, "1.0.0"},
+ {description, "Run and enqueue different concurrent tasks"},
+ {modules, [ppool, ppool_serv, ppool_sup, ppool_supersup, ppool_worker_sup]},
+ {applications, [stdlib, kernel]},
+ {registered, [ppool]},
+ {mod, {ppool, []}}
+ ]}.
diff --git a/learn-you-some-erlang/release/ppool-1.0/src/ppool.erl b/learn-you-some-erlang/release/ppool-1.0/src/ppool.erl
new file mode 100644
index 0000000..5723f98
--- /dev/null
+++ b/learn-you-some-erlang/release/ppool-1.0/src/ppool.erl
@@ -0,0 +1,26 @@
+%%% API module for the pool
+-module(ppool).
+-behaviour(application).
+-export([start/2, stop/1, start_pool/3,
+ run/2, sync_queue/2, async_queue/2, stop_pool/1]).
+
+start(normal, _Args) ->
+ ppool_supersup:start_link().
+
+stop(_State) ->
+ ok.
+
+start_pool(Name, Limit, {M,F,A}) ->
+ ppool_supersup:start_pool(Name, Limit, {M,F,A}).
+
+stop_pool(Name) ->
+ ppool_supersup:stop_pool(Name).
+
+run(Name, Args) ->
+ ppool_serv:run(Name, Args).
+
+async_queue(Name, Args) ->
+ ppool_serv:async_queue(Name, Args).
+
+sync_queue(Name, Args) ->
+ ppool_serv:sync_queue(Name, Args).
diff --git a/learn-you-some-erlang/release/ppool-1.0/src/ppool_serv.erl b/learn-you-some-erlang/release/ppool-1.0/src/ppool_serv.erl
new file mode 100644
index 0000000..bfb9b93
--- /dev/null
+++ b/learn-you-some-erlang/release/ppool-1.0/src/ppool_serv.erl
@@ -0,0 +1,113 @@
+-module(ppool_serv).
+-behaviour(gen_server).
+-export([start/4, start_link/4, run/2, sync_queue/2, async_queue/2, stop/1]).
+-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
+ code_change/3, terminate/2]).
+
+%% The friendly supervisor is started dynamically!
+-define(SPEC(MFA),
+ {worker_sup,
+ {ppool_worker_sup, start_link, [MFA]},
+ temporary,
+ 10000,
+ supervisor,
+ [ppool_worker_sup]}).
+
+-record(state, {limit=0,
+ sup,
+ refs,
+ queue=queue:new()}).
+
+start(Name, Limit, Sup, MFA) when is_atom(Name), is_integer(Limit) ->
+ gen_server:start({local, Name}, ?MODULE, {Limit, MFA, Sup}, []).
+
+start_link(Name, Limit, Sup, MFA) when is_atom(Name), is_integer(Limit) ->
+ gen_server:start_link({local, Name}, ?MODULE, {Limit, MFA, Sup}, []).
+
+run(Name, Args) ->
+ gen_server:call(Name, {run, Args}).
+
+sync_queue(Name, Args) ->
+ gen_server:call(Name, {sync, Args}, infinity).
+
+async_queue(Name, Args) ->
+ gen_server:cast(Name, {async, Args}).
+
+stop(Name) ->
+ gen_server:call(Name, stop).
+
+%% Gen server
+init({Limit, MFA, Sup}) ->
+ %% We need to find the Pid of the worker supervisor from here,
+ %% but alas, this would be calling the supervisor while it waits for us!
+ self() ! {start_worker_supervisor, Sup, MFA},
+ {ok, #state{limit=Limit, refs=gb_sets:empty()}}.
+
+handle_call({run, Args}, _From, S = #state{limit=N, sup=Sup, refs=R}) when N > 0 ->
+ {ok, Pid} = supervisor:start_child(Sup, Args),
+ Ref = erlang:monitor(process, Pid),
+ {reply, {ok,Pid}, S#state{limit=N-1, refs=gb_sets:add(Ref,R)}};
+handle_call({run, _Args}, _From, S=#state{limit=N}) when N =< 0 ->
+ {reply, noalloc, S};
+
+handle_call({sync, Args}, _From, S = #state{limit=N, sup=Sup, refs=R}) when N > 0 ->
+ {ok, Pid} = supervisor:start_child(Sup, Args),
+ Ref = erlang:monitor(process, Pid),
+ {reply, {ok,Pid}, S#state{limit=N-1, refs=gb_sets:add(Ref,R)}};
+handle_call({sync, Args}, From, S = #state{queue=Q}) ->
+ {noreply, S#state{queue=queue:in({From, Args}, Q)}};
+
+handle_call(stop, _From, State) ->
+ {stop, normal, ok, State};
+handle_call(_Msg, _From, State) ->
+ {noreply, State}.
+
+
+handle_cast({async, Args}, S=#state{limit=N, sup=Sup, refs=R}) when N > 0 ->
+ {ok, Pid} = supervisor:start_child(Sup, Args),
+ Ref = erlang:monitor(process, Pid),
+ {noreply, S#state{limit=N-1, refs=gb_sets:add(Ref,R)}};
+handle_cast({async, Args}, S=#state{limit=N, queue=Q}) when N =< 0 ->
+ {noreply, S#state{queue=queue:in(Args,Q)}};
+
+handle_cast(_Msg, State) ->
+ {noreply, State}.
+
+handle_info({'DOWN', Ref, process, _Pid, _}, S = #state{refs=Refs}) ->
+%% io:format("received down msg~n"),
+ case gb_sets:is_element(Ref, Refs) of
+ true ->
+ handle_down_worker(Ref, S);
+ false -> %% Not our responsibility
+ {noreply, S}
+ end;
+handle_info({start_worker_supervisor, Sup, MFA}, S = #state{}) ->
+ {ok, Pid} = supervisor:start_child(Sup, ?SPEC(MFA)),
+ link(Pid),
+ {noreply, S#state{sup=Pid}};
+handle_info(Msg, State) ->
+ io:format("Unknown msg: ~p~n", [Msg]),
+ {noreply, State}.
+
+code_change(_OldVsn, State, _Extra) ->
+ {ok, State}.
+
+terminate(_Reason, _State) ->
+ ok.
+
+handle_down_worker(Ref, S = #state{limit=L, sup=Sup, refs=Refs}) ->
+ case queue:out(S#state.queue) of
+ {{value, {From, Args}}, Q} ->
+ {ok, Pid} = supervisor:start_child(Sup, Args),
+ NewRef = erlang:monitor(process, Pid),
+ NewRefs = gb_sets:insert(NewRef, gb_sets:delete(Ref,Refs)),
+ gen_server:reply(From, {ok, Pid}),
+ {noreply, S#state{refs=NewRefs, queue=Q}};
+ {{value, Args}, Q} ->
+ {ok, Pid} = supervisor:start_child(Sup, Args),
+ NewRef = erlang:monitor(process, Pid),
+ NewRefs = gb_sets:insert(NewRef, gb_sets:delete(Ref,Refs)),
+ {noreply, S#state{refs=NewRefs, queue=Q}};
+ {empty, _} ->
+ {noreply, S#state{limit=L+1, refs=gb_sets:delete(Ref,Refs)}}
+ end.
diff --git a/learn-you-some-erlang/release/ppool-1.0/src/ppool_sup.erl b/learn-you-some-erlang/release/ppool-1.0/src/ppool_sup.erl
new file mode 100644
index 0000000..71ec31d
--- /dev/null
+++ b/learn-you-some-erlang/release/ppool-1.0/src/ppool_sup.erl
@@ -0,0 +1,17 @@
+-module(ppool_sup).
+-export([start_link/3, init/1]).
+-behaviour(supervisor).
+
+start_link(Name, Limit, MFA) ->
+ supervisor:start_link(?MODULE, {Name, Limit, MFA}).
+
+init({Name, Limit, MFA}) ->
+ MaxRestart = 1,
+ MaxTime = 3000,
+ {ok, {{one_for_all, MaxRestart, MaxTime},
+ [{serv,
+ {ppool_serv, start_link, [Name, Limit, self(), MFA]},
+ permanent,
+ 5000,
+ worker,
+ [ppool_serv]}]}}.
diff --git a/learn-you-some-erlang/release/ppool-1.0/src/ppool_supersup.erl b/learn-you-some-erlang/release/ppool-1.0/src/ppool_supersup.erl
new file mode 100644
index 0000000..06fa0af
--- /dev/null
+++ b/learn-you-some-erlang/release/ppool-1.0/src/ppool_supersup.erl
@@ -0,0 +1,22 @@
+-module(ppool_supersup).
+-behaviour(supervisor).
+-export([start_link/0, start_pool/3, stop_pool/1]).
+-export([init/1]).
+
+start_link() ->
+ supervisor:start_link({local, ppool}, ?MODULE, []).
+
+start_pool(Name, Limit, MFA) ->
+ ChildSpec = {Name,
+ {ppool_sup, start_link, [Name, Limit, MFA]},
+ permanent, 10500, supervisor, [ppool_sup]},
+ supervisor:start_child(ppool, ChildSpec).
+
+stop_pool(Name) ->
+ supervisor:terminate_child(ppool, Name),
+ supervisor:delete_child(ppool, Name).
+
+init([]) ->
+ MaxRestart = 6,
+ MaxTime = 3000,
+ {ok, {{one_for_one, MaxRestart, MaxTime}, []}}.
diff --git a/learn-you-some-erlang/release/ppool-1.0/src/ppool_worker_sup.erl b/learn-you-some-erlang/release/ppool-1.0/src/ppool_worker_sup.erl
new file mode 100644
index 0000000..2467c47
--- /dev/null
+++ b/learn-you-some-erlang/release/ppool-1.0/src/ppool_worker_sup.erl
@@ -0,0 +1,15 @@
+-module(ppool_worker_sup).
+-export([start_link/1, init/1]).
+-behaviour(supervisor).
+
+start_link(MFA = {_,_,_}) ->
+ supervisor:start_link(?MODULE, MFA).
+
+init({M,F,A}) ->
+ MaxRestart = 5,
+ MaxTime = 3600,
+ {ok, {{simple_one_for_one, MaxRestart, MaxTime},
+ [{ppool_worker,
+ {M,F,A},
+ temporary, 5000, worker, [M]}]}}.
+
diff --git a/learn-you-some-erlang/release/ppool-1.0/test/ppool_nagger.erl b/learn-you-some-erlang/release/ppool-1.0/test/ppool_nagger.erl
new file mode 100644
index 0000000..903f821
--- /dev/null
+++ b/learn-you-some-erlang/release/ppool-1.0/test/ppool_nagger.erl
@@ -0,0 +1,50 @@
+%% demo module, a nagger for tasks,
+%% because the previous one wasn't good enough
+%%
+%% Can take:
+%% - a time delay for which to nag,
+%% - an adress to say where the messages should be sent
+%% - a message to send in the mailbox telling you what to nag,
+%% with an id to be able to call: ->
+%% - a command to say the task is done
+-module(ppool_nagger).
+-behaviour(gen_server).
+-export([start_link/4, stop/1]).
+-export([init/1, handle_call/3, handle_cast/2,
+ handle_info/2, code_change/3, terminate/2]).
+
+start_link(Task, Delay, Max, SendTo) ->
+ gen_server:start_link(?MODULE, {Task, Delay, Max, SendTo} , []).
+
+stop(Pid) ->
+ gen_server:call(Pid, stop).
+
+init({Task, Delay, Max, SendTo}) ->
+ process_flag(trap_exit, true), % for tests & terminate too
+ {ok, {Task, Delay, Max, SendTo}, Delay}.
+
+%%% OTP Callbacks
+handle_call(stop, _From, State) ->
+ {stop, normal, ok, State};
+handle_call(_Msg, _From, State) ->
+ {noreply, State}.
+
+handle_cast(_Msg, State) ->
+ {noreply, State}.
+
+handle_info(timeout, {Task, Delay, Max, SendTo}) ->
+ SendTo ! {self(), Task},
+ if Max =:= infinity ->
+ {noreply, {Task, Delay, Max, SendTo}, Delay};
+ Max =< 1 ->
+ {stop, normal, {Task, Delay, 0, SendTo}};
+ Max > 1 ->
+ {noreply, {Task, Delay, Max-1, SendTo}, Delay}
+ end;
+handle_info(_Msg, State) ->
+ {noreply, State}.
+
+code_change(_OldVsn, State, _Extra) ->
+ {ok, State}.
+
+terminate(_Reason, _State) -> ok.
diff --git a/learn-you-some-erlang/release/ppool-1.0/test/ppool_tests.erl b/learn-you-some-erlang/release/ppool-1.0/test/ppool_tests.erl
new file mode 100644
index 0000000..8f0dfe2
--- /dev/null
+++ b/learn-you-some-erlang/release/ppool-1.0/test/ppool_tests.erl
@@ -0,0 +1,200 @@
+-module(ppool_tests).
+-include_lib("eunit/include/eunit.hrl").
+-export([test_mfa/1, wait_mfa/1]).
+
+%%% All Test Fixtures
+start_test_() ->
+ {"It should be possible to start a pool server and give it a name",
+ {setup,
+ fun find_unique_name/0,
+ fun(Name) ->
+ [start_and_test_name(Name)]
+ end}}.
+
+mfa_test_() ->
+ {"A pool process can be allocated which will be ordered "
+ "to run an MFA call determined at start time, with arguments "
+ "provided at call time",
+ {setup,
+ fun start_ppool/0,
+ fun kill_ppool/1,
+ fun(Name) ->
+ [pool_run_mfa(Name)]
+ end}
+ }.
+
+alloc_test_() ->
+ {"A pool process can be allocated which will be ordered "
+ "to run a worker, only if there are enough which "
+ "haven't been ordered to run yet.",
+ {setup,
+ fun start_ppool/0,
+ fun kill_ppool/1,
+ fun(Name) ->
+ [pool_run_alloc(Name),
+ pool_run_noalloc(Name)]
+ end}
+ }.
+
+realloc_test_() ->
+ {"When an allocated process dies, "
+ "A new one can be allocated to replace it.",
+ {setup,
+ fun start_ppool/0,
+ fun kill_ppool/1,
+ fun(Name) ->
+ [pool_run_realloc(Name)]
+ end}
+ }.
+
+queue_test_() ->
+ {"The queue function can be used to run the function as soon as possible. "
+ "If no space is available, the worker call is added to the queue.",
+ {foreach,
+ fun start_ppool/0,
+ fun kill_ppool/1,
+ [fun(Name) -> test_async_queue(Name) end,
+ fun(Name) -> test_sync_queue(Name) end]}
+ }.
+
+supervision_test_() ->
+ {"The ppool will never restart a dead child, but all children (OTP "
+ "compliant) will be shut down when closing the pool, even if they "
+ "are trapping exits",
+ {setup,
+ fun find_unique_name/0,
+ fun test_supervision/1}}.
+
+auth_test_() ->
+ {"The ppool should only dequeue tasks after receiving a down signal "
+ "from a worker and nobody else",
+ {setup,
+ fun start_ppool/0,
+ fun kill_ppool/1,
+ fun test_auth_dealloc/1}}.
+
+%%% Setups/teardowns
+find_unique_name() ->
+ application:start(ppool),
+ Name = list_to_atom(lists:flatten(io_lib:format("~p",[now()]))),
+ ?assertEqual(undefined, whereis(Name)),
+ Name.
+
+start_ppool() ->
+ Name = find_unique_name(),
+ ppool:start_pool(Name, 2, {ppool_nagger, start_link, []}),
+ Name.
+
+kill_ppool(Name) ->
+ ppool:stop_pool(Name).
+
+%%% Actual tests
+start_and_test_name(Name) ->
+ ppool:start_pool(Name, 1, {ppool_nagger, start_link, []}),
+ A = whereis(Name),
+ ppool:stop_pool(Name),
+ timer:sleep(100),
+ B = whereis(Name),
+ [?_assert(undefined =/= A),
+ ?_assertEqual(undefined, B)].
+
+pool_run_mfa(Name) ->
+ ppool:run(Name, [i_am_running, 1, 1, self()]),
+ X = receive
+ {_Pid, i_am_running} -> ok
+ after 3000 ->
+ timeout
+ end,
+ ?_assertEqual(ok, X).
+
+pool_run_alloc(Name) ->
+ {ok, Pid} = ppool:run(Name, [i_am_running, 1, 1, self()]),
+ X = receive
+ {Pid, i_am_running} -> ok
+ after 3000 ->
+ timeout
+ end,
+ [?_assert(is_pid(Pid)),
+ ?_assertEqual(ok, X)].
+
+pool_run_noalloc(Name) ->
+ %% Init function should have set the limit to 2
+ ppool:run(Name, [i_am_running, 300, 1, self()]),
+ ppool:run(Name, [i_am_running, 300, 1, self()]),
+ X = ppool:run(Name, [i_am_running, 1, 1, self()]),
+ ?_assertEqual(noalloc, X).
+
+pool_run_realloc(Name) ->
+ %% Init function should have set the limit to 2
+ {ok, A} = ppool:run(Name, [i_am_running, 500, 1, self()]),
+ timer:sleep(100),
+ {ok, B} = ppool:run(Name, [i_am_running, 500, 1, self()]),
+ timer:sleep(600),
+ {ok, Pid} = ppool:run(Name, [i_am_running, 1, 1, self()]),
+ timer:sleep(100),
+ L = flush(),
+ [?_assert(is_pid(Pid)),
+ ?_assertEqual([{A,i_am_running}, {B,i_am_running}, {Pid,i_am_running}],
+ L)].
+
+test_async_queue(Name) ->
+ %% Still two elements max!
+ ok = ppool:async_queue(Name, [i_am_running, 2000, 1, self()]),
+ ok = ppool:async_queue(Name, [i_am_running, 2000, 1, self()]),
+ noalloc = ppool:run(Name, [i_am_running, 2000, 1, self()]),
+ ok = ppool:async_queue(Name, [i_am_running, 500, 1, self()]),
+ timer:sleep(3500),
+ L = flush(),
+ ?_assertMatch([{_, i_am_running}, {_, i_am_running}, {_, i_am_running}], L).
+
+test_sync_queue(Name) ->
+ %% Hell yase, two max
+ {ok, Pid} = ppool:sync_queue(Name, [i_am_running, 200, 1, self()]),
+ ok = ppool:async_queue(Name, [i_am_running, 200, 1, self()]),
+ ok = ppool:async_queue(Name, [i_am_running, 200, 1, self()]),
+ {ok, Pid2} = ppool:sync_queue(Name, [i_am_running, 100, 1, self()]),
+ timer:sleep(300),
+ L = flush(),
+ [?_assert(is_pid(Pid)),
+ ?_assert(is_pid(Pid2)),
+ ?_assertMatch([{_,i_am_running}, {_,i_am_running},
+ {_,i_am_running}, {_,i_am_running}],
+ L)].
+
+test_supervision(Name) ->
+ ppool:start_pool(Name, 1, {ppool_nagger, start_link, []}),
+ {ok, Pid} = ppool:run(Name, [sup, 10000, 100, self()]),
+ ppool:stop_pool(Name),
+ timer:sleep(100),
+ ?_assertEqual(undefined, process_info(Pid)).
+
+test_auth_dealloc(Name) ->
+ %% Hell yase, two max
+ {ok, _Pid} = ppool:sync_queue(Name, [i_am_running, 500, 1, self()]),
+ ok = ppool:async_queue(Name, [i_am_running, 10000, 1, self()]),
+ ok = ppool:async_queue(Name, [i_am_running, 10000, 1, self()]),
+ ok = ppool:async_queue(Name, [i_am_running, 1, 1, self()]),
+ timer:sleep(600),
+ Name ! {'DOWN', make_ref(), process, self(), normal},
+ Name ! {'DOWN', make_ref(), process, self(), normal},
+ Name ! {'DOWN', make_ref(), process, self(), normal},
+ timer:sleep(200),
+ L = flush(),
+ ?_assertMatch([{_,i_am_running}], L).
+
+
+
+flush() ->
+ receive
+ X -> [X|flush()]
+ after 0 ->
+ []
+ end.
+
+%% Exported Helper functions
+test_mfa(Pid) ->
+ Pid ! i_am_running.
+
+wait_mfa(Pid) ->
+ Pid ! i_am_running,
+ timer:sleep(3000).
diff --git a/learn-you-some-erlang/release/rel/.this-file-intentionally-left-blank b/learn-you-some-erlang/release/rel/.this-file-intentionally-left-blank
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/learn-you-some-erlang/release/rel/.this-file-intentionally-left-blank
diff --git a/learn-you-some-erlang/reminder/Emakefile b/learn-you-some-erlang/reminder/Emakefile
new file mode 100644
index 0000000..fbde9cb
--- /dev/null
+++ b/learn-you-some-erlang/reminder/Emakefile
@@ -0,0 +1,4 @@
+{'src/*', [debug_info,
+ {i, "src"},
+ {i, "include"},
+ {outdir, "ebin"}]}.
diff --git a/learn-you-some-erlang/reminder/ebin/.this-file-intentionally-left-blank b/learn-you-some-erlang/reminder/ebin/.this-file-intentionally-left-blank
new file mode 100644
index 0000000..758f532
--- /dev/null
+++ b/learn-you-some-erlang/reminder/ebin/.this-file-intentionally-left-blank
@@ -0,0 +1 @@
+Thank you, Mercurial! I still love you...
diff --git a/learn-you-some-erlang/reminder/include/.this-file-intentionally-left-blank b/learn-you-some-erlang/reminder/include/.this-file-intentionally-left-blank
new file mode 100644
index 0000000..758f532
--- /dev/null
+++ b/learn-you-some-erlang/reminder/include/.this-file-intentionally-left-blank
@@ -0,0 +1 @@
+Thank you, Mercurial! I still love you...
diff --git a/learn-you-some-erlang/reminder/priv/.this-file-intentionally-left-blank b/learn-you-some-erlang/reminder/priv/.this-file-intentionally-left-blank
new file mode 100644
index 0000000..758f532
--- /dev/null
+++ b/learn-you-some-erlang/reminder/priv/.this-file-intentionally-left-blank
@@ -0,0 +1 @@
+Thank you, Mercurial! I still love you...
diff --git a/learn-you-some-erlang/reminder/src/.this-file-intentionally-left-blank b/learn-you-some-erlang/reminder/src/.this-file-intentionally-left-blank
new file mode 100644
index 0000000..758f532
--- /dev/null
+++ b/learn-you-some-erlang/reminder/src/.this-file-intentionally-left-blank
@@ -0,0 +1 @@
+Thank you, Mercurial! I still love you...
diff --git a/learn-you-some-erlang/reminder/src/dev_event.erl b/learn-you-some-erlang/reminder/src/dev_event.erl
new file mode 100644
index 0000000..5b752ac
--- /dev/null
+++ b/learn-you-some-erlang/reminder/src/dev_event.erl
@@ -0,0 +1,82 @@
+%%% This module is there to test the incomplete loops and constructs
+%%% that are presented in the text, but are not the final result.
+-module(dev_event).
+-compile(export_all).
+-record(state, {server,
+ name="",
+ to_go=0}).
+
+start1(EventName, Delay) ->
+ spawn(?MODULE, init1, [self(), EventName, Delay]).
+
+start_link1(EventName, Delay) ->
+ spawn_link(?MODULE, init1, [self(), EventName, Delay]).
+
+start2(EventName, Delay) ->
+ spawn(?MODULE, init2, [self(), EventName, Delay]).
+
+start_link2(EventName, Delay) ->
+ spawn_link(?MODULE, init2, [self(), EventName, Delay]).
+
+cancel(Pid) ->
+ %% Monitor in case the process is already dead
+ Ref = erlang:monitor(process, Pid),
+ Pid ! {self(), Ref, cancel},
+ receive
+ {Ref, ok} ->
+ erlang:demonitor(Ref, [flush]),
+ ok;
+ {'DOWN', Ref, process, Pid, _Reason} ->
+ ok
+ end.
+
+
+
+%%% Event's innards
+init1(Server, EventName, Delay) ->
+ loop2(#state{server=Server,
+ name=EventName,
+ to_go=normalize(Delay)}).
+
+init2(Server, EventName, DateTime) ->
+ loop2(#state{server=Server,
+ name=EventName,
+ to_go=time_to_go(DateTime)}).
+
+loop1(S = #state{server=Server}) ->
+ receive
+ {Server, Ref, cancel} ->
+ Server ! {Ref, ok}
+ after S#state.to_go * 1000 ->
+ Server ! {done, S#state.name}
+ end.
+
+%% Loop uses a list for times in order to go around the ~49 days limit
+%% on timeouts.
+loop2(S = #state{server=Server, to_go=[T|Next]}) ->
+ receive
+ {Server, Ref, cancel} ->
+ Server ! {Ref, ok}
+ after T*1000 ->
+ if Next =:= [] ->
+ Server ! {done, S#state.name};
+ Next =/= [] ->
+ loop2(S#state{to_go=Next})
+ end
+ end.
+
+
+time_to_go(TimeOut={{_,_,_}, {_,_,_}}) ->
+ Now = calendar:local_time(),
+ ToGo = calendar:datetime_to_gregorian_seconds(TimeOut) -
+ calendar:datetime_to_gregorian_seconds(Now),
+ Secs = if ToGo > 0 -> ToGo;
+ ToGo =< 0 -> 0
+ end,
+ normalize(Secs).
+
+%% Because Erlang is limited to about 49 days (49*24*60*60*1000) in
+%% milliseconds, the following function is used
+normalize(N) ->
+ Limit = 49*24*60*60,
+ [N rem Limit | lists:duplicate(N div Limit, Limit)].
diff --git a/learn-you-some-erlang/reminder/src/event.erl b/learn-you-some-erlang/reminder/src/event.erl
new file mode 100644
index 0000000..58b5b57
--- /dev/null
+++ b/learn-you-some-erlang/reminder/src/event.erl
@@ -0,0 +1,61 @@
+-module(event).
+-export([start/2, start_link/2, cancel/1]).
+-export([init/3, loop/1]).
+-record(state, {server,
+ name="",
+ to_go=0}).
+
+%%% Public interface
+start(EventName, DateTime) ->
+ spawn(?MODULE, init, [self(), EventName, DateTime]).
+
+start_link(EventName, DateTime) ->
+ spawn_link(?MODULE, init, [self(), EventName, DateTime]).
+
+cancel(Pid) ->
+ %% Monitor in case the process is already dead
+ Ref = erlang:monitor(process, Pid),
+ Pid ! {self(), Ref, cancel},
+ receive
+ {Ref, ok} ->
+ erlang:demonitor(Ref, [flush]),
+ ok;
+ {'DOWN', Ref, process, Pid, _Reason} ->
+ ok
+ end.
+
+%%% Event's innards
+init(Server, EventName, DateTime) ->
+ loop(#state{server=Server,
+ name=EventName,
+ to_go=time_to_go(DateTime)}).
+
+%% Loop uses a list for times in order to go around the ~49 days limit
+%% on timeouts.
+loop(S = #state{server=Server, to_go=[T|Next]}) ->
+ receive
+ {Server, Ref, cancel} ->
+ Server ! {Ref, ok}
+ after T*1000 ->
+ if Next =:= [] ->
+ Server ! {done, S#state.name};
+ Next =/= [] ->
+ loop(S#state{to_go=Next})
+ end
+ end.
+
+%%% private functions
+time_to_go(TimeOut={{_,_,_}, {_,_,_}}) ->
+ Now = calendar:local_time(),
+ ToGo = calendar:datetime_to_gregorian_seconds(TimeOut) -
+ calendar:datetime_to_gregorian_seconds(Now),
+ Secs = if ToGo > 0 -> ToGo;
+ ToGo =< 0 -> 0
+ end,
+ normalize(Secs).
+
+%% Because Erlang is limited to about 49 days (49*24*60*60*1000) in
+%% milliseconds, the following function is used
+normalize(N) ->
+ Limit = 49*24*60*60,
+ [N rem Limit | lists:duplicate(N div Limit, Limit)].
diff --git a/learn-you-some-erlang/reminder/src/evserv.erl b/learn-you-some-erlang/reminder/src/evserv.erl
new file mode 100644
index 0000000..1087376
--- /dev/null
+++ b/learn-you-some-erlang/reminder/src/evserv.erl
@@ -0,0 +1,168 @@
+%% Event server
+-module(evserv).
+-compile(export_all).
+
+-record(state, {events, %% list of #event{} records
+ clients}). %% list of Pids
+
+-record(event, {name="",
+ description="",
+ pid,
+ timeout={{1970,1,1},{0,0,0}}}).
+
+%%% User Interface
+
+start() ->
+ register(?MODULE, Pid=spawn(?MODULE, init, [])),
+ Pid.
+
+start_link() ->
+ register(?MODULE, Pid=spawn_link(?MODULE, init, [])),
+ Pid.
+
+terminate() ->
+ ?MODULE ! shutdown.
+
+init() ->
+ %% Loading events from a static file could be done here.
+ %% You would need to pass an argument to init (maybe change the functions
+ %% start/0 and start_link/0 to start/1 and start_link/1) telling where the
+ %% resource to find the events is. Then load it from here.
+ %% Another option is to just pass the event straight to the server
+ %% through this function.
+ loop(#state{events=orddict:new(),
+ clients=orddict:new()}).
+
+subscribe(Pid) ->
+ Ref = erlang:monitor(process, whereis(?MODULE)),
+ ?MODULE ! {self(), Ref, {subscribe, Pid}},
+ receive
+ {Ref, ok} ->
+ {ok, Ref};
+ {'DOWN', Ref, process, _Pid, Reason} ->
+ {error, Reason}
+ after 5000 ->
+ {error, timeout}
+ end.
+
+add_event(Name, Description, TimeOut) ->
+ Ref = make_ref(),
+ ?MODULE ! {self(), Ref, {add, Name, Description, TimeOut}},
+ receive
+ {Ref, Msg} -> Msg
+ after 5000 ->
+ {error, timeout}
+ end.
+
+add_event2(Name, Description, TimeOut) ->
+ Ref = make_ref(),
+ ?MODULE ! {self(), Ref, {add, Name, Description, TimeOut}},
+ receive
+ {Ref, {error, Reason}} -> erlang:error(Reason);
+ {Ref, Msg} -> Msg
+ after 5000 ->
+ {error, timeout}
+ end.
+
+cancel(Name) ->
+ Ref = make_ref(),
+ ?MODULE ! {self(), Ref, {cancel, Name}},
+ receive
+ {Ref, ok} -> ok
+ after 5000 ->
+ {error, timeout}
+ end.
+
+listen(Delay) ->
+ receive
+ M = {done, _Name, _Description} ->
+ [M | listen(0)]
+ after Delay*1000 ->
+ []
+ end.
+
+%%% The Server itself
+
+loop(S=#state{}) ->
+ receive
+ {Pid, MsgRef, {subscribe, Client}} ->
+ Ref = erlang:monitor(process, Client),
+ NewClients = orddict:store(Ref, Client, S#state.clients),
+ Pid ! {MsgRef, ok},
+ loop(S#state{clients=NewClients});
+ {Pid, MsgRef, {add, Name, Description, TimeOut}} ->
+ case valid_datetime(TimeOut) of
+ true ->
+ EventPid = event:start_link(Name, TimeOut),
+ NewEvents = orddict:store(Name,
+ #event{name=Name,
+ description=Description,
+ pid=EventPid,
+ timeout=TimeOut},
+ S#state.events),
+ Pid ! {MsgRef, ok},
+ loop(S#state{events=NewEvents});
+ false ->
+ Pid ! {MsgRef, {error, bad_timeout}},
+ loop(S)
+ end;
+ {Pid, MsgRef, {cancel, Name}} ->
+ Events = case orddict:find(Name, S#state.events) of
+ {ok, E} ->
+ event:cancel(E#event.pid),
+ orddict:erase(Name, S#state.events);
+ error ->
+ S#state.events
+ end,
+ Pid ! {MsgRef, ok},
+ loop(S#state{events=Events});
+ {done, Name} ->
+ case orddict:find(Name, S#state.events) of
+ {ok, E} ->
+ send_to_clients({done, E#event.name, E#event.description},
+ S#state.clients),
+ NewEvents = orddict:erase(Name, S#state.events),
+ loop(S#state{events=NewEvents});
+ error ->
+ %% This may happen if we cancel an event and
+ %% it fires at the same time
+ loop(S)
+ end;
+ shutdown ->
+ exit(shutdown);
+ {'DOWN', Ref, process, _Pid, _Reason} ->
+ loop(S#state{clients=orddict:erase(Ref, S#state.clients)});
+ code_change ->
+ ?MODULE:loop(S);
+ {Pid, debug} -> %% used as a hack to let me do some unit testing
+ Pid ! S,
+ loop(S);
+ Unknown ->
+ io:format("Unknown message: ~p~n",[Unknown]),
+ loop(S)
+ end.
+
+
+%%% Internal Functions
+send_to_clients(Msg, ClientDict) ->
+ orddict:map(fun(_Ref, Pid) -> Pid ! Msg end, ClientDict).
+
+valid_datetime({Date,Time}) ->
+ try
+ calendar:valid_date(Date) andalso valid_time(Time)
+ catch
+ error:function_clause -> %% not in {{Y,M,D},{H,Min,S}} format
+ false
+ end;
+valid_datetime(_) ->
+ false.
+
+%% calendar has valid_date, but nothing for days.
+%% This function is based on its interface.
+%% Ugly, but ugh.
+valid_time({H,M,S}) -> valid_time(H,M,S).
+
+valid_time(H,M,S) when H >= 0, H < 24,
+ M >= 0, M < 60,
+ S >= 0, S < 60 -> true;
+valid_time(_,_,_) -> false.
diff --git a/learn-you-some-erlang/reminder/src/sup.erl b/learn-you-some-erlang/reminder/src/sup.erl
new file mode 100644
index 0000000..71a463b
--- /dev/null
+++ b/learn-you-some-erlang/reminder/src/sup.erl
@@ -0,0 +1,22 @@
+-module(sup).
+-export([start/2, start_link/2, init/1, loop/1]).
+
+start(Mod, Args) ->
+ spawn(?MODULE, init, [{Mod, Args}]).
+
+start_link(Mod,Args) ->
+ spawn_link(?MODULE, init, [{Mod, Args}]).
+
+init({Mod,Args}) ->
+ process_flag(trap_exit, true),
+ loop({Mod,start_link,Args}).
+
+loop({M,F,A}) ->
+ Pid = apply(M,F,A),
+ receive
+ {'EXIT', _From, shutdown} ->
+ exit(shutdown); % will kill the child too
+ {'EXIT', Pid, Reason} ->
+ io:format("Process ~p exited for reason ~p~n",[Pid,Reason]),
+ loop({M,F,A})
+ end.
diff --git a/learn-you-some-erlang/reminder/src/tests/dev_event_tests.erl b/learn-you-some-erlang/reminder/src/tests/dev_event_tests.erl
new file mode 100644
index 0000000..5de18d1
--- /dev/null
+++ b/learn-you-some-erlang/reminder/src/tests/dev_event_tests.erl
@@ -0,0 +1,41 @@
+-module(dev_event_tests).
+-include_lib("eunit/include/eunit.hrl").
+%%% Very minimal tests, only verifying that the two implementations of
+%%% timeouts work the same in their final and primitive versions
+%%% as in the final module. The rest is rather cruft to emulate the normal
+%%% event.erl module. Little is tested, because little needs to be tested.
+
+
+-record(state, {server,
+ name="",
+ to_go=[0]}).
+
+timeout_test_() ->
+ {inorder,
+ [fun() -> timeout(loop1, 2) end,
+ fun() -> timeout(loop2, [2]) end]}.
+
+timeout(AtomFun, T) ->
+ S = self(),
+ spawn_link(dev_event, AtomFun, [#state{server=S, name="test", to_go=T}]),
+ timer:sleep(1000),
+ M1 = receive A -> A after 0 -> timeout end,
+ timer:sleep(1500),
+ M2 = receive B -> B after 0 -> timeout end,
+ M3 = receive C -> C after 0 -> timeout end,
+ ?assertEqual(timeout, M1),
+ ?assertEqual({done, "test"}, M2),
+ ?assertEqual(timeout, M3).
+
+cancel_msg_test_() ->
+ {inorder,
+ [fun() -> cancel_msg(loop1, 2) end,
+ fun() -> cancel_msg(loop2, [2]) end]}.
+
+cancel_msg(AtomFun, T) ->
+ S = self(),
+ R = make_ref(),
+ Pid = spawn_link(dev_event, AtomFun, [#state{server=S, name="test", to_go=T}]),
+ Pid ! {S, R, cancel},
+ M = receive A -> A after 500 -> timeout end,
+ ?assertEqual({R, ok}, M).
diff --git a/learn-you-some-erlang/reminder/src/tests/event_tests.erl b/learn-you-some-erlang/reminder/src/tests/event_tests.erl
new file mode 100644
index 0000000..80b3a44
--- /dev/null
+++ b/learn-you-some-erlang/reminder/src/tests/event_tests.erl
@@ -0,0 +1,43 @@
+-module(event_tests).
+-include_lib("eunit/include/eunit.hrl").
+-test_warnings([start/0, start_link/1, init/0, time_to_go/1]).
+%% defined in event.erl
+-record(state, {server,
+ name="",
+ to_go=[0]}).
+
+timeout_test_() ->
+ S = self(),
+ spawn_link(event, loop, [#state{server=S, name="test", to_go=[2]}]),
+ timer:sleep(1000),
+ M1 = receive A -> A after 0 -> timeout end,
+ timer:sleep(1500),
+ M2 = receive B -> B after 0 -> timeout end,
+ M3 = receive C -> C after 0 -> timeout end,
+ [?_assertEqual(timeout, M1),
+ ?_assertEqual({done, "test"}, M2),
+ ?_assertEqual(timeout, M3)].
+
+cancel_msg_test_() ->
+ S = self(),
+ R = make_ref(),
+ Pid = spawn_link(event, loop, [#state{server=S, name="test", to_go=[2]}]),
+ Pid ! {S, R, cancel},
+ M = receive A -> A after 500 -> timeout end,
+ [?_assertEqual({R, ok}, M)].
+
+cancel_fn_test_() ->
+ S = self(),
+ Pid = spawn_link(event, loop, [#state{server=S, name="test", to_go=[2]}]),
+ [?_assertEqual(ok, event:cancel(Pid)),
+ %% calling cancel again should fail, but still return ok.
+ ?_assertEqual(ok, event:cancel(Pid))].
+
+normalize_test_() ->
+ [?_assertEqual([0], event:normalize(0)),
+ ?_assertEqual([2], event:normalize(2)),
+ %% special cases w/ remainders
+ ?_assertEqual(1, length(event:normalize(49*24*60*59))),
+ ?_assertEqual(2, length(event:normalize(49*24*60*60))),
+ ?_assertEqual(2, length(event:normalize(49*24*60*60+1))),
+ ?_assertEqual(1000*24*60*60, lists:sum(event:normalize(1000*24*60*60)))].
diff --git a/learn-you-some-erlang/reminder/src/tests/evserv_tests.erl b/learn-you-some-erlang/reminder/src/tests/evserv_tests.erl
new file mode 100644
index 0000000..b093a58
--- /dev/null
+++ b/learn-you-some-erlang/reminder/src/tests/evserv_tests.erl
@@ -0,0 +1,203 @@
+-module(evserv_tests).
+-include_lib("eunit/include/eunit.hrl").
+-define(FUTURE_DATE, {{2100,1,1},{0,0,0}}).
+%% all the tests in this module would be much easier with property-based testing.
+%% see Triq, Quvic Quickcheck or Proper for libraries to download that let you
+%% do this.
+-test_warnings([start/0]).
+
+valid_time_test_() ->
+ [?_assert(evserv:valid_time({0,0,0})),
+ ?_assert(evserv:valid_time({23,59,59})),
+ ?_assert(not evserv:valid_time({23,59,60})),
+ ?_assert(not evserv:valid_time({23,60,59})),
+ ?_assert(not evserv:valid_time({24,59,59})),
+ ?_assert(not evserv:valid_time({-1,0,0})),
+ ?_assert(not evserv:valid_time({0,-1,0})),
+ ?_assert(not evserv:valid_time({0,0,-1}))].
+
+valid_datetime_test_() ->
+ [?_assert(evserv:valid_datetime({{0,1,1},{0,0,0}})),
+ ?_assert(evserv:valid_datetime({{2004,2,29},{23,59,59}})),
+ ?_assert(evserv:valid_datetime({{2004,12,31},{23,59,59}})),
+ ?_assert(not evserv:valid_datetime({{2004,12,31},{23,60,60}})),
+ ?_assert(not evserv:valid_datetime({{2003,2,29},{23,59,59}})),
+ ?_assert(not evserv:valid_datetime({{0,0,0},{0,0,0}})),
+ ?_assert(not evserv:valid_datetime(1209312303))].
+
+loop_test_() ->
+ {"Testing all server events on a protocol level",
+ [{"Subscribe Tests",
+ [{spawn,
+ {setup,
+ fun evserv:start_link/0,
+ fun terminate/1,
+ fun subscribe/1}},
+ {spawn,
+ {setup,
+ fun evserv:start_link/0,
+ fun terminate/1,
+ fun subscribe2/1}},
+ {spawn,
+ {setup,
+ fun evserv:start_link/0,
+ fun terminate/1,
+ fun down/1}}]},
+ {"Adding event integration tests",
+ [{spawn,
+ {setup,
+ fun evserv:start_link/0,
+ fun terminate/1,
+ fun add/1}},
+ {spawn,
+ {setup,
+ fun evserv:start_link/0,
+ fun terminate/1,
+ fun addremove/1}},
+ {spawn,
+ {setup,
+ fun evserv:start_link/0,
+ fun terminate/1,
+ fun done/1}}]}]}.
+
+interface_test_() ->
+ {"Testing all server events via the interface functions",
+ [{spawn,
+ {setup,
+ fun evserv:start_link/0,
+ fun terminate/1,
+ fun interface/1}}]}.
+
+subscribe(Pid) ->
+ Ref = make_ref(),
+ S = self(),
+ Pid ! {S, Ref, {subscribe, S}},
+ {ok, M1} = read(),
+ Pid ! {S, debug},
+ {ok, M2} = read(),
+ [?_assertEqual({Ref, ok}, M1),
+ ?_assertMatch({state,[], [{_, S}]}, M2)].
+
+subscribe2(Pid) ->
+ Ref1 = make_ref(),
+ Ref2 = make_ref(),
+ S = self(),
+ Pid ! {S, Ref1, {subscribe, S}},
+ {ok, M1} = read(),
+ Pid ! {S, Ref2, {subscribe, S}},
+ {ok, M2} = read(),
+ Pid ! {S, debug},
+ {ok, M3} = read(),
+ [?_assertEqual({Ref1, ok}, M1),
+ ?_assertEqual({Ref2, ok}, M2),
+ ?_assertMatch({state,[], [{_,S},{_,S}]}, M3)].
+
+down(Pid) ->
+ Ref = make_ref(),
+ ClientPid = spawn(fun() -> timer:sleep(50000) end),
+ Pid ! {self(), debug},
+ {ok, S1} = read(),
+ Pid ! {self(), Ref, {subscribe, ClientPid}},
+ {ok, M1} = read(),
+ Pid ! {self(), debug},
+ {ok, S2} = read(),
+ exit(ClientPid, testkill),
+ timer:sleep(100),
+ Pid ! {self(), debug},
+ {ok, S3} = read(),
+ [?_assertMatch({state, _, []}, S1),
+ ?_assertEqual({Ref, ok}, M1),
+ ?_assertMatch({state, _, [{_,ClientPid}]}, S2),
+ ?_assertEqual(S1, S3)].
+
+add(Pid) ->
+ Ref1 = make_ref(),
+ Ref2 = make_ref(),
+ Pid ! {self(), Ref1, {add, "test", "a test event", ?FUTURE_DATE}},
+ {ok, M1} = read(),
+ Pid ! {self(), Ref2, {add, "test", "a test event2", wrong_date}},
+ {ok, M2} = read(),
+ [?_assertEqual({Ref1, ok}, M1),
+ ?_assertEqual({Ref2, {error, bad_timeout}}, M2)].
+
+addremove(Pid) ->
+ Ref1 = make_ref(),
+ Ref2 = make_ref(),
+ Ref3 = make_ref(),
+ %% ask for useless deletion, check the state
+ Pid ! {self(), Ref1, {cancel, "nonexist"}},
+ {ok, M1} = read(),
+ Pid ! {self(), debug},
+ {ok, State1} = read(),
+ %% add an event, check the state
+ Pid ! {self(), Ref2, {add, "test", "a test event", ?FUTURE_DATE}},
+ {ok, M2} = read(),
+ Pid ! {self(), debug},
+ {ok, State2} = read(),
+ %% remove the event, check the state
+ Pid ! {self(), Ref3, {cancel, "test"}},
+ {ok, M3} = read(),
+ Pid ! {self(), debug},
+ {ok, State3} = read(),
+ [?_assertEqual({Ref1, ok}, M1),
+ ?_assertEqual({Ref2, ok}, M2),
+ ?_assertEqual({Ref3, ok}, M3),
+ ?_assertEqual(State1, State3),
+ ?_assert(State2 =/= State3),
+ ?_assertMatch({state,[{"test", {event,"test",_,_,?FUTURE_DATE}}],_}, State2)].
+
+done(Pid) ->
+ Ref = make_ref(),
+ {ok, _} = evserv:subscribe(self()),
+ Pid ! {self(), debug},
+ {ok, S1} = read(),
+ {Date,{H,Min,S}} = calendar:local_time(),
+ DateTime = {Date,{H,Min,S+1}},
+ Pid ! {self(), Ref, {add, "test", "a test event", DateTime}},
+ {ok, M1} = read(),
+ Pid ! {self(), debug},
+ {ok, S2} = read(),
+ X = read(),
+ timer:sleep(750),
+ {ok, M2} = read(),
+ Pid ! {self(), debug},
+ {ok, S3} = read(),
+ [?_assertMatch({state, [], _}, S1),
+ ?_assertEqual({Ref, ok}, M1),
+ ?_assertMatch({state, [{"test",_}], _}, S2),
+ ?_assertEqual(timeout, X),
+ ?_assertEqual({done, "test", "a test event"}, M2),
+ ?_assertEqual(S1,S3)].
+
+interface(_Pid) ->
+ Ref = evserv:subscribe(self()),
+ {Date,{H,Min,S}} = calendar:local_time(),
+ M1 = evserv:add_event("test", "desc", {Date,{H,Min,S+1}}),
+ M2 = evserv:cancel("test"),
+ M3 = evserv:add_event("test1", "desc1", calendar:local_time()),
+ M4 = evserv:add_event2("test2", "desc2", calendar:local_time()),
+ timer:sleep(100),
+ M5 = evserv:listen(2),
+ M6 = evserv:add_event("test3", "desc3", some_atom),
+ M7 = (catch evserv:add_event2("test4", "desc4", some_atom)),
+ [?_assertMatch({ok, _}, Ref),
+ ?_assert(is_reference(element(2, Ref))),
+ ?_assertEqual(ok, M1),
+ ?_assertEqual(ok, M2),
+ ?_assertEqual(ok, M3),
+ ?_assertEqual(ok, M4),
+ ?_assertEqual([{done,"test1","desc1"},{done,"test2","desc2"}], M5),
+ ?_assertEqual({error, bad_timeout}, M6),
+ ?_assertMatch({'EXIT', {bad_timeout,_}}, M7)].
+
+
+%% helpers
+terminate(Pid) -> Pid ! shutdown.
+
+read() ->
+ receive
+ M -> {ok, M}
+ after 500 ->
+ timeout
+ end.
+
diff --git a/learn-you-some-erlang/reminder/src/tests/sup_tests.erl b/learn-you-some-erlang/reminder/src/tests/sup_tests.erl
new file mode 100644
index 0000000..28d7fa9
--- /dev/null
+++ b/learn-you-some-erlang/reminder/src/tests/sup_tests.erl
@@ -0,0 +1,25 @@
+-module(sup_tests).
+-include_lib("eunit/include/eunit.hrl").
+
+restart_test_() ->
+ {"Test that everything restarts until a kill",
+ {setup,
+ fun() -> sup:start(evserv, []) end,
+ fun(_) -> ok end,
+ fun restart/1}}.
+
+restart(_SupPid) ->
+ timer:sleep(100),
+ A = is_pid(whereis(evserv)),
+ catch exit(whereis(evserv), die),
+ timer:sleep(100),
+ B = is_pid(whereis(evserv)),
+ catch exit(whereis(evserv), die),
+ timer:sleep(100),
+ C = is_pid(whereis(evserv)),
+ catch exit(whereis(evserv), shutdown),
+ timer:sleep(500),
+ D = is_pid(whereis(evserv)),
+ ?_assertEqual([true,true,true,false],
+ [A,B,C,D]).
+
diff --git a/learn-you-some-erlang/road.erl b/learn-you-some-erlang/road.erl
new file mode 100644
index 0000000..fa34f5a
--- /dev/null
+++ b/learn-you-some-erlang/road.erl
@@ -0,0 +1,39 @@
+-module(road).
+-compile(export_all).
+
+main([FileName]) ->
+ {ok, Bin} = file:read_file(FileName),
+ Map = parse_map(Bin),
+ io:format("~p~n",[optimal_path(Map)]),
+ erlang:halt().
+
+%% Transform a string into a readable map of triples
+parse_map(Bin) when is_binary(Bin) ->
+ parse_map(binary_to_list(Bin));
+parse_map(Str) when is_list(Str) ->
+ Values = [list_to_integer(X) || X <- string:tokens(Str,"\r\n\t ")],
+ group_vals(Values, []).
+
+group_vals([], Acc) ->
+ lists:reverse(Acc);
+group_vals([A,B,X|Rest], Acc) ->
+ group_vals(Rest, [{A,B,X} | Acc]).
+
+%% Picks the best of all paths, woo!
+optimal_path(Map) ->
+ {A,B} = lists:foldl(fun shortest_step/2, {{0,[]}, {0,[]}}, Map),
+ {_Dist,Path} = if hd(element(2,A)) =/= {x,0} -> A;
+ hd(element(2,B)) =/= {x,0} -> B
+ end,
+ lists:reverse(Path).
+
+%% actual problem solving
+%% change triples of the form {A,B,X}
+%% where A,B,X are distances and a,b,x are possible paths
+%% to the form {DistanceSum, PathList}.
+shortest_step({A,B,X}, {{DistA,PathA}, {DistB,PathB}}) ->
+ OptA1 = {DistA + A, [{a,A}|PathA]},
+ OptA2 = {DistB + B + X, [{x,X}, {b,B}|PathB]},
+ OptB1 = {DistB + B, [{b,B}|PathB]},
+ OptB2 = {DistA + A + X, [{x,X}, {a,A}|PathA]},
+ {erlang:min(OptA1, OptA2), erlang:min(OptB1, OptB2)}.
diff --git a/learn-you-some-erlang/road.txt b/learn-you-some-erlang/road.txt
new file mode 100644
index 0000000..fa6997e
--- /dev/null
+++ b/learn-you-some-erlang/road.txt
@@ -0,0 +1,12 @@
+50
+10
+30
+5
+90
+20
+40
+2
+25
+10
+8
+0
diff --git a/learn-you-some-erlang/tester.erl b/learn-you-some-erlang/tester.erl
new file mode 100644
index 0000000..f1d1b1c
--- /dev/null
+++ b/learn-you-some-erlang/tester.erl
@@ -0,0 +1,55 @@
+-module(tester).
+-export([dir/0, dir/2]).
+-define(EXT, ".erl"). % file extension to look for
+-define(MODS, "./").
+-define(TESTS, "./tests/").
+
+%% scans both a module directory and a test directory, compiles the
+%% modules inside and then call for tests to be ran.
+%%
+%% usage:
+%% tester:dir("./","./tests/").
+dir() -> dir(?MODS, ?TESTS).
+dir(ModulePath, TestPath) ->
+ ModuleList = module_list(ModulePath),
+ TestList = module_list(TestPath),
+ [compile(ModulePath++X) || X <- ModuleList],
+ [compile(TestPath++X) || X <- TestList],
+ test_all(TestList),
+ warnings(),
+ cleanup(ModuleList),
+ cleanup(TestList),
+ ok.
+
+%% assumes pre-compiled modules
+test_all(FileList) ->
+ Split = [lists:nth(1, string:tokens(File, ".")) || File <- FileList],
+ [eunit:test(list_to_existing_atom(F), [verbose]) || F <- Split].
+ % [(list_to_existing_atom(F)):test() || F <- Split].
+
+cleanup(Files) ->
+ [file:delete(lists:nth(1, string:tokens(F, "."))++".beam") || F <- Files].
+
+%% get module .erl file names from a directory
+module_list(Path) ->
+ SameExt = fun(File) -> get_ext(File) =:= ?EXT end,
+ {ok, Files} = file:list_dir(Path),
+ lists:filter(SameExt, Files).
+
+%% find the extension of a file (length is taken from the ?EXT macro).
+get_ext(Str) ->
+ lists:reverse(string:sub_string(lists:reverse(Str), 1, length(?EXT))).
+
+compile(FileName) ->
+ compile:file(FileName, [report, verbose, export_all]).
+
+warnings() ->
+ Warns = [{Mod, get_warnings(Mod)} || {Mod,_Path} <- code:all_loaded(),
+ has_warnings(Mod)],
+ io:format("These need to be tested better: ~n\t~p~n", [Warns]).
+
+has_warnings(Mod) ->
+ is_list(get_warnings(Mod)).
+
+get_warnings(Mod) ->
+ proplists:get_value(test_warnings, Mod:module_info(attributes)).
diff --git a/learn-you-some-erlang/tests/calc_tests.erl b/learn-you-some-erlang/tests/calc_tests.erl
new file mode 100644
index 0000000..4f6f530
--- /dev/null
+++ b/learn-you-some-erlang/tests/calc_tests.erl
@@ -0,0 +1,6 @@
+-module(calc_tests).
+-include_lib("eunit/include/eunit.hrl").
+
+%% runs the unit test function defined in calc.erl
+all_test() ->
+ ?assert(ok =:= calc:rpn_test()).
diff --git a/learn-you-some-erlang/tests/cases_tests.erl b/learn-you-some-erlang/tests/cases_tests.erl
new file mode 100644
index 0000000..5b7bd0b
--- /dev/null
+++ b/learn-you-some-erlang/tests/cases_tests.erl
@@ -0,0 +1,24 @@
+-module(cases_tests).
+-include_lib("eunit/include/eunit.hrl").
+
+insert_test_() ->
+ [?_assertEqual([1], cases:insert(1,[])),
+ ?_assertEqual([1], cases:insert(1,[1])),
+ ?_assertEqual([1,2], cases:insert(1,[2]))].
+
+beach_test_() ->
+ [?_assertEqual('favorable', cases:beach({celsius, 20})),
+ ?_assertEqual('favorable', cases:beach({celsius, 45})),
+ ?_assertEqual('avoid beach', cases:beach({celsius, 46})),
+ ?_assertEqual('avoid beach', cases:beach({celsius, 19})),
+ ?_assertEqual('scientifically favorable', cases:beach({kelvin, 293})),
+ ?_assertEqual('scientifically favorable', cases:beach({kelvin, 318})),
+ ?_assertEqual('avoid beach', cases:beach({kelvin, 292})),
+ ?_assertEqual('avoid beach', cases:beach({celsius, 319})),
+ ?_assertEqual('favorable in the US',
+ cases:beach({fahrenheit, 68})),
+ ?_assertEqual('favorable in the US',
+ cases:beach({fahrenheit, 113})),
+ ?_assertEqual('avoid beach', cases:beach({fahrenheit, 67})),
+ ?_assertEqual('avoid beach', cases:beach({fahrenheit, 114})),
+ ?_assertEqual('avoid beach', cases:beach(cat))].
diff --git a/learn-you-some-erlang/tests/cat_fsm_tests.erl b/learn-you-some-erlang/tests/cat_fsm_tests.erl
new file mode 100644
index 0000000..aa4b940
--- /dev/null
+++ b/learn-you-some-erlang/tests/cat_fsm_tests.erl
@@ -0,0 +1,21 @@
+-module(cat_fsm_tests).
+-include_lib("eunit/include/eunit.hrl").
+
+cat_fsm_test_() ->
+ {setup, fun setup/0, fun teardown/1, fun state_test_/1}.
+
+setup() ->
+ cat_fsm:start().
+
+teardown(Pid) ->
+ exit(Pid, end_test).
+
+state_test_(Pid) ->
+ [?_assertEqual(dont_give_crap, get_state(Pid)),
+ ?_assertEqual({ok, meh}, cat_fsm:event(Pid, event)),
+ ?_assertEqual(dont_give_crap, get_state(Pid))].
+
+get_state(Pid) ->
+ List = erlang:process_info(Pid),
+ {_, {_Mod, Fn, _Arity}} = lists:keyfind(current_function, 1, List),
+ Fn.
diff --git a/learn-you-some-erlang/tests/dog_fsm_tests.erl b/learn-you-some-erlang/tests/dog_fsm_tests.erl
new file mode 100644
index 0000000..7217244
--- /dev/null
+++ b/learn-you-some-erlang/tests/dog_fsm_tests.erl
@@ -0,0 +1,81 @@
+-module(dog_fsm_tests).
+-include_lib("eunit/include/eunit.hrl").
+
+bark_test_() ->
+ {foreach,
+ fun setup_bark/0,
+ fun teardown/1,
+ [fun bark_pet_test_/1, fun bark_other_test_/1]}.
+
+wag_tail_test_() ->
+ {foreach,
+ fun setup_wag_tail/0,
+ fun teardown/1,
+ [fun wag_pet_test_/1, fun wag_other_test_/1]}.
+
+sit_test_() ->
+ {foreach,
+ fun setup_sit/0,
+ fun teardown/1,
+ [fun sit_squirrel_test_/1, fun sit_other_test_/1]}.
+
+setup_bark() ->
+ Pid = dog_fsm:start(),
+ timer:sleep(100),
+ Pid.
+
+setup_wag_tail() ->
+ Pid = dog_fsm:start(),
+ dog_fsm:pet(Pid),
+ timer:sleep(100),
+ Pid.
+
+setup_sit() ->
+ Pid = dog_fsm:start(),
+ dog_fsm:pet(Pid),
+ dog_fsm:pet(Pid),
+ timer:sleep(100),
+ Pid.
+
+teardown(Pid) ->
+ exit(Pid, end_test).
+
+init_test() ->
+ Pid = dog_fsm:start(),
+ timer:sleep(100),
+ ?assertEqual(bark, get_state(Pid)).
+
+bark_pet_test_(Pid) ->
+ [?_assertEqual(bark, get_state(Pid)),
+ ?_assertEqual(pet, dog_fsm:pet(Pid)),
+ begin timer:sleep(100), ?_assertEqual(wag_tail, get_state(Pid)) end].
+
+bark_other_test_(Pid) ->
+ [?_assertEqual(bark, get_state(Pid)),
+ ?_assertEqual(squirrel, dog_fsm:squirrel(Pid)),
+ begin timer:sleep(100), ?_assertEqual(bark, get_state(Pid)) end].
+
+wag_pet_test_(Pid) ->
+ [?_assertEqual(wag_tail, get_state(Pid)),
+ ?_assertEqual(pet, dog_fsm:pet(Pid)),
+ begin timer:sleep(100), ?_assertEqual(sit, get_state(Pid)) end].
+
+wag_other_test_(Pid) ->
+ [?_assertEqual(wag_tail, get_state(Pid)),
+ ?_assertEqual(squirrel, dog_fsm:squirrel(Pid)),
+ begin timer:sleep(100), ?_assertEqual(wag_tail, get_state(Pid)) end].
+
+sit_squirrel_test_(Pid) ->
+ [?_assertEqual(sit, get_state(Pid)),
+ ?_assertEqual(squirrel, dog_fsm:squirrel(Pid)),
+ begin timer:sleep(100), ?_assertEqual(bark, get_state(Pid)) end].
+
+sit_other_test_(Pid) ->
+ [?_assertEqual(sit, get_state(Pid)),
+ ?_assertEqual(pet, dog_fsm:pet(Pid)),
+ begin timer:sleep(100), ?_assertEqual(sit, get_state(Pid)) end].
+
+get_state(Pid) ->
+ List = erlang:process_info(Pid),
+ {_, {_Mod, Fn, _Arity}} = lists:keyfind(current_function, 1, List),
+ Fn.
diff --git a/learn-you-some-erlang/tests/dolphins_tests.erl b/learn-you-some-erlang/tests/dolphins_tests.erl
new file mode 100644
index 0000000..670f6a2
--- /dev/null
+++ b/learn-you-some-erlang/tests/dolphins_tests.erl
@@ -0,0 +1,87 @@
+-module(dolphins_tests).
+-include_lib("eunit/include/eunit.hrl").
+%% sorry, this test library is a bit dirty, but it should do
+%% the job.
+
+%% cannot test dolphin1/0 for lack of any results outside of I/O.
+-test_warnings([dolphin1/0]).
+
+dolphin2_test_() ->
+ [?_assertEqual("How about no?", d2_do_a_flip()),
+ ?_assertEqual("So long and thanks for all the fish!",
+ d2_fish())].
+
+d2_do_a_flip() ->
+ Pid = spawn(dolphins, dolphin2, []),
+ Pid ! Pid ! {self(), do_a_flip},
+ %% this function receives a message and that's it
+ F = fun() ->
+ receive
+ M -> M
+ after 500 ->
+ error
+ end
+ end,
+ %% receive the first message
+ Msg1 = F(),
+ %% only one message should be received. If the second one
+ %% is anything except 'error' (no message), return an error.
+ %% otherwise send the message for validation.
+ case F() of
+ error -> Msg1;
+ _ -> error
+ end.
+
+d2_fish() ->
+ Pid = spawn(dolphins, dolphin2, []),
+ Pid ! Pid ! {self(), fish},
+ %% this function receives a message and that's it
+ F = fun() ->
+ receive
+ M -> M
+ after 500 ->
+ error
+ end
+ end,
+ %% receive the first message
+ Msg1 = F(),
+ %% only one message should be received. If the second one
+ %% is anything except 'error' (no message), return an error.
+ %% otherwise send the message for validation.
+ case F() of
+ error -> Msg1;
+ _ -> error
+ end.
+
+dolphin3_test_() ->
+ [?_assertEqual(["How about no?",
+ "How about no?",
+ "So long and thanks for all the fish!"],
+ d3())].
+
+d3() ->
+ Pid = spawn(dolphins, dolphin3, []),
+ Pid ! Pid ! {self(), do_a_flip}, % both should be received
+ Pid ! invalid, % should be ignored, but keep the process going
+ Pid ! {self(), fish}, % should terminate the process
+ Pid ! {self(), do_a_flip}, % should return nothing
+ %% this function receives a message and that's it
+ F = fun() ->
+ receive
+ M -> M
+ after 500 ->
+ error
+ end
+ end,
+ %% receive the expected messages
+ Msg1 = F(),
+ Msg2 = F(),
+ Msg3 = F(),
+ Msgs = [Msg1, Msg2, Msg3],
+ %% Additional messages should now fail. If a message is
+ %% received, add it to the list and let the test fail,
+ %% otherwise send the normal message list.
+ case F() of
+ error -> Msgs;
+ M -> Msgs ++ [M]
+ end.
diff --git a/learn-you-some-erlang/tests/exceptions_tests.erl b/learn-you-some-erlang/tests/exceptions_tests.erl
new file mode 100644
index 0000000..6adfd24
--- /dev/null
+++ b/learn-you-some-erlang/tests/exceptions_tests.erl
@@ -0,0 +1,61 @@
+-module(exceptions_tests).
+-include_lib("eunit/include/eunit.hrl").
+
+throws_test_() ->
+ [?_assertEqual(ok, exceptions:throws(fun() -> a end)),
+ ?_assertException(error, {badfun, _} , exceptions:throws(a)),
+ ?_assertEqual({throw, caught, a},
+ exceptions:throws(fun() -> throw(a) end))].
+
+errors_test_() ->
+ [?_assertEqual(ok, exceptions:errors(fun() -> a end)),
+ ?_assertException(throw,
+ a,
+ exceptions:errors(fun() -> throw(a) end)),
+ ?_assertEqual({error, caught, a},
+ exceptions:errors(fun() -> erlang:error(a) end))].
+exits_test_() ->
+ [?_assertEqual(ok, exceptions:exits(fun() -> a end)),
+ ?_assertException(error, {badfun, _}, exceptions:exits(a)),
+ ?_assertEqual({exit, caught, a},
+ exceptions:exits(fun() -> exit(a) end))].
+
+talk_test() ->
+ ?assertEqual("blah blah", exceptions:talk()).
+
+sword_test_() ->
+ [?_assertException(throw, slice, exceptions:sword(1)),
+ ?_assertException(error, cut_arm, exceptions:sword(2)),
+ ?_assertException(exit, cut_leg, exceptions:sword(3)),
+ ?_assertException(throw, punch, exceptions:sword(4)),
+ ?_assertException(exit, cross_bridge, exceptions:sword(5))].
+
+black_knight_test_() ->
+ [?_assertEqual("None shall pass.",
+ exceptions:black_knight(fun exceptions:talk/0)),
+ ?_assertEqual("It is but a scratch.",
+ exceptions:black_knight(fun() -> exceptions:sword(1) end)),
+ ?_assertEqual("I've had worse.",
+ exceptions:black_knight(fun() -> exceptions:sword(2) end)),
+ ?_assertEqual("Come on you pansy!",
+ exceptions:black_knight(fun() -> exceptions:sword(3) end)),
+ ?_assertEqual("Just a flesh wound.",
+ exceptions:black_knight(fun() -> exceptions:sword(4) end)),
+ ?_assertEqual("Just a flesh wound.",
+ exceptions:black_knight(fun() -> exceptions:sword(5) end))].
+
+whoa_test() ->
+ ?assertEqual({caught, throw, up}, exceptions:whoa()).
+
+im_impressed_test() ->
+ ?assertEqual({caught, throw, up}, exceptions:im_impressed()).
+
+catcher_test_() ->
+ [?_assertEqual("uh oh", exceptions:catcher(1,0)),
+ ?_assertEqual(1.0, exceptions:catcher(3,3)),
+ ?_assertEqual(2.0, exceptions:catcher(6,3))].
+
+one_or_two_test_() ->
+ [?_assertEqual(return, exceptions:one_or_two(1)),
+ ?_assertEqual(return, catch exceptions:one_or_two(2)),
+ ?_assertException(throw, return, exceptions:one_or_two(2))].
diff --git a/learn-you-some-erlang/tests/fifo_tests.erl b/learn-you-some-erlang/tests/fifo_tests.erl
new file mode 100644
index 0000000..1252a1b
--- /dev/null
+++ b/learn-you-some-erlang/tests/fifo_tests.erl
@@ -0,0 +1,22 @@
+-module(fifo_tests).
+-include_lib("eunit/include/eunit.hrl").
+
+new_test() -> ?assertEqual({fifo,[],[]}, fifo:new()).
+
+push_test_() ->
+ [?_assertEqual({fifo,[1,3],[2,4]}, fifo:push({fifo,[3],[2,4]},1)),
+ ?_assertEqual({fifo,[2],[]}, fifo:push({fifo,[],[]},2))].
+
+pop_test_() ->
+ [?_assertEqual({3,{fifo,[],[2]}}, fifo:pop({fifo,[],[3,2]})),
+ ?_assertEqual({3,{fifo,[],[2]}}, fifo:pop({fifo,[2,3],[]})),
+ ?_assertEqual({3,{fifo,[2,1],[]}},fifo:pop({fifo,[2,1],[3]})),
+ ?_assertEqual({1,{fifo,[],[2,3]}},
+ fifo:pop(fifo:push(fifo:push(fifo:push(fifo:new(),1),2),3))),
+ ?_assertError('empty fifo', fifo:pop({fifo,[],[]}))].
+
+empty_test_() ->
+ [?_assertEqual(true, fifo:empty(fifo:new())),
+ ?_assertEqual(false, fifo:empty({fifo,[1],[]})),
+ ?_assertEqual(false, fifo:empty({fifo,[],[1]})),
+ ?_assertEqual(false, fifo:empty({fifo,[1],[2]}))].
diff --git a/learn-you-some-erlang/tests/functions_tests.erl b/learn-you-some-erlang/tests/functions_tests.erl
new file mode 100644
index 0000000..7973b9e
--- /dev/null
+++ b/learn-you-some-erlang/tests/functions_tests.erl
@@ -0,0 +1,27 @@
+-module(functions_tests).
+-include_lib("eunit/include/eunit.hrl").
+-test_warnings([valid_time_test/0]).
+
+head_test() -> ?assertEqual(1, functions:head([1,2,3,4])).
+
+second_test() -> ?assertEqual(2, functions:second([1,2,3,4])).
+
+same_test_() ->
+ [?_assertEqual(true, functions:same(a,a)),
+ ?_assertEqual(true, functions:same(12,12)),
+ ?_assertEqual(false, functions:same(a,b)),
+ ?_assertEqual(false, functions:same(12.0, 12))].
+
+%% no clean way to test valid_time's io stuff, so this one is p. much the
+%% same thing as the main objective was to test pattern matching.
+%% io:format should be used as least as possible to do testing :(
+valid_time({_Date = {_Y,_M,_D}, _Time = {_H,_Min,_S}}) ->
+ matches;
+valid_time(_) ->
+ nomatch.
+
+valid_time_test_() ->
+ [?_assertEqual(matches, valid_time({{2011,09,06},{09,04,43}})),
+ ?_assertEqual(nomatch, valid_time({{2011,09,06},{09,04}}))].
+
+
diff --git a/learn-you-some-erlang/tests/guards_tests.erl b/learn-you-some-erlang/tests/guards_tests.erl
new file mode 100644
index 0000000..d908bcf
--- /dev/null
+++ b/learn-you-some-erlang/tests/guards_tests.erl
@@ -0,0 +1,22 @@
+-module(guards_tests).
+-include_lib("eunit/include/eunit.hrl").
+
+old_enough_test_() ->
+ [?_assertEqual(true, guards:old_enough(16)),
+ ?_assertEqual(false, guards:old_enough(15)),
+ ?_assertEqual(false, guards:old_enough(-16))].
+
+right_age_test_() ->
+ [?_assertEqual(true, guards:right_age(16)),
+ ?_assertEqual(true, guards:right_age(104)),
+ ?_assertEqual(true, guards:right_age(50)),
+ ?_assertEqual(false, guards:right_age(15)),
+ ?_assertEqual(false, guards:right_age(105))].
+
+wrong_age_test_() ->
+ [?_assertEqual(false, guards:wrong_age(16)),
+ ?_assertEqual(false, guards:wrong_age(104)),
+ ?_assertEqual(false, guards:wrong_age(50)),
+ ?_assertEqual(true, guards:wrong_age(15)),
+ ?_assertEqual(true, guards:wrong_age(105))].
+
diff --git a/learn-you-some-erlang/tests/hhfuns_tests.erl b/learn-you-some-erlang/tests/hhfuns_tests.erl
new file mode 100644
index 0000000..c5c27cf
--- /dev/null
+++ b/learn-you-some-erlang/tests/hhfuns_tests.erl
@@ -0,0 +1,95 @@
+-module(hhfuns_tests).
+-include_lib("eunit/include/eunit.hrl").
+
+one_test() ->
+ ?assertEqual(1, hhfuns:one()).
+
+two_test() ->
+ ?assertEqual(2, hhfuns:two()).
+
+add_test_() ->
+ [?_assertEqual(3, hhfuns:add(fun hhfuns:one/0, fun hhfuns:two/0)),
+ ?_assertError({badfun, _}, hhfuns:add(1,2)),
+ ?_assertEqual(2, hhfuns:add(fun hhfuns:one/0, fun hhfuns:one/0))].
+
+increment_test() ->
+ ?assertEqual([1,2,3], hhfuns:increment([0,1,2])).
+
+decrement_test() ->
+ ?assertEqual([1,2,3], hhfuns:decrement([2,3,4])).
+
+map_test_() ->
+ [?_assertEqual([1,2,3], hhfuns:map(fun hhfuns:incr/1, [0,1,2])),
+ ?_assertEqual([1,2,3], hhfuns:map(fun hhfuns:decr/1, [2,3,4]))].
+
+bases_test_() ->
+ [?_assertEqual(12, hhfuns:base1(3)),
+ ?_assertError({badmatch, _}, hhfuns:base2()),
+ ?_assertEqual(2, hhfuns:base3())].
+
+closure_test() ->
+ ?assertEqual("a/0's password is pony", hhfuns:b(hhfuns:a())).
+
+even_test_() ->
+ [?_assertEqual([], hhfuns:even([])),
+ ?_assertEqual([], hhfuns:even([3,5,7])),
+ ?_assertEqual([2,4], hhfuns:even([1,2,3,4]))].
+
+old_test_() ->
+ L = [{male,45},{female,67},{male,66},{female,12},{unkown,174},{male,74}],
+ [?_assertEqual([{male,66},{male,74}], hhfuns:old_men(L)),
+ ?_assertEqual([], hhfuns:old_men([{male,45}, {female, -54}])),
+ ?_assertEqual([], hhfuns:old_men([]))].
+
+filter_test_() ->
+ L = [{male,45},{female,67},{male,66},{female,12},{unkown,174},{male,74}],
+ IsEven = fun(X) -> X rem 2 == 0 end,
+ IsOldMale = fun({Gender, Age}) -> Gender == male andalso Age > 60 end,
+ [?_assertEqual([], hhfuns:filter(IsEven, [])),
+ ?_assertEqual([], hhfuns:filter(IsEven, [3,5,7])),
+ ?_assertEqual([2,4], hhfuns:filter(IsEven, [1,2,3,4])),
+ ?_assertEqual([{male,66},{male,74}], hhfuns:filter(IsOldMale, L)),
+ ?_assertEqual([], hhfuns:filter(IsOldMale, [{male,45}, {female, -54}])),
+ ?_assertEqual([], hhfuns:filter(IsOldMale, []))].
+
+max_test_() ->
+ [?_assertEqual(3, hhfuns:max([1,2,3])),
+ ?_assertEqual(-1, hhfuns:max([-10,-1,-5.5])),
+ ?_assertError(function_clause, hhfuns:max([]))].
+
+min_test_() ->
+ [?_assertEqual(0, hhfuns:min([1,2,0,3])),
+ ?_assertEqual(-10, hhfuns:min([-10,-1,-5.5])),
+ ?_assertError(function_clause, hhfuns:min([]))].
+
+sum_test_() ->
+ [?_assertEqual(0, hhfuns:sum([])),
+ ?_assertEqual(6, hhfuns:sum([1,2,3]))].
+
+fold_test_() ->
+ [H|T] = [1,7,3,5,9,0,2,3],
+ [?_assertEqual(9,
+ hhfuns:fold(fun(A,B) when A > B -> A; (_,B) -> B end, H, T)),
+ ?_assertEqual(0,
+ hhfuns:fold(fun(A,B) when A < B -> A; (_,B) -> B end, H, T)),
+ ?_assertEqual(21, hhfuns:fold(fun(A,B) -> A + B end, 0, lists:seq(1,6)))].
+
+reverse_test_() ->
+ [?_assertEqual([3,2,1], hhfuns:reverse([1,2,3])),
+ ?_assertEqual([], hhfuns:reverse([]))].
+
+map2_test_() ->
+ [?_assertEqual([1,2,3], hhfuns:map2(fun hhfuns:incr/1, [0,1,2])),
+ ?_assertEqual([1,2,3], hhfuns:map2(fun hhfuns:decr/1, [2,3,4]))].
+
+
+filter2_test_() ->
+ L = [{male,45},{female,67},{male,66},{female,12},{unkown,174},{male,74}],
+ IsEven = fun(X) -> X rem 2 == 0 end,
+ IsOldMale = fun({Gender, Age}) -> Gender == male andalso Age > 60 end,
+ [?_assertEqual([], hhfuns:filter2(IsEven, [])),
+ ?_assertEqual([], hhfuns:filter2(IsEven, [3,5,7])),
+ ?_assertEqual([2,4], hhfuns:filter2(IsEven, [1,2,3,4])),
+ ?_assertEqual([{male,66},{male,74}], hhfuns:filter2(IsOldMale, L)),
+ ?_assertEqual([], hhfuns:filter2(IsOldMale, [{male,45}, {female, -54}])),
+ ?_assertEqual([], hhfuns:filter2(IsOldMale, []))].
diff --git a/learn-you-some-erlang/tests/kitchen_tests.erl b/learn-you-some-erlang/tests/kitchen_tests.erl
new file mode 100644
index 0000000..821f60c
--- /dev/null
+++ b/learn-you-some-erlang/tests/kitchen_tests.erl
@@ -0,0 +1,118 @@
+-module(kitchen_tests).
+-include_lib("eunit/include/eunit.hrl").
+
+fridge1_test_() ->
+ {"Tests fridge1 although the function is never run in the text",
+ {foreach,
+ fun() -> spawn(kitchen, fridge1, []) end,
+ fun(Pid) -> exit(Pid, kill) end,
+ [fun fridge1_store/1,
+ fun fridge1_take/1]
+ }
+ }.
+
+fridge1_store(Pid) ->
+ Pid ! {self(), {store, item}},
+ Reply = receive_or_timeout(),
+ [?_assertEqual({Pid, ok}, Reply)].
+
+fridge1_take(Pid) ->
+ Pid ! {self(), {take, item}},
+ Reply = receive_or_timeout(),
+ [?_assertEqual({Pid, not_found}, Reply)].
+
+
+fridge2_test_() ->
+ {"Tests fridge2",
+ {foreach,
+ fun() -> spawn(kitchen, fridge2, [[]]) end,
+ fun(Pid) -> exit(Pid, kill) end,
+ [fun fridge2_store/1,
+ fun fridge2_take_nostore/1,
+ fun fridge2_take_stored/1]
+ }
+ }.
+
+fridge2_store(Pid) ->
+ Pid ! {self(), {store, item}},
+ Pid ! {self(), {store, item2}},
+ Reply1 = receive_or_timeout(),
+ Reply2 = receive_or_timeout(),
+ [?_assertEqual({Pid, ok}, Reply1),
+ ?_assertEqual({Pid, ok}, Reply2)].
+
+fridge2_take_nostore(Pid) ->
+ Pid ! {self(), {take, item}},
+ Reply = receive_or_timeout(),
+ [?_assertEqual({Pid, not_found}, Reply)].
+
+fridge2_take_stored(Pid) ->
+ Pid ! {self(), {store, item}},
+ _ = receive_or_timeout(), % flush the 'ok' msg
+ Pid ! {self(), {take, item}},
+ Pid ! {self(), {take, item}}, % check if the food is removed
+ R1 = receive_or_timeout(),
+ R2 = receive_or_timeout(),
+ [?_assertEqual({Pid, {ok, item}}, R1),
+ ?_assertEqual({Pid, not_found}, R2)].
+
+
+abstractions_test_() ->
+ {"Tests the abstraction function we added to the script",
+ [{"Basic take/2 and store/2 abstractions. Can't test the hanging.",
+ {foreach,
+ fun() -> spawn(kitchen, fridge2, [[]]) end,
+ fun(Pid) -> exit(Pid, kill) end,
+ [fun store/1,
+ fun take/1]
+ }},
+ {"Start/1 abstraction tests. Reuses the take/2 and store/2 tests.",
+ {foreach,
+ fun() -> kitchen:start([]) end,
+ fun(Pid) -> exit(Pid, kill) end,
+ [fun store/1,
+ fun take/1]
+ }},
+ {"take2/2 and store2/2 tests.",
+ {timeout, 10,
+ {foreach,
+ fun() -> kitchen:start([]) end,
+ fun(Pid) -> exit(Pid, kill) end,
+ [fun store2/1,
+ fun take2/1]
+ }
+ }}
+ ]
+ }.
+
+store(Pid) ->
+ [?_assertEqual(ok, kitchen:store(Pid, item))].
+
+take(Pid) ->
+ kitchen:store(Pid, item),
+ R1 = kitchen:take(Pid, item),
+ R2 = kitchen:take(Pid, item),
+ [?_assertEqual({ok, item}, R1),
+ ?_assertEqual(not_found, R2)].
+
+store2(Pid) ->
+ R1 = kitchen:store2(c:pid(0,7,0), item),
+ R2 = kitchen:store2(Pid, item),
+ [?_assertEqual(timeout, R1),
+ ?_assertEqual(ok, R2)].
+
+take2(Pid) ->
+ kitchen:store2(Pid, item),
+ R1 = kitchen:take2(c:pid(0,7,0), item),
+ R2 = kitchen:take2(Pid, item),
+ R3 = kitchen:take2(Pid, item),
+ [?_assertEqual(timeout, R1),
+ ?_assertEqual({ok, item}, R2),
+ ?_assertEqual(not_found, R3)].
+
+receive_or_timeout() ->
+ receive
+ M -> M
+ after 1000 ->
+ timeout
+ end.
diff --git a/learn-you-some-erlang/tests/kitty_gen_server_tests.erl b/learn-you-some-erlang/tests/kitty_gen_server_tests.erl
new file mode 100644
index 0000000..e167158
--- /dev/null
+++ b/learn-you-some-erlang/tests/kitty_gen_server_tests.erl
@@ -0,0 +1,19 @@
+-module(kitty_gen_server_tests).
+-record(cat, {name, color=green, description}). % stolen from kitty_gen_server.erl
+-include_lib("eunit/include/eunit.hrl").
+-define(CAT1, #cat{name=a, color=b, description=c}).
+-define(CAT2, #cat{name=d, color=e, description=f}).
+
+order_test() ->
+ {ok, Pid} = kitty_gen_server:start_link(),
+ ?assertEqual(?CAT1, kitty_gen_server:order_cat(Pid, a, b, c)),
+ ?assertEqual(?CAT2, kitty_gen_server:order_cat(Pid, d, e, f)),
+ ?assertEqual(ok, kitty_gen_server:close_shop(Pid)).
+
+return_test() ->
+ {ok, Pid} = kitty_gen_server:start_link(),
+ ?assertEqual(ok, kitty_gen_server:return_cat(Pid, ?CAT1)),
+ ?assertEqual(?CAT1, kitty_gen_server:order_cat(Pid, d, e, f)),
+ ?assertEqual(?CAT2, kitty_gen_server:order_cat(Pid, d, e, f)),
+ ?assertEqual(ok, kitty_gen_server:close_shop(Pid)).
+
diff --git a/learn-you-some-erlang/tests/kitty_server2_tests.erl b/learn-you-some-erlang/tests/kitty_server2_tests.erl
new file mode 100644
index 0000000..a41594a
--- /dev/null
+++ b/learn-you-some-erlang/tests/kitty_server2_tests.erl
@@ -0,0 +1,23 @@
+-module(kitty_server2_tests).
+-record(cat, {name, color=green, description}). % stolen from kitty_server2.erl
+-include_lib("eunit/include/eunit.hrl").
+-define(CAT1, #cat{name=a, color=b, description=c}).
+-define(CAT2, #cat{name=d, color=e, description=f}).
+
+order_test() ->
+ Pid = kitty_server2:start_link(),
+ ?assertEqual(?CAT1, kitty_server2:order_cat(Pid, a, b, c)),
+ ?assertEqual(?CAT2, kitty_server2:order_cat(Pid, d, e, f)),
+ ?assertEqual(ok, kitty_server2:close_shop(Pid)).
+
+return_test() ->
+ Pid = kitty_server2:start_link(),
+ ?assertEqual(ok, kitty_server2:return_cat(Pid, ?CAT1)),
+ ?assertEqual(?CAT1, kitty_server2:order_cat(Pid, d, e, f)),
+ ?assertEqual(?CAT2, kitty_server2:order_cat(Pid, d, e, f)),
+ ?assertEqual(ok, kitty_server2:close_shop(Pid)).
+
+close_noproc_test() ->
+ DeadPid = spawn_link(fun() -> ok end),
+ timer:sleep(100),
+ ?assertError(noproc, kitty_server2:close_shop(DeadPid)).
diff --git a/learn-you-some-erlang/tests/kitty_server_tests.erl b/learn-you-some-erlang/tests/kitty_server_tests.erl
new file mode 100644
index 0000000..47b2561
--- /dev/null
+++ b/learn-you-some-erlang/tests/kitty_server_tests.erl
@@ -0,0 +1,23 @@
+-module(kitty_server_tests).
+-record(cat, {name, color=green, description}). % stolen from kitty_server.erl
+-include_lib("eunit/include/eunit.hrl").
+-define(CAT1, #cat{name=a, color=b, description=c}).
+-define(CAT2, #cat{name=d, color=e, description=f}).
+
+order_test() ->
+ Pid = kitty_server:start_link(),
+ ?assertEqual(?CAT1, kitty_server:order_cat(Pid, a, b, c)),
+ ?assertEqual(?CAT2, kitty_server:order_cat(Pid, d, e, f)),
+ ?assertEqual(ok, kitty_server:close_shop(Pid)).
+
+return_test() ->
+ Pid = kitty_server:start_link(),
+ ?assertEqual(ok, kitty_server:return_cat(Pid, ?CAT1)),
+ ?assertEqual(?CAT1, kitty_server:order_cat(Pid, d, e, f)),
+ ?assertEqual(?CAT2, kitty_server:order_cat(Pid, d, e, f)),
+ ?assertEqual(ok, kitty_server:close_shop(Pid)).
+
+close_noproc_test() ->
+ DeadPid = spawn_link(fun() -> ok end),
+ timer:sleep(100),
+ ?assertError(noproc, kitty_server:close_shop(DeadPid)).
diff --git a/learn-you-some-erlang/tests/linkmon_tests.erl b/learn-you-some-erlang/tests/linkmon_tests.erl
new file mode 100644
index 0000000..a04ee27
--- /dev/null
+++ b/learn-you-some-erlang/tests/linkmon_tests.erl
@@ -0,0 +1,43 @@
+-module(linkmon_tests).
+-include_lib("eunit/include/eunit.hrl").
+
+myproc_test_() ->
+ {timeout,
+ 7,
+ ?_assertEqual({'EXIT', reason},
+ catch linkmon:myproc())}.
+
+chain_test_() ->
+ {timeout,
+ 3,
+ ?_assertEqual(ok, chain_proc())}.
+
+chain_proc() ->
+ process_flag(trap_exit, true),
+ link(spawn(linkmon, chain, [3])),
+ receive
+ {'EXIT', _, "chain dies here"} -> ok
+ end.
+
+critic1_test_() ->
+ Critic = linkmon:start_critic(),
+ A = linkmon:judge(Critic, "Genesis", "The Lambda Lies Down on Broadway"),
+ exit(Critic, solar_storm),
+ B = linkmon:judge(Critic, "Genesis", "A trick of the Tail Recursion"),
+ [?_assertEqual("They are terrible!", A),
+ ?_assertEqual(timeout, B)].
+
+critic2_test_() ->
+ catch unregister(critic),
+ linkmon:start_critic2(),
+ timer:sleep(200),
+ A = linkmon:judge2("The Doors", "Light my Firewall"),
+ exit(whereis(critic), kill),
+ timer:sleep(200),
+ B = linkmon:judge2("Rage Against the Turing Machine", "Unit Testify"),
+ exit(whereis(critic), shutdown),
+ timer:sleep(200),
+ C = (catch linkmon:judge2("a", "b")),
+ [?_assertEqual("They are terrible!", A),
+ ?_assertEqual("They are great!", B),
+ ?_assertMatch({'EXIT', {badarg, _}}, C)].
diff --git a/learn-you-some-erlang/tests/multiproc_tests.erl b/learn-you-some-erlang/tests/multiproc_tests.erl
new file mode 100644
index 0000000..1020324
--- /dev/null
+++ b/learn-you-some-erlang/tests/multiproc_tests.erl
@@ -0,0 +1,28 @@
+-module(multiproc_tests).
+-include_lib("eunit/include/eunit.hrl").
+
+%% sleep's implementation is copy/pasted from the timer module.
+%% not much to test to be safe.
+sleep_test_() ->
+ [?_assertEqual(ok, multiproc:sleep(10))].
+
+flush_test_() ->
+ {spawn,
+ [fun() ->
+ self() ! a,
+ self() ! b,
+ ok = multiproc:flush(),
+ self() ! c,
+ [?assertEqual(receive M -> M end, c)]
+ end]}.
+
+priority_test_() ->
+ {spawn,
+ [fun() ->
+ self() ! {15, high},
+ self() ! {7, low},
+ self() ! {1, low},
+ self() ! {17, high},
+ [?assertEqual([high, high, low, low],
+ multiproc:important())]
+ end]}.
diff --git a/learn-you-some-erlang/tests/musicians_tests.erl b/learn-you-some-erlang/tests/musicians_tests.erl
new file mode 100644
index 0000000..b1b4254
--- /dev/null
+++ b/learn-you-some-erlang/tests/musicians_tests.erl
@@ -0,0 +1,56 @@
+%% WARNING: THESE TESTS TAKE A LONG TIME TO RUN
+-module(musicians_tests).
+-include_lib("eunit/include/eunit.hrl").
+-define(INSTRUMENTS, [a,b,c,d,e,f,g,h]).
+
+rand_name_test_() ->
+ {"Make sure that random names are generated",
+ {setup,
+ fun setup_many_good/0,
+ fun teardown_many/1,
+ fun test_names/1}}.
+
+eventual_crash_test_() ->
+ {"Checks that bad musicians die at some point, while"
+ "good ones don't",
+ {inparallel,
+ [{timeout, 20000, crash()},
+ {timeout, 20000, nocrash()}]}}.
+
+crash() ->
+ {ok, Pid} = musicians:start_link(drum, bad),
+ Ref = erlang:monitor(process, Pid),
+ unlink(Pid),
+ Rec = receive
+ {'DOWN', Ref, process, Pid, _R} -> ok
+ after 19000 -> timeout
+ end,
+ ?_assertEqual(ok, Rec).
+
+nocrash() ->
+ {ok, Pid} = musicians:start_link(carhorn, good),
+ Ref = erlang:monitor(process, Pid),
+ unlink(Pid),
+ Rec = receive
+ {'DOWN', Ref, process, Pid, _R} -> ok
+ after 19000 -> musicians:stop(carhorn), timeout
+ end,
+ ?_assertEqual(timeout, Rec).
+
+setup_many_good() ->
+ [element(2,musicians:start_link(X, good)) ||
+ X <- ?INSTRUMENTS].
+
+teardown_many(_) ->
+ [musicians:stop(X) || X <- ?INSTRUMENTS].
+
+test_names(Musicians) ->
+ Names = [find_name(M) || M <- Musicians],
+ SetNames = ordsets:to_list(ordsets:from_list(Names)),
+ ?_assert(2 < length(SetNames)). % totally arbitrary ratio
+
+find_name(Inst) ->
+ {status, _Pid, _, [_Dict, _Status, _Ancestor, _,
+ [_Header, _,
+ {data, [{"State", {state, Name, _, _}}]}]]} = sys:get_status(Inst),
+ Name.
diff --git a/learn-you-some-erlang/tests/oop_tests.erl b/learn-you-some-erlang/tests/oop_tests.erl
new file mode 100644
index 0000000..6ea444a
--- /dev/null
+++ b/learn-you-some-erlang/tests/oop_tests.erl
@@ -0,0 +1,16 @@
+-module(oop_tests).
+-include_lib("eunit/include/eunit.hrl").
+
+shell_test_() ->
+ Bird = oop:animal("Bird"),
+ Dog = oop:dog("Raptor-Dog"),
+ Cat = oop:cat("Sgt. McMittens"),
+ [?_assertEqual("living thing", Bird(type)),
+ ?_assertEqual("Bird eats worm", Bird({eat, "worm"})),
+ ?_assertEqual("Raptor-Dog says: Woof!", Dog(talk)),
+ ?_assertEqual("Raptor-Dog", Dog(name)),
+ ?_assertEqual("cat", Cat(type)),
+ ?_assertEqual("Raptor-Dog chases a cat named Sgt. McMittens around",
+ Dog({chase, Cat})),
+ ?_assertEqual("I'm sorry Dave, I can't do that.", Cat({play, "yarn"}))].
+
diff --git a/learn-you-some-erlang/tests/records_tests.erl b/learn-you-some-erlang/tests/records_tests.erl
new file mode 100644
index 0000000..327120f
--- /dev/null
+++ b/learn-you-some-erlang/tests/records_tests.erl
@@ -0,0 +1,46 @@
+-module(records_tests).
+-include_lib("eunit/include/eunit.hrl").
+
+first_robot_test_() ->
+ ?_assertEqual(records:first_robot(),
+ {robot,
+ "Mechatron",
+ handmade,
+ undefined,
+ ["Moved by a small man inside"]}).
+
+car_factory_test_() ->
+ ?_assertEqual(records:car_factory("Jokeswagen"),
+ {robot,
+ "Jokeswagen",
+ industrial,
+ "building cars",
+ []}).
+
+repairman_test_() ->
+ ?_assertEqual(records:repairman({robot,
+ "Ulbert",
+ industrial,
+ ["trying to have feelings"],
+ []}),
+ {repaired, {robot,
+ "Ulbert",
+ industrial,
+ ["trying to have feelings"],
+ ["Repaired by repairman"]}}).
+
+admin_panel_test_() ->
+ [?_assertEqual(records:admin_panel({user, 1, "ferd", admin, 96}),
+ "ferd is allowed!"),
+ ?_assertEqual(records:admin_panel({user, 2, "you", users, 66}),
+ "you is not allowed")].
+
+adult_section_test_() ->
+ [?_assertEqual(records:adult_section({user, 21, "Bill", users, 72}),
+ allowed),
+ ?_assertEqual(records:adult_section({user, 22, "Noah", users, 13}),
+ forbidden)].
+
+included_test_() ->
+ ?_assertEqual(records:included(),
+ {included, "Some value", "yeah!", undefined}).
diff --git a/learn-you-some-erlang/tests/recursive_tests.erl b/learn-you-some-erlang/tests/recursive_tests.erl
new file mode 100644
index 0000000..554c691
--- /dev/null
+++ b/learn-you-some-erlang/tests/recursive_tests.erl
@@ -0,0 +1,109 @@
+-module(recursive_tests).
+-include_lib("eunit/include/eunit.hrl").
+
+%% those were not in the module, but yeah
+
+fac_test_() ->
+ [?_assert(24 == recursive:fac(4)),
+ ?_assert(1 == recursive:fac(0)),
+ ?_assert(1 == recursive:fac(1)),
+ ?_assertError(function_clause, recursive:fac(-1))].
+
+tail_fac_test_() ->
+ [?_assert(recursive:fac(4) == recursive:tail_fac(4)),
+ ?_assert(recursive:fac(0) == recursive:tail_fac(0)),
+ ?_assert(recursive:fac(1) == recursive:tail_fac(1)),
+ ?_assertError(function_clause, recursive:tail_fac(-1))].
+
+len_test_() ->
+ [?_assert(1 == recursive:len([a])),
+ ?_assert(0 == recursive:len([])),
+ ?_assert(5 == recursive:len([1,2,3,4,5]))].
+
+tail_len_test_() ->
+ [?_assert(recursive:len([a]) == recursive:tail_len([a])),
+ ?_assert(recursive:len([]) == recursive:tail_len([])),
+ ?_assert(recursive:len([1,2,3,4,5]) == recursive:tail_len([1,2,3,4,5]))].
+
+duplicate_test_() ->
+ [?_assert([] == recursive:duplicate(0,a)),
+ ?_assert([a] == recursive:duplicate(1,a)),
+ ?_assert([a,a,a] == recursive:duplicate(3,a))].
+
+tail_duplicate_test_() ->
+ [?_assert(recursive:tail_duplicate(0,a) == recursive:duplicate(0,a)),
+ ?_assert(recursive:tail_duplicate(1,a) == recursive:duplicate(1,a)),
+ ?_assert(recursive:tail_duplicate(3,a) == recursive:duplicate(3,a))].
+
+reverse_test_() ->
+ [?_assert([] == recursive:reverse([])),
+ ?_assert([1] == recursive:reverse([1])),
+ ?_assert([3,2,1] == recursive:reverse([1,2,3]))].
+
+tail_reverse_test_() ->
+ [?_assertEqual(recursive:tail_reverse([]),
+ recursive:reverse([])),
+ ?_assertEqual(recursive:tail_reverse([1]),
+ recursive:reverse([1])),
+ ?_assertEqual(recursive:tail_reverse([1,2,3]),
+ recursive:reverse([1,2,3]))].
+
+sublist_test_() ->
+ [?_assert([] == recursive:sublist([1,2,3],0)),
+ ?_assert([1,2] == recursive:sublist([1,2,3],2)),
+ ?_assert([] == recursive:sublist([], 4))].
+
+tail_sublist_test_() ->
+ [?_assertEqual(recursive:tail_sublist([1,2,3],0),
+ recursive:sublist([1,2,3],0)),
+ ?_assertEqual(recursive:tail_sublist([1,2,3],2),
+ recursive:sublist([1,2,3],2)),
+ ?_assertEqual(recursive:tail_sublist([], 4),
+ recursive:sublist([], 4))].
+
+zip_test_() ->
+ [?_assert([{a,1},{b,2},{c,3}] == recursive:zip([a,b,c],[1,2,3])),
+ ?_assert([] == recursive:zip([],[])),
+ ?_assertError(function_clause, recursive:zip([1],[1,2]))].
+
+lenient_zip_test_() ->
+ [?_assertEqual([{a,1},{b,2},{c,3}],
+ recursive:lenient_zip([a,b,c],[1,2,3])),
+ ?_assert([] == recursive:lenient_zip([],[])),
+ ?_assert([{a,1}] == recursive:lenient_zip([a],[1,2]))].
+
+%% exercises!
+tail_zip_test_() ->
+ [?_assertEqual(recursive:tail_zip([a,b,c],[1,2,3]),
+ recursive:zip([a,b,c],[1,2,3])),
+ ?_assertEqual(recursive:tail_zip([],[]),
+ recursive:zip([],[])),
+ ?_assertError(function_clause, recursive:tail_zip([1],[1,2]))].
+
+tail_lenient_zip_test_() ->
+ [?_assertEqual(recursive:tail_lenient_zip([a,b,c],[1,2,3]),
+ recursive:lenient_zip([a,b,c],[1,2,3])),
+ ?_assertEqual(recursive:tail_lenient_zip([],[]),
+ recursive:lenient_zip([],[])),
+ ?_assertEqual(recursive:tail_lenient_zip([a],[1,2]),
+ recursive:lenient_zip([a],[1,2]))].
+
+%% quick, sort!
+quicksort_test_() ->
+ [?_assert([] == recursive:quicksort([])),
+ ?_assert([1] == recursive:quicksort([1])),
+ ?_assert([1,2,2,4,6] == recursive:quicksort([4,2,6,2,1])),
+ ?_assert(" JRaceeinqqsuu" == recursive:quicksort("Jacques Requin"))].
+
+lc_quicksort_test_() ->
+ [?_assert([] == recursive:lc_quicksort([])),
+ ?_assert([1] == recursive:lc_quicksort([1])),
+ ?_assert([1,2,2,4,6] == recursive:lc_quicksort([4,2,6,2,1])),
+ ?_assert(" JRaceeinqqsuu" == recursive:lc_quicksort("Jacques Requin"))].
+
+bestest_qsort_test_() ->
+ [?_assert([] == recursive:bestest_qsort([])),
+ ?_assert([1] == recursive:bestest_qsort([1])),
+ ?_assert([1,2,2,4,6] == recursive:bestest_qsort([4,2,6,2,1])),
+ ?_assert(" JRaceeinqqsuu" == recursive:bestest_qsort("Jacques Requin"))].
+
diff --git a/learn-you-some-erlang/tests/road_tests.erl b/learn-you-some-erlang/tests/road_tests.erl
new file mode 100644
index 0000000..e99eeff
--- /dev/null
+++ b/learn-you-some-erlang/tests/road_tests.erl
@@ -0,0 +1,33 @@
+-module(road_tests).
+-include_lib("eunit/include/eunit.hrl").
+-test_warnings([main/1]).
+
+group_vals_test_() ->
+ [?_assertEqual([{a,b,x},{a,b,x}], road:group_vals([a,b,x,a,b,x],[])),
+ ?_assertEqual([], road:group_vals([],[])),
+ ?_assertError(function_clause, road:group_vals([a,b,x,a],[]))].
+
+parse_map_test_() ->
+ [?_assertEqual([], road:parse_map("")),
+ ?_assertEqual([], road:parse_map(<<"">>)),
+ ?_assertEqual([{10,5,4}], road:parse_map("10 5 4")),
+ ?_assertEqual([{10,5,4},{1,2,3}], road:parse_map("10 5 4 1 2 3")),
+ ?_assertEqual([{10,5,4}], road:parse_map(<<"10 5 4">>)),
+ ?_assertEqual([{10,5,4}], road:parse_map("10\t5\n4")),
+ ?_assertEqual([{10,5,4},{1,2,3}],
+ road:parse_map("10\r\n5 4 1\t\t2\r3"))].
+
+%% little testing required on this one, the optimal_path tests will
+%% do it in a hidden manner.
+shortest_step_test_() ->
+ [?_assertEqual({{1,[{a,1}]},{2,[{x,1},{a,1}]}},
+ road:shortest_step({1,8,1},{{0,[]},{0,[]}}))].
+
+optimal_path_test_() ->
+ [?_assertEqual([{b,10},{x,30},{a,5},{x,20},{b,2},{b,8}],
+ road:optimal_path(
+ road:parse_map("50 10 30 5 90 20 40 2 24 10 8 0"))),
+ ?_assertEqual([{a,1},{a,1},{a,1}],
+ road:optimal_path([{1,10,2},{1,3,3},{1,2,0}])),
+ ?_assertEqual([{a,1},{x,1},{b,1},{x,1},{a,1}],
+ road:optimal_path([{1,3,1},{4,1,1},{1,6,1}]))].
diff --git a/learn-you-some-erlang/tests/tree_tests.erl b/learn-you-some-erlang/tests/tree_tests.erl
new file mode 100644
index 0000000..5a2633b
--- /dev/null
+++ b/learn-you-some-erlang/tests/tree_tests.erl
@@ -0,0 +1,64 @@
+-module(tree_tests).
+-include_lib("eunit/include/eunit.hrl").
+
+empty_test() ->
+ ?assert({node, 'nil'} =:= tree:empty()).
+
+%% oh god this gets ugly
+insert_test_() ->
+ T1 = tree:insert(a, "a", tree:empty()),
+ T2 = tree:insert(c, "c", T1),
+ T3 = tree:insert(n, "n",
+ tree:insert(z, "z",
+ tree:insert(t, "t",
+ tree:insert(x, "x", T2)))),
+ [?_assertEqual(T1, {node, {a,"a",{node,nil},{node,nil}}}),
+ ?_assertEqual(T2, {node, {a,"a",
+ {node,nil},
+ {node, {c,"c",{node,nil},{node,nil}}}}}),
+ ?_assertEqual(T3, {node, {a,"a",
+ {node, nil},
+ {node, {c,"c",
+ {node, nil},
+ {node, {x,"x",
+ {node, {t,"t",
+ {node, {n,"n",
+ {node,nil},
+ {node,nil}}},
+ {node, nil}}},
+ {node, {z,"z",
+ {node,nil},
+ {node,nil}}}}}}}}})].
+%% not as bad!
+lookup_test_() ->
+ T = tree:insert(x, "x",
+ tree:insert(t, "t",
+ tree:insert(z, "z",
+ tree:insert(n, "n",
+ tree:insert(c, "c",
+ tree:insert(a, "a", tree:empty())))))),
+ [?_assert({ok,"t"} == tree:lookup(t,T)),
+ ?_assert(undefined == tree:lookup(21, T)),
+ ?_assert(undefined == tree:lookup(a, tree:empty()))].
+
+%% done with both insert and lookup
+update_test_() ->
+ T1 = tree:insert(x, "x",
+ tree:insert(t, "t",
+ tree:insert(z, "z",
+ tree:insert(n, "n",
+ tree:insert(c, "c",
+ tree:insert(a, "a", tree:empty())))))),
+ T2 = tree:insert(x, "X", T1),
+ [?_assertEqual({ok, "x"}, tree:lookup(x, T1)),
+ ?_assertEqual({ok, "X"}, tree:lookup(x, T2))].
+
+has_value_test_() ->
+ T1 = tree:insert(x, "x",
+ tree:insert(t, "t",
+ tree:insert(z, "z",
+ tree:insert(n, "n",
+ tree:insert(c, "c",
+ tree:insert(a, "a", tree:empty())))))),
+ [?_assertEqual(true, tree:has_value("z", T1)),
+ ?_assertEqual(false,tree:has_value("Z", T1))].
diff --git a/learn-you-some-erlang/tests/useless_tests.erl b/learn-you-some-erlang/tests/useless_tests.erl
new file mode 100644
index 0000000..8f174be
--- /dev/null
+++ b/learn-you-some-erlang/tests/useless_tests.erl
@@ -0,0 +1,18 @@
+-module(useless_tests).
+-include_lib("eunit/include/eunit.hrl").
+-test_warnings([hello_test/0]).
+
+add_test_() ->
+ [?_assertEqual(-5, useless:add(-3, -2)),
+ ?_assertEqual(4, useless:add(2, 2)),
+ ?_assertEqual(2.5, useless:add(2.0, 0.5)),
+ ?_assertEqual(1, useless:add(-3, 4))].
+
+hello_test() ->
+ ok. % no test possible for I/O. Curse you, side effects!
+
+greet_and_add_test_() ->
+ [?_assertEqual(useless:greet_and_add_two(-3), useless:add(-3, 2)),
+ ?_assertEqual(useless:greet_and_add_two(2), useless:add(2, 2)),
+ ?_assertEqual(useless:greet_and_add_two(0.5), useless:add(2, 0.5)),
+ ?_assertEqual(useless:greet_and_add_two(-3), useless:add(-3, 2))].
diff --git a/learn-you-some-erlang/tests/what_the_if_tests.erl b/learn-you-some-erlang/tests/what_the_if_tests.erl
new file mode 100644
index 0000000..13465e9
--- /dev/null
+++ b/learn-you-some-erlang/tests/what_the_if_tests.erl
@@ -0,0 +1,17 @@
+-module(what_the_if_tests).
+-include_lib("eunit/include/eunit.hrl").
+
+heh_fine_test() ->
+ ?assertException(error, if_clause, what_the_if:heh_fine()).
+
+oh_god_test_() ->
+ [?_assertEqual(might_succeed, what_the_if:oh_god(2)),
+ ?_assertEqual(always_does, what_the_if:oh_god(3))].
+
+help_me_test_() ->
+ [?_assertEqual({cat, "says meow!"}, what_the_if:help_me(cat)),
+ ?_assertEqual({beef, "says mooo!"}, what_the_if:help_me(beef)),
+ ?_assertEqual({dog, "says bark!"}, what_the_if:help_me(dog)),
+ ?_assertEqual({tree, "says bark!"}, what_the_if:help_me(tree)),
+ ?_assertEqual({"other", "says fgdadfgna!"}, what_the_if:help_me("other")),
+ ?_assertEqual({5, "says fgdadfgna!"}, what_the_if:help_me(5))].
diff --git a/learn-you-some-erlang/trade/trade_calls.erl b/learn-you-some-erlang/trade/trade_calls.erl
new file mode 100644
index 0000000..e72c891
--- /dev/null
+++ b/learn-you-some-erlang/trade/trade_calls.erl
@@ -0,0 +1,144 @@
+-module(trade_calls).
+-compile(export_all).
+
+%% test a little bit of everything and also deadlocks on ready state
+%% -- leftover messages possible on race conditions on ready state
+main_ab() ->
+ S = self(),
+ PidCliA = spawn(fun() -> a(S) end),
+ receive PidA -> PidA end,
+ spawn(fun() -> b(PidA, PidCliA) end).
+
+a(Parent) ->
+ {ok, Pid} = trade_fsm:start_link("Carl"),
+ Parent ! Pid,
+ io:format("Spawned Carl: ~p~n", [Pid]),
+ %sys:trace(Pid,true),
+ timer:sleep(800),
+ trade_fsm:accept_trade(Pid),
+ timer:sleep(400),
+ io:format("~p~n",[trade_fsm:ready(Pid)]),
+ timer:sleep(1000),
+ trade_fsm:make_offer(Pid, "horse"),
+ trade_fsm:make_offer(Pid, "sword"),
+ timer:sleep(1000),
+ io:format("a synchronizing~n"),
+ sync2(),
+ trade_fsm:ready(Pid),
+ timer:sleep(200),
+ trade_fsm:ready(Pid),
+ timer:sleep(1000).
+
+b(PidA, PidCliA) ->
+ {ok, Pid} = trade_fsm:start_link("Jim"),
+ io:format("Spawned Jim: ~p~n", [Pid]),
+ %sys:trace(Pid,true),
+ timer:sleep(500),
+ trade_fsm:trade(Pid, PidA),
+ trade_fsm:make_offer(Pid, "boots"),
+ timer:sleep(200),
+ trade_fsm:retract_offer(Pid, "boots"),
+ timer:sleep(500),
+ trade_fsm:make_offer(Pid, "shotgun"),
+ timer:sleep(1000),
+ io:format("b synchronizing~n"),
+ sync1(PidCliA),
+ trade_fsm:make_offer(Pid, "horse"), %% race condition!
+ trade_fsm:ready(Pid),
+ timer:sleep(200),
+ timer:sleep(1000).
+
+%% force a race condition on cd trade negotiation
+main_cd() ->
+ S = self(),
+ PidCliC = spawn(fun() -> c(S) end),
+ receive PidC -> PidC end,
+ spawn(fun() -> d(S, PidC, PidCliC) end),
+ receive PidD -> PidD end,
+ PidCliC ! PidD.
+
+c(Parent) ->
+ {ok, Pid} = trade_fsm:start_link("Marc"),
+ Parent ! Pid,
+ receive PidD -> PidD end,
+ io:format("Spawned Marc: ~p~n", [Pid]),
+ %sys:trace(Pid, true),
+ sync2(),
+ trade_fsm:trade(Pid, PidD),
+ %% no need to accept_trade thanks to the race condition
+ timer:sleep(200),
+ trade_fsm:retract_offer(Pid, "car"),
+ trade_fsm:make_offer(Pid, "horse"),
+ timer:sleep(600),
+ trade_fsm:cancel(Pid),
+ timer:sleep(1000).
+
+d(Parent, PidC, PidCliC) ->
+ {ok, Pid} = trade_fsm:start_link("Pete"),
+ Parent ! Pid,
+ io:format("Spawned Jim: ~p~n", [Pid]),
+ %sys:trace(Pid,true),
+ sync1(PidCliC),
+ trade_fsm:trade(Pid, PidC),
+ %% no need to accept_trade thanks to the race condition
+ timer:sleep(200),
+ trade_fsm:retract_offer(Pid, "car"),
+ trade_fsm:make_offer(Pid, "manatee"),
+ timer:sleep(100),
+ trade_fsm:ready(Pid),
+ timer:sleep(1000).
+
+main_ef() ->
+ S = self(),
+ PidCliE = spawn(fun() -> e(S) end),
+ receive PidE -> PidE end,
+ spawn(fun() -> f(PidE, PidCliE) end).
+
+e(Parent) ->
+ {ok, Pid} = trade_fsm:start_link("Carl"),
+ Parent ! Pid,
+ io:format("Spawned Carl: ~p~n", [Pid]),
+ %sys:trace(Pid,true),
+ timer:sleep(800),
+ trade_fsm:accept_trade(Pid),
+ timer:sleep(400),
+ io:format("~p~n",[trade_fsm:ready(Pid)]),
+ timer:sleep(1000),
+ trade_fsm:make_offer(Pid, "horse"),
+ trade_fsm:make_offer(Pid, "sword"),
+ timer:sleep(1000),
+ io:format("a synchronizing~n"),
+ sync2(),
+ trade_fsm:ready(Pid),
+ timer:sleep(200),
+ trade_fsm:ready(Pid),
+ timer:sleep(1000).
+
+f(PidE, PidCliE) ->
+ {ok, Pid} = trade_fsm:start_link("Jim"),
+ io:format("Spawned Jim: ~p~n", [Pid]),
+ %sys:trace(Pid,true),
+ timer:sleep(500),
+ trade_fsm:trade(Pid, PidE),
+ trade_fsm:make_offer(Pid, "boots"),
+ timer:sleep(200),
+ trade_fsm:retract_offer(Pid, "boots"),
+ timer:sleep(500),
+ trade_fsm:make_offer(Pid, "shotgun"),
+ timer:sleep(1000),
+ io:format("b synchronizing~n"),
+ sync1(PidCliE),
+ trade_fsm:make_offer(Pid, "horse"),
+ timer:sleep(200),
+ trade_fsm:ready(Pid),
+ timer:sleep(1000).
+
+%%% Utils
+sync1(Pid) ->
+ Pid ! self(),
+ receive ack -> ok end.
+
+sync2() ->
+ receive
+ From -> From ! ack
+ end.
diff --git a/learn-you-some-erlang/trade/trade_fsm.erl b/learn-you-some-erlang/trade/trade_fsm.erl
new file mode 100644
index 0000000..fc66870
--- /dev/null
+++ b/learn-you-some-erlang/trade/trade_fsm.erl
@@ -0,0 +1,359 @@
+-module(trade_fsm).
+-behaviour(gen_fsm).
+
+%% public API
+-export([start/1, start_link/1, trade/2, accept_trade/1,
+ make_offer/2, retract_offer/2, ready/1, cancel/1]).
+%% gen_fsm callbacks
+-export([init/1, handle_event/3, handle_sync_event/4, handle_info/3,
+ terminate/3, code_change/4,
+ % custom state names
+ idle/2, idle/3, idle_wait/2, idle_wait/3, negotiate/2,
+ negotiate/3, wait/2, ready/2, ready/3]).
+
+-record(state, {name="",
+ other,
+ ownitems=[],
+ otheritems=[],
+ monitor,
+ from}).
+
+%%% PUBLIC API
+start(Name) ->
+ gen_fsm:start(?MODULE, [Name], []).
+
+start_link(Name) ->
+ gen_fsm:start_link(?MODULE, [Name], []).
+
+%% ask for a begin session. Returns when/if the other accepts
+trade(OwnPid, OtherPid) ->
+ gen_fsm:sync_send_event(OwnPid, {negotiate, OtherPid}, 30000).
+
+%% Accept someone's trade offer.
+accept_trade(OwnPid) ->
+ gen_fsm:sync_send_event(OwnPid, accept_negotiate).
+
+%% Send an item on the table to be traded
+make_offer(OwnPid, Item) ->
+ gen_fsm:send_event(OwnPid, {make_offer, Item}).
+
+%% Cancel trade offer
+retract_offer(OwnPid, Item) ->
+ gen_fsm:send_event(OwnPid, {retract_offer, Item}).
+
+%% Mention that you're ready for a trade. When the other
+%% player also declares being ready, the trade is done
+ready(OwnPid) ->
+ gen_fsm:sync_send_event(OwnPid, ready, infinity).
+
+%% Cancel the transaction.
+cancel(OwnPid) ->
+ gen_fsm:sync_send_all_state_event(OwnPid, cancel).
+
+%%% CLIENT-TO-CLIENT API
+%% These calls are only listed for the gen_fsm to call
+%% among themselves
+%% All calls are asynchronous to avoid deadlocks
+
+%% Ask the other FSM for a trade session
+ask_negotiate(OtherPid, OwnPid) ->
+ gen_fsm:send_event(OtherPid, {ask_negotiate, OwnPid}).
+
+%% Forward the client message accepting the transaction
+accept_negotiate(OtherPid, OwnPid) ->
+ gen_fsm:send_event(OtherPid, {accept_negotiate, OwnPid}).
+
+%% forward a client's offer
+do_offer(OtherPid, Item) ->
+ gen_fsm:send_event(OtherPid, {do_offer, Item}).
+
+%% forward a client's offer cancellation
+undo_offer(OtherPid, Item) ->
+ gen_fsm:send_event(OtherPid, {undo_offer, Item}).
+
+%% Ask the other side if he's ready to trade.
+are_you_ready(OtherPid) ->
+ gen_fsm:send_event(OtherPid, are_you_ready).
+
+%% Reply that the side is not ready to trade
+%% i.e. is not in 'wait' state.
+not_yet(OtherPid) ->
+ gen_fsm:send_event(OtherPid, not_yet).
+
+%% Tells the other fsm that the user is currently waiting
+%% for the ready state. State should transition to 'ready'
+am_ready(OtherPid) ->
+ gen_fsm:send_event(OtherPid, 'ready!').
+
+%% Acknowledge that the fsm is in a ready state.
+ack_trans(OtherPid) ->
+ gen_fsm:send_event(OtherPid, ack).
+
+%% ask if ready to commit
+ask_commit(OtherPid) ->
+ gen_fsm:sync_send_event(OtherPid, ask_commit).
+
+%% begin the synchronous commit
+do_commit(OtherPid) ->
+ gen_fsm:sync_send_event(OtherPid, do_commit).
+
+%% Make the other FSM aware that your client cancelled the trade
+notify_cancel(OtherPid) ->
+ gen_fsm:send_all_state_event(OtherPid, cancel).
+
+%%% GEN_FSM API
+init(Name) ->
+ {ok, idle, #state{name=Name}}.
+
+
+%% idle state is the state before any trade is done.
+%% The other player asks for a negotiation. We basically
+%% only wait for our own user to accept the trade,
+%% and store the other's Pid for future uses
+idle({ask_negotiate, OtherPid}, S=#state{}) ->
+ Ref = monitor(process, OtherPid),
+ notice(S, "~p asked for a trade negotiation", [OtherPid]),
+ {next_state, idle_wait, S#state{other=OtherPid, monitor=Ref}};
+idle(Event, Data) ->
+ unexpected(Event, idle),
+ {next_state, idle, Data}.
+
+%% trade call coming from the user. Forward to the other side,
+%% forward it and store the other's Pid
+idle({negotiate, OtherPid}, From, S=#state{}) ->
+ ask_negotiate(OtherPid, self()),
+ notice(S, "asking user ~p for a trade", [OtherPid]),
+ Ref = monitor(process, OtherPid),
+ {next_state, idle_wait, S#state{other=OtherPid, monitor=Ref, from=From}};
+idle(Event, _From, Data) ->
+ unexpected(Event, idle),
+ {next_state, idle, Data}.
+
+%% idle_wait allows to expect replies from the other side and
+%% start negotiating for items
+
+%% the other side asked for a negotiation while we asked for it too.
+%% this means both definitely agree to the idea of doing a trade.
+%% Both sides can assume the other feels the same!
+idle_wait({ask_negotiate, OtherPid}, S=#state{other=OtherPid}) ->
+ gen_fsm:reply(S#state.from, ok),
+ notice(S, "starting negotiation", []),
+ {next_state, negotiate, S};
+%% The other side has accepted our offer. Move to negotiate state
+idle_wait({accept_negotiate, OtherPid}, S=#state{other=OtherPid}) ->
+ gen_fsm:reply(S#state.from, ok),
+ notice(S, "starting negotiation", []),
+ {next_state, negotiate, S};
+%% different call from someone else. Not supported! Let it die.
+idle_wait(Event, Data) ->
+ unexpected(Event, idle_wait),
+ {next_state, idle_wait, Data}.
+
+%% Our own client has decided to accept the transaction.
+%% Make the other FSM aware of it and move to negotiate state.
+idle_wait(accept_negotiate, _From, S=#state{other=OtherPid}) ->
+ accept_negotiate(OtherPid, self()),
+ notice(S, "accepting negotiation", []),
+ {reply, ok, negotiate, S};
+idle_wait(Event, _From, Data) ->
+ unexpected(Event, idle_wait),
+ {next_state, idle_wait, Data}.
+
+%% own side offering an item
+negotiate({make_offer, Item}, S=#state{ownitems=OwnItems}) ->
+ do_offer(S#state.other, Item),
+ notice(S, "offering ~p", [Item]),
+ {next_state, negotiate, S#state{ownitems=add(Item, OwnItems)}};
+%% Own side retracting an item offer
+negotiate({retract_offer, Item}, S=#state{ownitems=OwnItems}) ->
+ undo_offer(S#state.other, Item),
+ notice(S, "cancelling offer on ~p", [Item]),
+ {next_state, negotiate, S#state{ownitems=remove(Item, OwnItems)}};
+%% other side offering an item
+negotiate({do_offer, Item}, S=#state{otheritems=OtherItems}) ->
+ notice(S, "other player offering ~p", [Item]),
+ {next_state, negotiate, S#state{otheritems=add(Item, OtherItems)}};
+%% other side retracting an item offer
+negotiate({undo_offer, Item}, S=#state{otheritems=OtherItems}) ->
+ notice(S, "Other player cancelling offer on ~p", [Item]),
+ {next_state, negotiate, S#state{otheritems=remove(Item, OtherItems)}};
+%% Other side has declared itself ready. Our own FSM should tell it to
+%% wait (with not_yet/1).
+negotiate(are_you_ready, S=#state{other=OtherPid}) ->
+ io:format("Other user ready to trade.~n"),
+ notice(S,
+ "Other user ready to transfer goods:~n"
+ "You get ~p, The other side gets ~p",
+ [S#state.otheritems, S#state.ownitems]),
+ not_yet(OtherPid),
+ {next_state, negotiate, S};
+negotiate(Event, Data) ->
+ unexpected(Event, negotiate),
+ {next_state, negotiate, Data}.
+
+%% own user mentioning he is ready. Next state should be wait
+%% and we add the 'from' to the state so we can reply to the
+%% user once ready.
+negotiate(ready, From, S = #state{other=OtherPid}) ->
+ are_you_ready(OtherPid),
+ notice(S, "asking if ready, waiting", []),
+ {next_state, wait, S#state{from=From}};
+negotiate(Event, _From, S) ->
+ unexpected(Event, negotiate),
+ {next_state, negotiate, S}.
+
+%% other side offering an item. Don't forget our client is still
+%% waiting for a reply, so let's tell them the trade state changed
+%% and move back to the negotiate state
+wait({do_offer, Item}, S=#state{otheritems=OtherItems}) ->
+ gen_fsm:reply(S#state.from, offer_changed),
+ notice(S, "other side offering ~p", [Item]),
+ {next_state, negotiate, S#state{otheritems=add(Item, OtherItems)}};
+%% other side cancelling an item offer. Don't forget our client is still
+%% waiting for a reply, so let's tell them the trade state changed
+%% and move back to the negotiate state
+wait({undo_offer, Item}, S=#state{otheritems=OtherItems}) ->
+ gen_fsm:reply(S#state.from, offer_changed),
+ notice(S, "Other side cancelling offer of ~p", [Item]),
+ {next_state, negotiate, S#state{otheritems=remove(Item, OtherItems)}};
+%% The other client falls in ready state and asks us about it.
+%% However, the other client could have moved out of wait state already.
+%% Because of this, we send that we indeed are 'ready!' and hope for them
+%% to do the same.
+wait(are_you_ready, S=#state{}) ->
+ am_ready(S#state.other),
+ notice(S, "asked if ready, and I am. Waiting for same reply", []),
+ {next_state, wait, S};
+%% The other client is not ready to trade yet. We keep waiting
+%% and won't reply to our own client yet.
+wait(not_yet, S = #state{}) ->
+ notice(S, "Other not ready yet", []),
+ {next_state, wait, S};
+%% The other client was waiting for us! Let's reply to ours and
+%% send the ack message for the commit initiation on the other end.
+%% We can't go back after this.
+wait('ready!', S=#state{}) ->
+ am_ready(S#state.other),
+ ack_trans(S#state.other),
+ gen_fsm:reply(S#state.from, ok),
+ notice(S, "other side is ready. Moving to ready state", []),
+ {next_state, ready, S};
+wait(Event, Data) ->
+ unexpected(Event, wait),
+ {next_state, wait, Data}.
+
+%% Ready state with the acknowledgement message coming from the
+%% other side. We determine if we should begin the synchronous
+%% commit or if the other side should.
+%% A successful commit (if we initiated it) could be done
+%% in the terminate function or any other before.
+ready(ack, S=#state{}) ->
+ case priority(self(), S#state.other) of
+ true ->
+ try
+ notice(S, "asking for commit", []),
+ ready_commit = ask_commit(S#state.other),
+ notice(S, "ordering commit", []),
+ ok = do_commit(S#state.other),
+ notice(S, "committing...", []),
+ commit(S),
+ {stop, normal, S}
+ catch Class:Reason ->
+ %% abort! Either ready_commit or do_commit failed
+ notice(S, "commit failed", []),
+ {stop, {Class, Reason}, S}
+ end;
+ false ->
+ {next_state, ready, S}
+ end;
+ready(Event, Data) ->
+ unexpected(Event, ready),
+ {next_state, ready, Data}.
+
+%% We weren't the ones to initiate the commit.
+%% Let's reply to the other side to say we're doing our part
+%% and terminate.
+ready(ask_commit, _From, S) ->
+ notice(S, "replying to ask_commit", []),
+ {reply, ready_commit, ready, S};
+ready(do_commit, _From, S) ->
+ notice(S, "committing...", []),
+ commit(S),
+ {stop, normal, ok, S};
+ready(Event, _From, Data) ->
+ unexpected(Event, ready),
+ {next_state, ready, Data}.
+
+%% This cancel event has been sent by the other player
+%% stop whatever we're doing and shut down!
+handle_event(cancel, _StateName, S=#state{}) ->
+ notice(S, "received cancel event", []),
+ {stop, other_cancelled, S};
+handle_event(Event, StateName, Data) ->
+ unexpected(Event, StateName),
+ {next_state, StateName, Data}.
+
+%% This cancel event comes from the client. We must warn the other
+%% player that we have a quitter!
+handle_sync_event(cancel, _From, _StateName, S = #state{}) ->
+ notify_cancel(S#state.other),
+ notice(S, "cancelling trade, sending cancel event", []),
+ {stop, cancelled, ok, S};
+%% Note: DO NOT reply to unexpected calls. Let the call-maker crash!
+handle_sync_event(Event, _From, StateName, Data) ->
+ unexpected(Event, StateName),
+ {next_state, StateName, Data}.
+
+%% The other player's FSM has gone down. We have
+%% to abort the trade.
+handle_info({'DOWN', Ref, process, Pid, Reason}, _, S=#state{other=Pid, monitor=Ref}) ->
+ notice(S, "Other side dead", []),
+ {stop, {other_down, Reason}, S};
+handle_info(Info, StateName, Data) ->
+ unexpected(Info, StateName),
+ {next_state, StateName, Data}.
+
+code_change(_OldVsn, StateName, Data, _Extra) ->
+ {ok, StateName, Data}.
+
+%% Transaction completed.
+terminate(normal, ready, S=#state{}) ->
+ notice(S, "FSM leaving.", []);
+terminate(_Reason, _StateName, _StateData) ->
+ ok.
+
+%%% PRIVATE FUNCTIONS
+
+%% adds an item to an item list
+add(Item, Items) ->
+ [Item | Items].
+
+%% remove an item from an item list
+remove(Item, Items) ->
+ Items -- [Item].
+
+%% Send players a notice. This could be messages to their clients
+%% but for our purposes, outputting to the shell is enough.
+notice(#state{name=N}, Str, Args) ->
+ io:format("~s: "++Str++"~n", [N|Args]).
+
+%% Unexpected allows to log unexpected messages
+unexpected(Msg, State) ->
+ io:format("~p received unknown event ~p while in state ~p~n",
+ [self(), Msg, State]).
+
+%% This function allows two processes to make a synchronous call to each
+%% other by electing one Pid to do it. Both processes call it and it
+%% tells them whether they should initiate the call or not.
+%% This is done by knowing that Erlang will alwys sort Pids in an
+%% absolute manner depending on when and where they were spawned.
+priority(OwnPid, OtherPid) when OwnPid > OtherPid -> true;
+priority(OwnPid, OtherPid) when OwnPid < OtherPid -> false.
+
+commit(S = #state{}) ->
+ io:format("Transaction completed for ~s. "
+ "Items sent are:~n~p,~n received are:~n~p.~n"
+ "This operation should have some atomic save "
+ "in a database.~n",
+ [S#state.name, S#state.ownitems, S#state.otheritems]).
+
diff --git a/learn-you-some-erlang/tree.erl b/learn-you-some-erlang/tree.erl
new file mode 100644
index 0000000..3cda7c3
--- /dev/null
+++ b/learn-you-some-erlang/tree.erl
@@ -0,0 +1,43 @@
+-module(tree).
+-export([empty/0, insert/3, lookup/2, has_value/2]).
+
+empty() -> {node, 'nil'}.
+
+insert(Key, Val, {node, 'nil'}) ->
+ {node, {Key, Val, {node, 'nil'}, {node, 'nil'}}};
+insert(NewKey, NewVal, {node, {Key, Val, Smaller, Larger}}) when NewKey < Key ->
+ {node, {Key, Val, insert(NewKey, NewVal, Smaller), Larger}};
+insert(NewKey, NewVal, {node, {Key, Val, Smaller, Larger}}) when NewKey > Key ->
+ {node, {Key, Val, Smaller, insert(NewKey, NewVal, Larger)}};
+insert(Key, Val, {node, {Key, _, Smaller, Larger}}) ->
+ {node, {Key, Val, Smaller, Larger}}.
+
+lookup(_, {node, 'nil'}) ->
+ undefined;
+lookup(Key, {node, {Key, Val, _, _}}) ->
+ {ok, Val};
+lookup(Key, {node, {NodeKey, _, Smaller, _}}) when Key < NodeKey ->
+ lookup(Key, Smaller);
+lookup(Key, {node, {_, _, _, Larger}}) ->
+ lookup(Key, Larger).
+
+%%---------------------------------------------------------
+%% The code after this comment is added in the errors and
+%% exceptions chapter. Ignore it if you're still reading
+%% the chapter about recursion.
+%%---------------------------------------------------------
+
+has_value(Val, Tree) ->
+ try has_value1(Val, Tree) of
+ _ -> false
+ catch
+ true -> true
+ end.
+
+has_value1(_, {node, 'nil'}) ->
+ false;
+has_value1(Val, {node, {_, Val, _, _}}) ->
+ throw(true);
+has_value1(Val, {node, {_, _, Left, Right}}) ->
+ has_value1(Val, Left),
+ has_value1(Val, Right).
diff --git a/learn-you-some-erlang/useless.erl b/learn-you-some-erlang/useless.erl
new file mode 100644
index 0000000..d5542a2
--- /dev/null
+++ b/learn-you-some-erlang/useless.erl
@@ -0,0 +1,14 @@
+-module(useless).
+-export([add/2, hello/0, greet_and_add_two/1]).
+
+add(A,B) ->
+ A + B.
+
+%% Shows greetings.
+%% io:format/1 is the standard function used to output text.
+hello() ->
+ io:format("Hello, world!~n").
+
+greet_and_add_two(X) ->
+ hello(),
+ add(X,2).
diff --git a/learn-you-some-erlang/what_the_if.erl b/learn-you-some-erlang/what_the_if.erl
new file mode 100644
index 0000000..723c818
--- /dev/null
+++ b/learn-you-some-erlang/what_the_if.erl
@@ -0,0 +1,30 @@
+-module(what_the_if).
+-export([heh_fine/0, oh_god/1, help_me/1]).
+
+%% should check if this actually works (hint: an error will be thrown)
+heh_fine() ->
+ if 1 =:= 1 ->
+ works
+ end,
+ if 1 =:= 2; 1 =:= 1 ->
+ works
+ end,
+ if 1 =:= 2, 1 =:= 1 ->
+ fails
+ end.
+
+oh_god(N) ->
+ if N =:= 2 -> might_succeed;
+ true -> always_does %% this is Erlang's if's 'else!'
+ end.
+
+%% note, this one would be better as a pattern match in function heads!
+%% I'm doing it this way for the sake of the example.
+help_me(Animal) ->
+ Talk = if Animal == cat -> "meow";
+ Animal == beef -> "mooo";
+ Animal == dog -> "bark";
+ Animal == tree -> "bark";
+ true -> "fgdadfgna"
+ end,
+ {Animal, "says " ++ Talk ++ "!"}.
diff --git a/learn-you-some-erlang/zoo.erl b/learn-you-some-erlang/zoo.erl
new file mode 100644
index 0000000..1e3c2eb
--- /dev/null
+++ b/learn-you-some-erlang/zoo.erl
@@ -0,0 +1,45 @@
+-module(zoo).
+-export([main/0]).
+
+-type red_panda() :: bamboo | birds | eggs | berries.
+-type squid() :: sperm_whale.
+-type food(A) :: fun(() -> A).
+
+-spec feeder(red_panda) -> food(red_panda());
+ (squid) -> food(squid()).
+feeder(red_panda) ->
+ fun() ->
+ element(random:uniform(4), {bamboo, birds, eggs, berries})
+ end;
+feeder(squid) ->
+ fun() -> sperm_whale end.
+
+-spec feed_red_panda(food(red_panda())) -> red_panda().
+feed_red_panda(Generator) ->
+ Food = Generator(),
+ io:format("feeding ~p to the red panda~n", [Food]),
+ Food.
+
+-spec feed_squid(food(squid())) -> squid().
+feed_squid(Generator) ->
+ Food = Generator(),
+ io:format("feeding ~p to the squid~n", [Food]),
+ Food.
+
+main() ->
+ %% Random seeding
+ <<A:32, B:32, C:32>> = crypto:rand_bytes(12),
+ random:seed(A, B, C),
+ %% The zoo buys a feeder for both the red panda and squid
+ FeederRP = feeder(red_panda),
+ FeederSquid = feeder(squid),
+ %% Time to feed them! If we do it correctly at least once,
+ %% Then no warning ever happens. Comment the two lines
+ %% below to enable dialyzer figuring stuff out! This is likely
+ %% failing because Dialyzer sees both calls as valid and thus
+ %% needs not to reevaluate them again.
+ %feed_squid(FeederSquid),
+ %feed_red_panda(FeederRP),
+ %% This should not be right!
+ feed_squid(FeederRP),
+ feed_red_panda(FeederSquid).