= Binbo
:toc: macro
:toclevels: 4
image:https://img.shields.io/hexpm/v/binbo.svg?color=yellow["Binbo on Hex.pm", link="https://hex.pm/packages/binbo"]
image:https://travis-ci.org/DOBRO/binbo.svg?branch=master["Build Status", link="https://travis-ci.org/DOBRO/binbo"]
image:https://img.shields.io/badge/erlang-%3E%3D%2020.0-0d6e8c.svg["Erlang", link="https://www.erlang.org/"]
image:https://img.shields.io/badge/license-Apache%202.0-blue.svg["License", link="LICENSE"]
Binbo is a Chess representation written in pure Erlang using https://www.chessprogramming.org/Bitboards[Bitboards]. It is basically aimed to be used on game servers where people play chess online.
It's called `Binbo` because its ground is a **bin**ary **bo**ard containing only _zeros_ and _ones_ (`0` and `1`) since this is the main meaning of Bitboards as an internal chessboard representation.
Binbo also uses the https://www.chessprogramming.org/Magic_Bitboards[Magic Bitboards] approach for a **blazing fast** move generation of sliding pieces (rook, bishop, and queen).
**Note:** it's not a chess engine but it could be a good starting point for it. It can play the role of a core (regarding move generation and validation) for multiple chess engines running on distributed Erlang nodes, since Binbo is an OTP application itself.
image::https://user-images.githubusercontent.com/296845/61208986-40792d80-a701-11e9-93c8-d2c41c5ef00d.png[Binbo sample]
'''
toc::[]
'''
== Features
* Blazing fast move generation and validation.
* No bottlenecks. Every game is an Erlang process (`gen_server`) with its own game state.
* Ability to create as many concurrent games as many Erlang processes allowed in VM.
* Support for PGN loading.
* All the chess rules are completely covered including:
** https://en.wikipedia.org/wiki/En_passant[En-passant move];
** https://en.wikipedia.org/wiki/Castling[Castling];
** https://en.wikipedia.org/wiki/Fifty-move_rule[Fifty-move rule];
** https://en.wikipedia.org/wiki/Threefold_repetition[Threefold repetition];
** Draw by insufficient material:
*** King versus King,
*** King and Bishop versus King,
*** King and Knight versus King,
*** King and Bishop versus King and Bishop with the bishops on the same color;
* Unicode chess symbols support for the board visualization right in Erlang shell: +
♙{nbsp}♘{nbsp}♗{nbsp}♖{nbsp}♕{nbsp}♔{nbsp}{nbsp}{nbsp}{nbsp}♟{nbsp}♞{nbsp}♝{nbsp}♜{nbsp}♛{nbsp}♚
* Passes all https://www.chessprogramming.org/Perft_Results[perft] tests.
* Ready for use on game servers.
== Requirements
** https://www.erlang.org/[Erlang/OTP] 20.0 or higher.
** https://www.rebar3.org/[rebar3]
== Quick start
Run `make shell` or `rebar3 shell` and then:
[source,erlang]
----
%% Start Binbo application first:
binbo:start().
%% Start new process for the game:
{ok, Pid} = binbo:new_server().
%% Start new game in the process:
binbo:new_game(Pid).
%% Or start new game with a given FEN:
binbo:new_game(Pid, <<"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1">>).
%% Look at the board with ascii or unicode pieces:
binbo:print_board(Pid).
binbo:print_board(Pid, [unicode]).
%% Make move for White and Black:
binbo:move(Pid, <<"e2e4">>).
binbo:move(Pid, <<"e7e5">>).
%% Have a look at the board again:
binbo:print_board(Pid).
binbo:print_board(Pid, [unicode]).
----
== Interface
There are three steps to be done before making game moves:
. Start Binbo application.
. Create process for the game.
. Initialize game state in the process.
**Note:** process creation and game initialization are separated for the following reason: since Binbo is aimed to handle a number of concurrent games, the game process should be started as quick as possible leaving the http://erlang.org/doc/design_principles/sup_princ.html[supervisor] doing the same job for another game. It's important for high-load systems where game creation is a very frequent event.
=== Starting application
To start Binbo, call:
[source,erlang]
----
binbo:start().
----
=== Creating game process
[source,erlang]
----
binbo:new_server() -> {ok, pid()}.
----
So, to start one or more game processes:
[source,erlang]
----
{ok, Pid1} = binbo:new_server(),
{ok, Pid2} = binbo:new_server(),
{ok, Pid3} = binbo:new_server().
----
[[initializing-new-game]]
=== Initializing new game
[source,erlang]
----
binbo:new_game(Pid) -> {ok, GameStatus} | {error, Reason}.
binbo:new_game(Pid, Fen) -> {ok, GameStatus} | {error, Reason}.
----
.where:
* `Pid` is the `pid` of the process where the game is to be initialized;
* `Fen` (`string()` or `binary()`) is the https://en.wikipedia.org/wiki/Forsyth%E2%80%93Edwards_Notation[Forsyth–Edwards Notation] (FEN);
* `GameStatus` is the link:#game-status[game status].
It is possible to reinitilize game in the same process. For example:
[source,erlang]
----
binbo:new_game(Pid),
binbo:new_game(Pid, Fen2),
binbo:new_game(Pid, Fen3).
----
.Example:
[source,erlang]
----
%% In Erlang shell.
> {ok, Pid} = binbo:new_server().
{ok,<0.185.0>}
% New game from the starting position:
> binbo:new_game(Pid).
{ok,continue}
% New game with the given FEN:
> binbo:new_game(Pid, <<"rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1">>).
{ok,continue}
----
=== Making moves
==== API
[source,erlang]
----
binbo:move(Pid, Move) -> {ok, GameStatus} | {error, Reason}.
binbo:san_move(Pid, Move) -> {ok, GameStatus} | {error, Reason}.
----
where:
* `Pid` is the pid of the game process;
* `Move` is of `binary()` or `string()` type;
* `GameStatus` is the link:#game-status[game status].
Function `binbo:move/2` supports only _strict square notation_ with respect to argument `Move`, for example: `<<"e2e4">>`, `<<"e7e5">>`, etc.
Function `binbo:san_move/2` is intended to handle various formats of argument `Move` including https://en.wikipedia.org/wiki/Algebraic_notation_(chess)[_standard algebraic notation_] (*SAN*), for example: `<<"e4">>`, `<<"Nf3">>`, `<<"Qxd5">>`, `<<"a8=Q">>`, `<<"Rdf8">>`, `<<"R1a3">>`, `<<"O-O">>`, `<<"O-O-O">>`, `<<"e1e8">>`, etc.
.Examples for `binbo:move/2`:
[source,erlang]
----
%% In Erlang shell.
% New game from the starting position:
> {ok, Pid} = binbo:new_server().
{ok,<0.190.0>}
> binbo:new_game(Pid).
{ok,continue}
% Start making moves
> binbo:move(Pid, <<"e2e4">>). % e4
{ok,continue}
> binbo:move(Pid, <<"e7e5">>). % e5
{ok,continue}
> binbo:move(Pid, <<"f1c4">>). % Bc4
{ok,continue}
> binbo:move(Pid, <<"d7d6">>). % d6
{ok,continue}
> binbo:move(Pid, <<"d1f3">>). % Qf3
{ok,continue}
> binbo:move(Pid, <<"b8c6">>). % Nc6
{ok,continue}
% And here is checkmate!
> binbo:move(Pid, <<"f3f7">>). % Qf7#
{ok,checkmate}
----
.Examples for `binbo:san_move/2`:
[source,erlang]
----
%% In Erlang shell.
% New game from the starting position:
> {ok, Pid} = binbo:new_server().
{ok,<0.190.0>}
> binbo:new_game(Pid).
{ok,continue}
% Start making moves
> binbo:san_move(Pid, <<"e4">>).
{ok,continue}
> binbo:san_move(Pid, <<"e5">>).
{ok,continue}
> binbo:san_move(Pid, <<"Bc4">>).
{ok,continue}
> binbo:san_move(Pid, <<"d6">>).
{ok,continue}
> binbo:san_move(Pid, <<"Qf3">>).
{ok,continue}
> binbo:san_move(Pid, <<"Nc6">>).
{ok,continue}
% Checkmate!
> binbo:san_move(Pid, <<"Qf7#">>).
{ok,checkmate}
----
==== Castling
Binbo recognizes https://en.wikipedia.org/wiki/Castling[castling] when:
* White king moves from `E1` to `G1` (`O-O`);
* White king moves from `E1` to `C1` (`O-O-O`);
* Black king moves from `E8` to `G8` (`O-O`);
* Black king moves from `E8` to `C8` (`O-O-O`).
Binbo also checks whether castling allowed or not acording to the chess rules.
.Castling examples:
[source,erlang]
----
% White castling kingside
binbo:move(Pid, <<"e1g1">>).
binbo:san_move(Pid, <<"O-O">>).
% White castling queenside
binbo:move(Pid, <<"e1c1">>).
binbo:san_move(Pid, <<"O-O-O">>).
% Black castling kingside
binbo:move(Pid, <<"e8g8">>).
binbo:san_move(Pid, <<"O-O">>).
% Black castling queenside
binbo:move(Pid, <<"e8c8">>).
binbo:san_move(Pid, <<"O-O-O">>).
----
==== Promotion
Binbo recognizes https://en.wikipedia.org/wiki/Promotion_(chess)[promotion] when:
* White pawn moves from square of `rank 7` to square of `rank 8`;
* Black pawn moves from square of `rank 2` to square of `rank 1`.
.Promotion examples:
[source,erlang]
----
% White pawn promoted to Queen:
binbo:move(Pid, <<"a7a8q">>).
binbo:san_move(Pid, <<"a8=Q">>).
% or just:
binbo:move(Pid, <<"a7a8">>).
binbo:san_move(Pid, <<"a8">>).
% White pawn promoted to Knight:
binbo:move(Pid, <<"a7a8n">>).
binbo:san_move(Pid, <<"a8=N">>).
% Black pawn promoted to Queen:
binbo:move(Pid, <<"a2a1q">>).
binbo:san_move(Pid, <<"a1=Q">>).
% or just:
binbo:move(Pid, <<"a2a1">>).
binbo:san_move(Pid, <<"a1">>).
% Black pawn promoted to Knight:
binbo:move(Pid, <<"a2a1n">>).
binbo:san_move(Pid, <<"a1=N">>).
----
==== En passant
Binbo also recognizes the https://en.wikipedia.org/wiki/En_passant[en passant capture] in strict accordance with the chess rules.
=== Getting FEN
[source,erlang]
----
binbo:get_fen(Pid) -> {ok, Fen}.
----
.Example:
[source,erlang]
----
> binbo:get_fen(Pid).
{ok, <<"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1">>}.
----
=== PGN loading
[source,erlang]
----
binbo:load_pgn(Pid, PGN) -> {ok, GameStatus} | {error, Reason}.
binbo:load_pgn_file(Pid, Filename) -> {ok, GameStatus} | {error, Reason}.
----
.where:
* `Pid` is the pid of the game process;
* `PGN` is a https://en.wikipedia.org/wiki/Portable_Game_Notation[Portable Game Notation], its type is `binary()`;
* `Filename` is a path to the file from which PGN is to be loaded. Its type is `binary()` or `string()`.
* `GameStatus` is the link:#game-status[game status].
Function `binbo:load_pgn/2` loads PGN itself.
If `PGN` is pretty large and you are able to load it from *local* file, to avoid sending large data between processes, use `binbo:load_pgn_file/2` since it's highly optimized for reading local files.
To extract move list, Binbo takes into account various cases specific to PGN such as _comments in braces_,
https://chess.stackexchange.com/questions/18214/valid-pgn-variations[_recursive annotation variations_] (RAVs) and
https://en.wikipedia.org/wiki/Numeric_Annotation_Glyphs[_numeric annotation glyphs_] (NAGs).
.Examples:
[source,erlang]
----
%% Binary PGN:
load_pgn() ->
PGN = <<"1. e4 e5 2. Nf3 Nc6 3. Bb5 a6">>,
{ok, Pid} = binbo:new_server(),
binbo:load_pgn(Pid, PGN).
%% From file:
load_pgn_from_file() ->
Filename = "/path/to/game.pgn",
{ok, Pid} = binbo:new_server(),
binbo:load_pgn_file(Pid, Filename).
----
=== Board visualization
[source,erlang]
----
binbo:print_board(Pid) -> ok.
binbo:print_board(Pid, [unicode|ascii|flip]) -> ok.
----
You may want to see the current position right in Elang shell. To do it, call:
[source,erlang]
----
% With ascii pieces:
binbo:print_board(Pid).
% With unicode pieces:
binbo:print_board(Pid, [unicode]).
% Flipped board:
binbo:print_board(Pid, [flip]).
binbo:print_board(Pid, [unicode, flip]).
----
[[game-status]]
=== Game status
[source,erlang]
----
binbo:game_status(Pid) -> {ok, GameStatus} | {error, Reason}.
----
.where:
* `Pid` is the the pid of the game process;
* `GameStatus` is the game status itself;
* `Reason` is the reason why the game status cannot be obtained (usually due to the fact that the game is not initialized via link:#initializing-new-game[binbo:new_game/1,2]).
.The value of `GameStatus`:
* `continue` - game in progress;
* `checkmate` - one of the sides (White or Black) checkmated;
* `{draw, stalemate}` - draw because of stalemate;
* `{draw, rule50}` - draw according to the fifty-move rule;
* `{draw, insufficient_material}` - draw because of insufficient material;
* `{draw, threefold_repetition}` - draw according to the threefold repetition rule;
* `{draw, {manual, WhyDraw}}` - draw was set link:#setting-a-draw[manually] for the reason of `WhyDraw`.
=== List of legal moves
[source,erlang]
----
binbo:all_legal_moves(Pid) -> {ok, Movelist} | {error, Reason}.
binbo:all_legal_moves(Pid, Movetype) -> {ok, Movelist} | {ok, Number} | {error, Reason}.
----
.where:
* `Pid` is the pid of the game process;
* `Movelist` is a list of all legal moves for the current position. Each element of `Movelist` is a tuple `{From, To}` or `{From, To, Promo}`, where:
** `From` and `To` are starting and target square respectively.
** `Promo` is one of the _atoms_: `q`, `r`, `b`, `n` (i.e. _queen_, _rook_, _bishop_, and _knight_ respectively). Three-element tuple `{From, To, Promo}` occurs in case of *pawn promotion*.
* `Movetype` can take on of the values: `int`, `bin`, `str`, or `count`.
The call `binbo:all_legal_moves(Pid)` is the same as `binbo:all_legal_moves(Pid, int)`.
If `Movetype` is `count`, the function returns tuple `{ok, Number}` where `Number` is the number of legal moves.
The values of `From` and `To` depend on `Movetype` as follows:
* `int`: the values of `From` and `To` are _integers_ in range `0..63`, namely, square indices. For example, the move from `A1` to `H8` corresponds to `{0, 63}`. Use `int` to get the *fastest* reply from the game process.
* `bin`: the values of `From` and `To` are _binaries_. For example: `{<<"e2">>, <<"e4">>}`.
* `str`: the values of `From` and `To` are _strings_. For example: `{"e2", "e4"}`.
.Example:
[source,erlang]
----
> {ok, Pid} = binbo:new_server().
{ok,<0.212.0>}
%% Start new game from FEN that corresponds to Position 5
%% from Perft Results: https://www.chessprogramming.org/Perft_Results
> binbo:new_game(Pid, <<"rnbq1k1r/pp1Pbppp/2p5/8/2B5/8/PPP1NnPP/RNBQK2R w KQ - 1 8">>).
{ok,continue}
%% Count legal moves
> binbo:all_legal_moves(Pid, count).
{ok,44}
> {ok, Movelist} = binbo:all_legal_moves(Pid).
{ok,[{51,58,q},
{51,58,r},
{51,58,b},
{51,58,n},
{26,53},
{26,44},
{26,40},
{26,35},
{26,33},
{26,19},
{26,17},
{15,31},
{15,23},
{14,30},
{14,22},
{12,29},
{12,27},
{12,22},
{12,18},
{12,6},
{10,18},
{9,25},
{9,17},
{8,24},
{8,16},
{7,...},
{...}|...]}
%% Count moves:
> erlang:length(Movelist).
44
> binbo:all_legal_moves(Pid, bin).
{ok,[{<<"d7">>,<<"c8">>,q},
{<<"d7">>,<<"c8">>,r},
{<<"d7">>,<<"c8">>,b},
{<<"d7">>,<<"c8">>,n},
{<<"c4">>,<<"f7">>},
{<<"c4">>,<<"e6">>},
{<<"c4">>,<<"a6">>},
{<<"c4">>,<<"d5">>},
{<<"c4">>,<<"b5">>},
{<<"c4">>,<<"d3">>},
{<<"c4">>,<<"b3">>},
{<<"h2">>,<<"h4">>},
{<<"h2">>,<<"h3">>},
{<<"g2">>,<<"g4">>},
{<<"g2">>,<<"g3">>},
{<<"e2">>,<<"f4">>},
{<<"e2">>,<<"d4">>},
{<<"e2">>,<<"g3">>},
{<<"e2">>,<<"c3">>},
{<<"e2">>,<<"g1">>},
{<<"c2">>,<<"c3">>},
{<<"b2">>,<<"b4">>},
{<<"b2">>,<<"b3">>},
{<<"a2">>,<<"a4">>},
{<<"a2">>,<<...>>},
{<<...>>,...},
{...}|...]}
> binbo:all_legal_moves(Pid, str).
{ok,[{"d7","c8",q},
{"d7","c8",r},
{"d7","c8",b},
{"d7","c8",n},
{"c4","f7"},
{"c4","e6"},
{"c4","a6"},
{"c4","d5"},
{"c4","b5"},
{"c4","d3"},
{"c4","b3"},
{"h2","h4"},
{"h2","h3"},
{"g2","g4"},
{"g2","g3"},
{"e2","f4"},
{"e2","d4"},
{"e2","g3"},
{"e2","c3"},
{"e2","g1"},
{"c2","c3"},
{"b2","b4"},
{"b2","b3"},
{"a2","a4"},
{"a2",[...]},
{[...],...},
{...}|...]}
----
[[setting-a-draw]]
=== Setting a draw
It is possible to set a draw via API:
[source,erlang]
----
binbo:game_draw(Pid) -> ok | {error, Reason}.
binbo:game_draw(Pid, WhyDraw) -> ok | {error, Reason}.
----
.where:
* `Pid` is the pid of the game process;
* `WhyDraw` is the reason why a draw is to be set.
Calling `binbo:game_draw(Pid)` is the same as: `binbo:game_draw(Pid, undefined)`.
.Example:
[source,erlang]
----
% Players agreed to a draw:
> binbo:game_draw(Pid, by_agreement).
ok
% Trying to set a draw for the other reason:
> binbo:game_draw(Pid, other_reason).
{error,{already_has_status,{draw,{manual,by_agreement}}}}
----
=== Stopping game process
If, for some reason, you want to stop the game process and free resources, use:
[source,erlang]
----
binbo:stop_server(Pid) -> ok | {error, {not_pid, Pid}}.
----
Function terminates the game process with pid `Pid`.
=== Stopping application
To stop Binbo, call:
[source,erlang]
----
binbo:stop().
----
== Building and testing
Two possible ways are presented here for building and testing the application (with `make` and `rebar3`).
=== Building
[source,bash]
----
$ make
----
[source,bash]
----
$ rebar3 compile
----
=== Dialyzer
[source,bash]
----
$ make dialyze
----
[source,bash]
----
$ rebar3 dialyzer
----
=== Testing
[source,bash]
----
$ make test
----
[source,bash]
----
$ rebar3 ct --verbose
----
=== Code coverage
[source,bash]
----
$ make cover
----
[source,bash]
----
$ rebar3 cover
----
=== Generating Edoc files
[source,bash]
----
$ make docs
----
[source,bash]
----
$ rebar3 edoc
----
== Binbo and Magic Bitboards
As mentioned above, Binbo uses https://www.chessprogramming.org/Magic_Bitboards[Magic Bitboards], the fastest solution for move generation of sliding pieces
(rook, bishop, and queen). Good explanations of this aproach can also be found https://stackoverflow.com/questions/16925204/sliding-move-generation-using-magic-bitboard/30862064#30862064[here]
and http://vicki-chess.blogspot.com/2013/04/magics.html[here].
The main problem is to find the _index_ which is then used to lookup legal moves
of sliding pieces in a preinitialized move database.
The formula for the _index_ is:
._in C/C++:_
[source,c]
----
magic_index = ((occupied & mask) * magic_number) >> shift;
----
._in Erlang:_
[source,erlang]
----
MagicIndex = (((Occupied band Mask) * MagicNumber) bsr Shift).
----
._where:_
* `Occupied` is the bitboard of all pieces.
* `Mask` is the attack mask of a piece for a given square.
* `MagicNumber` is the magic number, see "https://www.chessprogramming.org/Looking_for_Magics[Looking for Magics]".
* `Shift = (64 - Bits)`, where `Bits` is the number of bits corresponding to attack mask of a given square.
All values for _magic numbers_ and _shifts_ are precalculated before and stored in `binbo_magic.hrl`.
To be accurate, Binbo uses https://www.chessprogramming.org/Magic_Bitboards#Fancy[Fancy Magic Bitboards].
It means that all moves are stored in a table of its own (individual) size for each square.
In _C/C++_ such tables are actually two-dimensional arrays and any move can be accessed by
a simple lookup:
[source,c]
----
move = global_move_table[square][magic_index]
----
._If detailed:_
[source,c]
----
moves_from = global_move_table[square];
move = moves_from[magic_index];
----
The size of `moves_from` table depends on piece and square where it is placed on. For example:
* for rook on `A1` the size of `moves_from` is `4096` (2^12 = 4096, 12 bits requred for the attack mask);
* for bishop on `A1` it is `64` (2^6 = 64, 6 bits requred for the attack mask).
There are no two-dimensional arrays in Erlang, and no global variables which could help us
to get the fast access to the move tables **from everywhere**.
So, how does Binbo beat this? Well, it's simple :).
Erlang gives us the power of _tuples_ and _maps_ with their blazing fast lookup of _elements/values_ by their _index/key_.
Since the number of squares on the chessboard is the constant value (it's always **64**, right?),
our `global_move_table` can be constructed as a _tuple_ of 64 elements, and each element of this _tuple_
is a _map_ containing the _key-value_ association as `MagicIndex => Moves`.
._If detailed, for moves:_
[source,erlang]
----
GlobalMovesTable = { MoveMap1, ..., MoveMap64 }
----
._where:_
[source,erlang]
----
MoveMap1 = #{
MagicIndex_1_1 => Moves_1_1,
...
MagicIndex_1_K => Moves_1_K
},
MoveMap64 = #{
MagicIndex_64_1 => Moves_64_1, ...
...
MagicIndex_64_N => Moves_64_N
},
----
and then we lookup legal moves from a square, say, `E4` (29th element of the _tuple_):
[source,erlang]
----
E4 = 29,
MoveMapE4 = erlang:element(E4, GlobalMovesTable),
MovesFromE4 = maps:get(MagicIndex, MovesMapE4).
----
To calculate _magic index_ we also need the _attack mask_ for a given square.
Every _attack mask_ generated is stored in a _tuple_ of 64 elements:
[source,erlang]
----
GlobalMaskTable = {Mask1, Mask2, ..., Mask64}
----
where `Mask1`, `Mask2`, ..., `Mask64` are _bitboards_ (integers).
Finally, if we need to get all moves from `E4`:
[source,erlang]
----
E4 = 29,
Mask = erlang:element(E4, GlobalMaskTable),
MagicIndex = ((Occupied band Mask) * MagicNumber) bsr Shift,
MoveMapE4 = erlang:element(E4, GlobalMovesTable),
MovesFromE4 = maps:get(MagicIndex, MovesMapE4).
----
Next, no global variables? We make them global!
How do we get the fastest access to the _move tables_ and to the _atack masks_ **from everywhere**?
http://erlang.org/doc/man/ets.html[ETS]? No! Using ETS as a storage for _static terms_ we get the overhead due to extra data copying during lookup.
And now we are coming to the fastest solution.
When Binbo starts up, all _move tables_ are initialized.
Once these tables (_tuples_, actually) initialized, they are "injected" into **dynamically generated
modules compiled at Binbo start**. Then, to get the values, we just call a _getter function_
(`binbo_global:get/1`) with the argument as the name of the corresponding dynamic module.
This awesome trick is used in MochiWeb library, see module https://github.com/mochi/mochiweb/blob/master/src/mochiglobal.erl[mochiglobal].
Using http://erlang.org/doc/man/persistent_term.html[persistent_term] (since OTP 21.2) for storing static data is also a good idea.
But it doesn't seem to be a better way for the following reason with respect to dynamic modules.
When Binbo stops, it gets them **unloaded** as they are not necessary anymore.
It should do the similar things for `persistent_term` data, say, delete all _unused
terms_ to free memory.
In this case we run into the issue regarding scanning the _heaps_ in all processes.
So, using `global` dynamic modules with large static data seems to be more reasonable in spite of that fact that it significantly slows down the application startup due to the run-time compilation of these modules.
== Changelog
See link:CHANGELOG.md[CHANGELOG] for details.
== Contributing
Want to contribute? Really? Awesome!
Please refer to the link:CONTRIBUTING.md[CONTRIBUTING] file for details.
== License
This project is licensed under the terms of the Apache License, Version 2.0.
See the link:LICENSE[LICENSE] file for details.