Share
Embed
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
Transcript
141 slides
-
# 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 -
# 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 -
# 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 -
# 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 -
# 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 -
# 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 -
# 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 -
iex> i :an_atom Term :an_atom Data type Atom Reference modules
Atom Implemented protocols IEx.Info, Inspect, String.Chars, List.Chars -
iex> i :an_atom Term :an_atom Data type Atom Reference modules
Atom Implemented protocols IEx.Info, Inspect, String.Chars, List.Chars -
iex> i :an_atom Term :an_atom Data type Atom Reference modules
Atom Implemented protocols IEx.Info, Inspect, String.Chars, List.Chars -
iex> i AnAtom Term AnAtom Data type Atom Raw representation
:"Elixir.AnAtom" Reference modules Atom Implemented protocols IEx.Info, Inspect, String.Chars, List.Chars -
iex> i AnAtom Term AnAtom Data type Atom Raw representation
:"Elixir.AnAtom" Reference modules Atom Implemented protocols IEx.Info, Inspect, String.Chars, List.Chars -
iex> i AnAtom Term AnAtom Data type Atom Raw representation
:"Elixir.AnAtom" Reference modules Atom Implemented protocols IEx.Info, Inspect, String.Chars, List.Chars -
iex> i AnAtom Term AnAtom Data type Atom Raw representation
:"Elixir.AnAtom" Reference modules Atom Implemented protocols IEx.Info, Inspect, String.Chars, List.Chars -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
–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.” -
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 -
–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.” -
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. -
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 -
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 -
$ 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> -
$ 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>} -
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: '.' ] -
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'}} -
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. -
$ 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> -
ssh • Both client and server. • SFTP client and
server too. • Basically everything you can do with your unix ssh. • Even tunneling! -
ssh • Both client and server. • SFTP client and
server too. • Basically everything you can do with your unix ssh. • Even tunneling! -
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 -
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() -
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() -
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() -
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() -
@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() -
@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() -
@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() -
@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() -
–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.” -
: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) -
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. -
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 -
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 -
# I want to print this: "elixir" # centered, like
this: "--elixir--" -
–Johan Bevemyr (Cisco Systems) “90% of all internet traffic goes
through Erlang controller nodes.” -
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 -
defp black_wins?(board) do # ... black |> graph() |> connects?(left,
right) end defp white_wins?(board) do # ... white |> graph() |> connects?(top, bottom) end -
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 -
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 ⚠ -
not_flat = [1, [2, 3, [4, [5, 6]], 7], 8]
not_flat |> List.flatten() |> length() # 8 -
not_flat = [1, [2, 3, [4, [5, 6]], 7], 8]
:lists.flatlength(not_flat) # 8 -
list = [1, 2, 3, 4, 5] {_, body} =
List.pop_at(list, -1) body # [1, 2, 3, 4] -
list = [1, 2, 3, 4, 5] Enum.slice(list, 0..-2) #
[1, 2, 3, 4] -
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"} -
three_hours = 3 * 60 * 60 * 1000 #
10_800_000 -
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]} -
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 -
“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.” -
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 -
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 -
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