Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Erlang/OTP: What's in the box? @ ElixirConf LA ...

Erlang/OTP: What's in the box? @ ElixirConf LA 2019

When we are first introduced to Elixir we notice that although it does an excellent job in simplifying and abstracting the thorny parts of the platform, every once in a while we encounter some straight calls to Erlang code. Why do we need them? How do they work? Fear not, for these are in fact the tips of amazing, old, beautiful icebergs.

This talk opens the box of Erlang/OTP and introduces some of the coolest built-in tools and libraries available in a standard Erlang/OTP installation, such as wx, ssh, timer, crypto and digraph. We will go over the most popular ones, without forgetting to stop by the most underground (and often underrated) ones, showing some examples of how they can come in handy in numerous occasions.

João Britto

October 25, 2019
Tweet

More Decks by João Britto

Other Decks in Programming

Transcript

  1. # lib/plug/session/ets.ex def init(opts) do Keyword.fetch!(opts, :table) end def put(_conn,

    sid, data, table) do :ets.insert(table, {sid, data, now()}) sid end defp now() do :os.timestamp() end
  2. # lib/plug/session/ets.ex def init(opts) do Keyword.fetch!(opts, :table) end def put(_conn,

    sid, data, table) do :ets.insert(table, {sid, data, now()}) sid end defp now() do :os.timestamp() end
  3. # lib/plug/session/ets.ex def init(opts) do Keyword.fetch!(opts, :table) end def put(_conn,

    sid, data, table) do :ets.insert(table, {sid, data, now()}) sid end defp now() do :os.timestamp() end
  4. # lib/plug/session/ets.ex def init(opts) do Keyword.fetch!(opts, :table) end def put(_conn,

    sid, data, table) do :ets.insert(table, {sid, data, now()}) sid end defp now() do :os.timestamp() end
  5. # lib/plug/session/ets.ex def init(opts) do Keyword.fetch!(opts, :table) end def put(_conn,

    sid, data, table) do :ets.insert(table, {sid, data, now()}) sid end defp now() do :os.timestamp() end
  6. # lib/plug/session/ets.ex def init(opts) do Keyword.fetch!(opts, :table) end def put(_conn,

    sid, data, table) do :ets.insert(table, {sid, data, now()}) sid end defp now() do :os.timestamp() end
  7. # lib/plug/session/ets.ex def init(opts) do Keyword.fetch!(opts, :table) end def put(_conn,

    sid, data, table) do :ets.insert(table, {sid, data, now()}) sid end defp now() do :os.timestamp() end
  8. iex> i :an_atom Term :an_atom Data type Atom Reference modules

    Atom Implemented protocols IEx.Info, Inspect, String.Chars, List.Chars
  9. iex> i :an_atom Term :an_atom Data type Atom Reference modules

    Atom Implemented protocols IEx.Info, Inspect, String.Chars, List.Chars
  10. iex> i :an_atom Term :an_atom Data type Atom Reference modules

    Atom Implemented protocols IEx.Info, Inspect, String.Chars, List.Chars
  11. iex> i AnAtom Term AnAtom Data type Atom Raw representation

    :"Elixir.AnAtom" Reference modules Atom Implemented protocols IEx.Info, Inspect, String.Chars, List.Chars
  12. iex> i AnAtom Term AnAtom Data type Atom Raw representation

    :"Elixir.AnAtom" Reference modules Atom Implemented protocols IEx.Info, Inspect, String.Chars, List.Chars
  13. iex> i AnAtom Term AnAtom Data type Atom Raw representation

    :"Elixir.AnAtom" Reference modules Atom Implemented protocols IEx.Info, Inspect, String.Chars, List.Chars
  14. iex> i AnAtom Term AnAtom Data type Atom Raw representation

    :"Elixir.AnAtom" Reference modules Atom Implemented protocols IEx.Info, Inspect, String.Chars, List.Chars
  15. iex> i Keyword Term Keyword Data type Atom Module bytecode

    elixir/ebin/Elixir.Keyword.beam Source elixir/lib/keyword.ex Version [18490583473799941736109510131724656698] Compile options [:debug_info] Description Use h(Keyword) to access its documentation. Call Keyword.module_info() to access metadata. Raw representation :"Elixir.Keyword" Reference modules Module, Atom Implemented protocols IEx.Info, Inspect, String.Chars, List.Chars
  16. iex> i Keyword Term Keyword Data type Atom Module bytecode

    elixir/ebin/Elixir.Keyword.beam Source elixir/lib/keyword.ex Version [18490583473799941736109510131724656698] Compile options [:debug_info] Description Use h(Keyword) to access its documentation. Call Keyword.module_info() to access metadata. Raw representation :"Elixir.Keyword" Reference modules Module, Atom Implemented protocols IEx.Info, Inspect, String.Chars, List.Chars
  17. iex> i Keyword Term Keyword Data type Atom Module bytecode

    elixir/ebin/Elixir.Keyword.beam Source elixir/lib/keyword.ex Version [18490583473799941736109510131724656698] Compile options [:debug_info] Description Use h(Keyword) to access its documentation. Call Keyword.module_info() to access metadata. Raw representation :"Elixir.Keyword" Reference modules Module, Atom Implemented protocols IEx.Info, Inspect, String.Chars, List.Chars
  18. iex> i Keyword Term Keyword Data type Atom Module bytecode

    elixir/ebin/Elixir.Keyword.beam Source elixir/lib/keyword.ex Version [18490583473799941736109510131724656698] Compile options [:debug_info] Description Use h(Keyword) to access its documentation. Call Keyword.module_info() to access metadata. Raw representation :"Elixir.Keyword" Reference modules Module, Atom Implemented protocols IEx.Info, Inspect, String.Chars, List.Chars
  19. iex> i Keyword Term Keyword Data type Atom Module bytecode

    elixir/ebin/Elixir.Keyword.beam Source elixir/lib/keyword.ex Version [18490583473799941736109510131724656698] Compile options [:debug_info] Description Use h(Keyword) to access its documentation. Call Keyword.module_info() to access metadata. Raw representation :"Elixir.Keyword" Reference modules Module, Atom Implemented protocols IEx.Info, Inspect, String.Chars, List.Chars
  20. iex> i :os Term :os Data type Atom Module bytecode

    erlang/21.0.4/lib/kernel-6.0/ebin/os.beam Source otp_src_21.0.4/lib/kernel/src/os.erl Version [8891021078382918036082292112481311050] Compile options [:debug_info, {:i, 'otp_src_21.0.4/lib/kernel/src/../include'}] Description Call :os.module_info() to access metadata. Raw representation :"os" Reference modules Module, Atom Implemented protocols IEx.Info, Inspect, String.Chars, List.Chars
  21. iex> i :os Term :os Data type Atom Module bytecode

    erlang/21.0.4/lib/kernel-6.0/ebin/os.beam Source otp_src_21.0.4/lib/kernel/src/os.erl Version [8891021078382918036082292112481311050] Compile options [:debug_info, {:i, 'otp_src_21.0.4/lib/kernel/src/../include'}] Description Call :os.module_info() to access metadata. Raw representation :"os" Reference modules Module, Atom Implemented protocols IEx.Info, Inspect, String.Chars, List.Chars
  22. iex> i :os Term :os Data type Atom Module bytecode

    erlang/21.0.4/lib/kernel-6.0/ebin/os.beam Source otp_src_21.0.4/lib/kernel/src/os.erl Version [8891021078382918036082292112481311050] Compile options [:debug_info, {:i, 'otp_src_21.0.4/lib/kernel/src/../include'}] Description Call :os.module_info() to access metadata. Raw representation :"os" Reference modules Module, Atom Implemented protocols IEx.Info, Inspect, String.Chars, List.Chars
  23. iex> i :os Term :os Data type Atom Module bytecode

    erlang/21.0.4/lib/kernel-6.0/ebin/os.beam Source otp_src_21.0.4/lib/kernel/src/os.erl Version [8891021078382918036082292112481311050] Compile options [:debug_info, {:i, 'otp_src_21.0.4/lib/kernel/src/../include'}] Description Call :os.module_info() to access metadata. Raw representation :"os" Reference modules Module, Atom Implemented protocols IEx.Info, Inspect, String.Chars, List.Chars
  24. iex> i :os Term :os Data type Atom Module bytecode

    erlang/21.0.4/lib/kernel-6.0/ebin/os.beam Source otp_src_21.0.4/lib/kernel/src/os.erl Version [8891021078382918036082292112481311050] Compile options [:debug_info, {:i, 'otp_src_21.0.4/lib/kernel/src/../include'}] Description Call :os.module_info() to access metadata. Raw representation :"os" Reference modules Module, Atom Implemented protocols IEx.Info, Inspect, String.Chars, List.Chars
  25. iex> :c c calendar cerl cerl_clauses cerl_inline cerl_sets cerl_trees code

    code_server compile core_lib core_lint core_parse core_pp core_scan iex> :d dets dets_server dets_sup dets_utils dets_v9 dict digraph digraph_utils disk_log disk_log_1 disk_log_server disk_log_sup dist_ac dist_util iex> :g gb_sets gb_trees gen gen_event gen_fsm gen_sctp gen_server gen_statem gen_tcp gen_udp global global_group global_search group group_history iex> :l lists local_tcp local_udp log_mf_h logger logger_backend logger_config logger_disk_log_h logger_filters logger_formatter logger_h_common logger_server logger_simple_h logger_std_h logger_sup
  26. iex> :c c calendar cerl cerl_clauses cerl_inline cerl_sets cerl_trees code

    code_server compile core_lib core_lint core_parse core_pp core_scan iex> :d dets dets_server dets_sup dets_utils dets_v9 dict digraph digraph_utils disk_log disk_log_1 disk_log_server disk_log_sup dist_ac dist_util iex> :g gb_sets gb_trees gen gen_event gen_fsm gen_sctp gen_server gen_statem gen_tcp gen_udp global global_group global_search group group_history iex> :l lists local_tcp local_udp log_mf_h logger logger_backend logger_config logger_disk_log_h logger_filters logger_formatter logger_h_common logger_server logger_simple_h logger_std_h logger_sup
  27. –Elixir Getting Started Guide “Elixir provides excellent interoperability with Erlang

    libraries. In fact, Elixir discourages simply wrapping Erlang libraries in favor of directly interfacing with Erlang code.”
  28. Atoms & Modules • All modules are atoms • But

    not all atoms are modules • Elixir identifiers are prefixed implicitly • Interoperability between Elixir and Erlang is very seamless
  29. –Francesco Cesarini & Steve Vinoski “OTP is a domain-independent set

    of frameworks, principles, and patterns that guide and support the structure, design, implementation, and deployment of Erlang systems.”
  30. Erlang/OTP • Language: Erlang itself, its programming model and the

    BEAM. • Tools: all the applications and libraries included. • Design Principles: supervision trees, behaviours, applications and releases.
  31. Manual Page Application alarm_handler sasl-3.2 app kernel-6.0 application kernel-6.0 appup

    sasl-3.2 array stdlib-3.5 asn1ct asn1-5.0.6 assert.hrl stdlib-3.5 auth kernel-6.0 base64 stdlib-3.5 beam_lib stdlib-3.5 binary stdlib-3.5 c stdlib-3.5 calendar stdlib-3.5 cdv observer-2.8 code kernel-6.0 common_test common_test-1.16 compile compiler-7.2 config kernel-6.0 cover tools-3.0 cprof tools-3.0 cpu_sup os_mon-2.4.5 crashdump_viewer observer-2.8 crypto crypto-4.3 crypto_app crypto-4.3 ct common_test-1.16 ct_cover common_test-1.16 ct_ftp common_test-1.16
  32. snmpc snmp-5.2.11 snmpc(command) snmp-5.2.11 snmpm snmp-5.2.11 snmpm_conf snmp-5.2.11 snmpm_mpd snmp-5.2.11

    snmpm_network_interface snmp-5.2.11 snmpm_network_interface_filter snmp-5.2.11 snmpm_user snmp-5.2.11 sofs stdlib-3.5 ssh ssh-4.7 SSH_app ssh-4.7 ssh_client_channel ssh-4.7 ssh_client_key_api ssh-4.7 ssh_connection ssh-4.7 ssh_server_channel ssh-4.7 ssh_server_key_api ssh-4.7 ssh_sftp ssh-4.7 ssh_sftpd ssh-4.7 ssl ssl-9.0 ssl_app ssl-9.0 ssl_crl_cache ssl-9.0 ssl_crl_cache_api ssl-9.0 ssl_session_cache_api ssl-9.0 start erts-10.0 start_erl erts-10.0 STDLIB_app stdlib-3.5 string stdlib-3.5 supervisor stdlib-3.5 supervisor_bridge stdlib-3.5
  33. $ iex --app inets Erlang/OTP 21 [erts-10.0.4] [source] [64-bit] [smp:4:4]

    [ds:4:4:10] [async-threads:1] [hipe] Interactive Elixir (1.7.3) - press Ctrl+C to exit (type h() ENTER for help) iex>
  34. $ iex --app inets Erlang/OTP 21 [erts-10.0.4] [source] [64-bit] [smp:4:4]

    [ds:4:4:10] [async-threads:1] [hipe] Interactive Elixir (1.7.3) - press Ctrl+C to exit (type h() ENTER for help) iex> options = [ server_name: 'Elixir Conf 2018', document_root: '.', server_root: '.', port: 8080 ] iex> {:ok, server} = :inets.start(:httpd, options) {:ok, #PID<0.116.0>}
  35. iex> :httpd.info(server) [ mime_types: [{'htm', 'text/html'}, {'html', 'text/html'}], ipfamily: :inet,

    server_name: 'Elixir Conf 2018', bind_address: :any, server_root: '.', port: 8080, document_root: '.' ]
  36. iex> :httpc.request('http://localhost:8080/index.html') {:ok, {{'HTTP/1.1', 200, 'OK'}, [ {'date', 'Wed, 05

    Sep 2018 05:55:55 GMT'}, {'etag', 'nJCRTD54'}, {'server', 'inets/7.0'}, {'content-length', '54'}, {'content-type', 'text/html'}, {'last-modified', 'Sun, 02 Sep 2018 20:19:03 GMT'} ], '<html>\n <body>\n <h1>Hello!</h1>\n </body>\n</html>\n'}}
  37. inets • Not only a fully capable HTTP server, but

    also client. • It used to manage FTP and T(rivial)FTP clients and servers, but they have been recently split out into their own apps.
  38. ssh

  39. $ iex --app ssh Erlang/OTP 21 [erts-10.0.4] [source] [64-bit] [smp:4:4]

    [ds:4:4:10] [async-threads:1] [hipe] Interactive Elixir (1.7.2) - press Ctrl+C to exit (type h() ENTER for help) iex> :ok = :ssh.shell('secure.locati.on', user: 'me', password: ‘secret') :ok [email protected]:> pwd /home/me [email protected]:> exit logout iex>
  40. ssh • Both client and server. • SFTP client and

    server too. • Basically everything you can do with your unix ssh. • Even tunneling!
  41. ssh • Both client and server. • SFTP client and

    server too. • Basically everything you can do with your unix ssh. • Even tunneling!
  42. ets

  43. defmodule Plug.Session.ETS do @moduledoc """ Stores the session in an

    in-memory ETS table. The data is stored in ETS in the following format: {sid :: String.t, data :: map, timestamp :: :erlang.timestamp} The timestamp is updated whenever there is a read or write to the table and it may be used to detect if a session is still active. ## Examples # Create an ETS table when the application starts :ets.new(:session, [:named_table, :public, read_concurrency: true]) # Use the session plug with the table name plug Plug.Session, store: :ets, key: "sid", table: :session """ @behaviour Plug.Session.Store def init(opts) do Keyword.fetch!(opts, :table) end end
  44. def get(_conn, sid, table) do case :ets.lookup(table, sid) do [{^sid,

    data, _timestamp}] -> :ets.update_element(table, sid, {3, now()}) {sid, data} [] -> {nil, %{}} end end def put(_conn, sid, data, table) do :ets.insert(table, {sid, data, now()}) sid end def delete(_conn, sid, table) do :ets.delete(table, sid) :ok end defp now, do: :os.timestamp()
  45. def get(_conn, sid, table) do case :ets.lookup(table, sid) do [{^sid,

    data, _timestamp}] -> :ets.update_element(table, sid, {3, now()}) {sid, data} [] -> {nil, %{}} end end def put(_conn, sid, data, table) do :ets.insert(table, {sid, data, now()}) sid end def delete(_conn, sid, table) do :ets.delete(table, sid) :ok end defp now, do: :os.timestamp()
  46. def get(_conn, sid, table) do case :ets.lookup(table, sid) do [{^sid,

    data, _timestamp}] -> :ets.update_element(table, sid, {3, now()}) {sid, data} [] -> {nil, %{}} end end def put(_conn, sid, data, table) do :ets.insert(table, {sid, data, now()}) sid end def delete(_conn, sid, table) do :ets.delete(table, sid) :ok end defp now, do: :os.timestamp()
  47. def get(_conn, sid, table) do case :ets.lookup(table, sid) do [{^sid,

    data, _timestamp}] -> :ets.update_element(table, sid, {3, now()}) {sid, data} [] -> {nil, %{}} end end def put(_conn, sid, data, table) do :ets.insert(table, {sid, data, now()}) sid end def delete(_conn, sid, table) do :ets.delete(table, sid) :ok end defp now, do: :os.timestamp()
  48. @max_tries 100 def put(_conn, nil, data, table) do put_new(data, table)

    end def put(_conn, sid, data, table) do :ets.insert(table, {sid, data, now()}) sid end defp put_new(data, table, counter \\ 0) when counter < @max_tries do sid = Base.encode64(:crypto.strong_rand_bytes(96)) if :ets.insert_new(table, {sid, data, now()}) do sid else put_new(data, table, counter + 1) end end defp now, do: :os.timestamp()
  49. @max_tries 100 def put(_conn, nil, data, table) do put_new(data, table)

    end def put(_conn, sid, data, table) do :ets.insert(table, {sid, data, now()}) sid end defp put_new(data, table, counter \\ 0) when counter < @max_tries do sid = Base.encode64(:crypto.strong_rand_bytes(96)) if :ets.insert_new(table, {sid, data, now()}) do sid else put_new(data, table, counter + 1) end end defp now, do: :os.timestamp()
  50. @max_tries 100 def put(_conn, nil, data, table) do put_new(data, table)

    end def put(_conn, sid, data, table) do :ets.insert(table, {sid, data, now()}) sid end defp put_new(data, table, counter \\ 0) when counter < @max_tries do sid = Base.encode64(:crypto.strong_rand_bytes(96)) if :ets.insert_new(table, {sid, data, now()}) do sid else put_new(data, table, counter + 1) end end defp now, do: :os.timestamp()
  51. @max_tries 100 def put(_conn, nil, data, table) do put_new(data, table)

    end def put(_conn, sid, data, table) do :ets.insert(table, {sid, data, now()}) sid end defp put_new(data, table, counter \\ 0) when counter < @max_tries do sid = Base.encode64(:crypto.strong_rand_bytes(96)) if :ets.insert_new(table, {sid, data, now()}) do sid else put_new(data, table, counter + 1) end end defp now, do: :os.timestamp()
  52. –Event Tracer User’s Guide “Erlang tracing is a seething pile

    of pain that involves reasonably complex knowledge of clever ports, tracing return formats, and specialized tracing MatchSpecs (which are really their own special kind of hell). The tracing mechanism is very powerful indeed, but it can be hard to grasp.”
  53. :et_viewer.start( trace_global: true, trace_pattern: {:et, :max}, title: "Making Sandwich", width:

    500, height: 400 ) sandwich = %{ bread: :white, mayo: true, tomato: false } :et.trace_me(70, :alice, :bob, :make_sandwich, sandwich) :et.trace_me(80, :bob, :fridge, :get_bread, :white) :et.trace_me(80, :fridge, :bob, :give_bread, :white) :et.trace_me(80, :bob, :fridge, :get_spread, :mayo) :et.trace_me(80, :fridge, :bob, :give_spread, :mayo) :et.trace_me(90, :bob, :bob, :assemble_sandwich, [:white_bread, :mayo, :filling]) :et.trace_me(70, :bob, :alice, :sandwich_ready, sandwich)
  54. et • Collects and displays events in real-time. • Nice

    filtering features. • Possibilities seem to be endless: debugging a piece of concurrent code, building simulated scenarios, understanding a new codebase.
  55. wx

  56. defmodule Longest do def common_prefix([]), do: raise(ArgumentError) def common_prefix([word]), do:

    String.length(word) def common_prefix([word1, word2 | words]) do length1 = common_prefix([word2 | words]) length2 = common_prefix(word1, word2, 0) Enum.min([length1, length2]) end defp common_prefix(<<hd::utf8, a::binary>>, <<hd::utf8, b::binary>>, count) do common_prefix(a, b, count + 1) end defp common_prefix(_, _, count), do: count end
  57. defmodule Longest do def common_prefix([]), do: raise(ArgumentError) def common_prefix(words), do:

    common_prefix(words, 0) defp common_prefix([word | words] = all, position) do common = Enum.reduce_while(words, String.at(word, position), fn current, char -> if char && char == String.at(current, position), do: {:cont, char}, else: {:halt, false} end) if common, do: common_prefix(all, position + 1), else: position end end
  58. test "convoluted path" do board = remove_spaces([ ". X X

    . .", " X . X . X", " . X . X .", " . X X . .", " O O O O O" ]) assert Connect.result_for(board) == :black end
  59. defp black_wins?(board) do # ... black |> graph() |> connects?(left,

    right) end defp white_wins?(board) do # ... white |> graph() |> connects?(top, bottom) end
  60. defp graph(tiles) do graph = :digraph.new() for tile <- tiles,

    do: :digraph.add_vertex(graph, tile) for tile <- tiles, do: connect_neighbors(graph, tile) graph end defp connect_neighbors(graph, tile) do for neighbor <- neighbors(tile), :digraph.vertex(graph, neighbor) do :digraph.add_edge(graph, tile, neighbor) :digraph.add_edge(graph, neighbor, tile) end end
  61. defp graph(tiles) do graph = :digraph.new() for tile <- tiles,

    do: :digraph.add_vertex(graph, tile) for tile <- tiles, do: connect_neighbors(graph, tile) graph end defp connect_neighbors(graph, tile) do for neighbor <- neighbors(tile), :digraph.vertex(graph, neighbor) do :digraph.add_edge(graph, tile, neighbor) :digraph.add_edge(graph, neighbor, tile) end end ⚠ Mutability ⚠
  62. not_flat = [1, [2, 3, [4, [5, 6]], 7], 8]

    not_flat |> List.flatten() |> length() # 8
  63. not_flat = [1, [2, 3, [4, [5, 6]], 7], 8]

    :lists.flatlength(not_flat) # 8
  64. list = [1, 2, 3, 4, 5] {_, body} =

    List.pop_at(list, -1) body # [1, 2, 3, 4]
  65. def time_this(fun) do start = System.monotonic_time(:microseconds) result = fun.() elapsed

    = System.monotonic_time(:microseconds) - start; {elapsed, result} end time_this(fn -> "too fast" end) # {5, "too fast"}
  66. iex> q = :queue.new() {[], []} iex> q = :queue.in(:first,

    q) {[:first], []} iex> q = :queue.in(:second, q) {[:second], [:first]} iex> q = :queue.in(:third, q) {[:third, :second], [:first]} iex> q = :queue.in(:fourth, q) {[:fourth, :third, :second], [:first]} iex> {{:value, first}, q} = :queue.out(q) {{:value, :first}, {[:fourth, :third], [:second]}} iex> first :first iex> q {[:fourth, :third], [:second]}
  67. iex> :rand.uniform(20) 13 iex> :rand.uniform(30) 25 iex> :rand.uniform(30) 12 iex>

    :rand.uniform(30) 27 iex> :rand.uniform(30) 10 iex> :rand.uniform(30) 6 iex> :rand.uniform(30) 2 iex> :rand.uniform(30) 20 iex> :rand.uniform(30) 16 iex> :rand.uniform(30) 9 iex> :rand.uniform(30) 28
  68. “If you ever feel something is missing from Elixir standard

    libraries, it’s for a reason. It’s somewhere else, and that’s by design. So go explore. Don’t stop at Elixir, there’s a lot underneath the surface.”
  69. Resources and References • Plug Session ETS adapter • https://github.com/elixir-plug/plug/blob/v1.6.2/lib/plug/session/ets.ex

    • Open Erlang - celebrating 20 years of open sourced Erlang • https://www.erlang-solutions.com/open-erlang.html • Open-source Erlang - White Paper • https://web.archive.org/web/20150721215830/http://ftp.sunet.se/pub/lang/erlang/white_paper.html • Elixir Getting Started Guides - Erlang Libraries • https://elixir-lang.org/getting-started/erlang-libraries.html • Designing for Scalability with Erlang/OTP: Implementing Robust, Fault-Tolerant Systems • https://www.goodreads.com/book/show/18324312-designing-for-scalability-with-erlang-otp • Erlang Programming • https://www.goodreads.com/book/show/4826120-erlang-programming
  70. Resources and References • Erlang/OTP 21.0 official documentation • http://erlang.org/doc/index.html

    • Learn You Some Erlang for Great Good! • https://learnyousomeerlang.com/ • Implementing SSH tunnels in Elixir • https://medium.com/@Drowzy/implementing-ssh-tunnels-in-elixir-e7ad9d1af01a • Exercism Connect challenge and solution • https://exercism.io/solutions/bb97260806b349c2a14203af56846d7a • Hex (board game) • https://en.wikipedia.org/wiki/Hex_(board_game) • Mix dependencies converger • https://github.com/elixir-lang/elixir/blob/v1.7.3/lib/mix/lib/mix/dep/converger.ex
  71. Image Credits • https://unsplash.com/photos/Y3vPEuNlf7w • https://unsplash.com/photos/nCj0zBLIaAk • https://www.adweek.com/agencies/nikes-new-soccer-film-is-a-monster-of-a-tall-tale/ • https://www.reshot.com/photos/red-christmas-ribbon-coffee-cup-cup-box-xmas-present-present-box-christmas-holidays_rs_P3ryYN

    • https://unsplash.com/photos/MGaFENpDCsw • https://unsplash.com/photos/1YXkcpxu8S8 • https://unsplash.com/photos/uGcDWKN91Fs • https://unsplash.com/photos/JuFcQxgCXwA • https://pixabay.com/en/move-key-new-apartment-catchment-2481718/ • https://www.pexels.com/photo/man-in-grey-dress-shirt-using-brown-cardboard-vr-glasses-936575/ • https://unsplash.com/photos/qWwpHwip31M • https://www.pexels.com/photo/close-up-of-crayons-inside-box-1234937/ • https://unsplash.com/photos/wEJK4q_YlNQ • https://unsplash.com/photos/sxNt9g77PE0