├── .gitignore ├── LICENSE ├── README.md ├── config ├── config.exs ├── dev.exs ├── ecto_perf.exs ├── libcluster.exs ├── prod.exs ├── tenancy.exs └── test.exs ├── data └── client.json ├── dist.sh ├── lib ├── ecto_bench │ ├── ectobench.ex │ ├── repo.ex │ ├── table1.ex │ ├── table2.ex │ └── table3.ex ├── exploring_elixir.ex ├── exploring_elixir │ ├── application.ex │ ├── e001 │ │ └── jsonfilter.ex │ ├── e002 │ │ ├── benchmark_ets.ex │ │ ├── benchmark_map.ex │ │ └── onefive.ex │ ├── e003 │ │ ├── childspec.ex │ │ └── randomjump.ex │ ├── e004 │ │ └── collatz.ex │ ├── e006 │ │ ├── schemas │ │ │ ├── order.ex │ │ │ └── orderitem.ex │ │ ├── tenant_orders.ex │ │ ├── tenants.ex │ │ └── tenants_repo.ex │ ├── e007 │ │ ├── autocluster.ex │ │ └── dist.ex │ └── e008 │ │ └── time.ex └── toolbelt.ex ├── mix.exs ├── mix.lock ├── priv ├── repo │ └── migrations │ │ └── 20170628102407_add_ectobench_tables.exs └── tenants │ ├── migrations │ ├── 20170801141717_items.exs │ └── 20171001104601_dates_and_times.exs │ └── tenant_migrations │ ├── 20170801085445_tenant_orders.exs │ ├── 20170801141007_tenant_orderitems.exs │ ├── 20170801141947_reference_shared_items.exs │ └── 20170804140705_constrain_item_counts.exs ├── src └── all_atoms.erl └── test ├── collatz_test.exs ├── exploring_elixir_test.exs └── test_helper.exs /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | .*.swp 22 | 23 | /benchmarks 24 | /.tool-versions 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017 Aaron Seigo 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ExploringElixir 2 | 3 | Contains code examples used in the Exploring Elixir screencast series: 4 | 5 | https://www.youtube.com/ExploringElixir 6 | 7 | Note that this is not a "coherent" Elixir application, but rather the 8 | collection of snippets and modules used in the screencasts. 9 | 10 | Code for each episode is typically found in lib/exploring_elixir/e###/ 11 | 12 | Setup functions for each episode are found in the ExploringElixir module, 13 | so a typical interactive session will look like: 14 | 15 | > iex -S mix 16 | iex(1)> ExploringElixir.episode1 17 | .. some output .. 18 | iex(2)> 19 | 20 | At which point you can continue exploring the code for that episode. 21 | 22 | Otherwise, the usual `mix do deps.get, compile` should suffice to try it out. 23 | Benchmarks should be run with `MIX_ENV=prod`. 24 | 25 | Thanks to everyone who contributes to Elixir and its community, with 26 | a special thanks to the authors of the libraris used in this repository! 27 | 28 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | import_config "ecto_perf.exs" 4 | import_config "tenancy.exs" 5 | import_config "libcluster.exs" 6 | 7 | config :exploring_elixir, 8 | ecto_repos: [EctoBench.Repo, ExploringElixir.Repo.Tenants] 9 | 10 | import_config "#{Mix.env}.exs" 11 | -------------------------------------------------------------------------------- /config/dev.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | config :logger, :console, format: "[$level] $message\n" 4 | 5 | -------------------------------------------------------------------------------- /config/ecto_perf.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | config :exploring_elixir, EctoBench.Repo, 4 | adapter: Ecto.Adapters.Postgres, 5 | database: "ectobench", 6 | username: "postgres", 7 | password: "", 8 | hostname: "localhost", 9 | size: 64 10 | 11 | -------------------------------------------------------------------------------- /config/libcluster.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | config :libcluster, 4 | topologies: [ 5 | exploring_elixir: [ 6 | strategy: Cluster.Strategy.Gossip, 7 | #config: {}, 8 | connect: {ExploringElixir.AutoCluster, :connect_node, []}, 9 | disconnect: {ExploringElixir.AutoCluster, :disconnect_node, []}, 10 | #list_nodes: {:erlang, :nodes, [:connected]}, 11 | #child_spec: [restart: :transient] 12 | ] 13 | ] 14 | -------------------------------------------------------------------------------- /config/prod.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | config :logger, 4 | level: :warn, 5 | compile_time_purge_level: :warn 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /config/tenancy.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | config :exploring_elixir, ExploringElixir.Repo.Tenants, 4 | adapter: Ecto.Adapters.Postgres, 5 | database: "exploring_elixir_tenants", 6 | username: "postgres", 7 | password: "", 8 | hostname: "localhost" 9 | 10 | config :triplex, 11 | tenant_prefix: "ee_" 12 | 13 | -------------------------------------------------------------------------------- /config/test.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | 4 | -------------------------------------------------------------------------------- /data/client.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "user": "jane.doe" 4 | }, 5 | "init": { 6 | "v": 1, 7 | "methods": [ 8 | "password", 9 | "captcha" 10 | ] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /dist.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [[ $# -eq 0 ]] ; then 4 | echo "For use with Episode 7's distribution example" 5 | echo "Used to start a node without epmd and with a specific node name" 6 | echo "Usage: $0 " 7 | exit 0 8 | fi 9 | 10 | export ERL_ZFLAGS="-pa _build/dev/lib/exploring_elixir/ebin/ -start-epmd false -proto_dist Elixir.ExploringElixir.Dist.Service -epmd_module Elixir.ExploringElixir.Dist.Client" 11 | iex --sname $1 -S mix 12 | -------------------------------------------------------------------------------- /lib/ecto_bench/ectobench.ex: -------------------------------------------------------------------------------- 1 | defmodule EctoBench do 2 | def simpleWrites(batch_size) do 3 | inputs = %{ 4 | "Single" => 1, 5 | "Concurrency 2" => 2, 6 | "Concurrency 16" => 16, 7 | "Concurrency 32" => 32, 8 | "Concurrency 64" => 64 9 | } 10 | Benchee.run %{ 11 | "Simple single-table writes" => fn(concurrency) -> EctoBench.simpleWrites(concurrency, batch_size) end 12 | }, inputs: inputs 13 | end 14 | 15 | def simpleWrites(1, iterations) do 16 | changeset = table1SimpleInsert() 17 | Enum.each(1..iterations, 18 | fn _ -> EctoBench.Repo.insert(changeset) end) 19 | end 20 | 21 | def simpleWrites(concurrency, iterations) do 22 | changeset = table1SimpleInsert() 23 | Flow.from_enumerable(1..iterations) 24 | |> Flow.partition(stages: concurrency) 25 | |> Flow.each(fn _ -> EctoBench.Repo.insert(changeset) end) 26 | |> Flow.run 27 | end 28 | 29 | def simpleWritesHandRolled(concurrency, iterations) do 30 | changeset = table1SimpleInsert() 31 | jobs = Integer.floor_div(iterations, concurrency) 32 | #rem = iterations - (jobs * concurrency) 33 | spawn_workers(changeset, jobs, iterations) 34 | wait_on_workers(concurrency) 35 | end 36 | 37 | defp wait_on_workers(0) do 38 | :ok 39 | end 40 | 41 | defp wait_on_workers(concurrency) do 42 | receive do 43 | :worker_done -> wait_on_workers(concurrency - 1) 44 | end 45 | end 46 | 47 | defp spawn_workers(_changeset, _segment_size, iterations) when iterations <= 0 do 48 | :ok 49 | end 50 | 51 | defp spawn_workers(changeset, segment_size, iterations) do 52 | pid = self() 53 | spawn(fn -> worker(changeset, pid, segment_size) end) 54 | spawn_workers(changeset, segment_size, iterations - segment_size) 55 | end 56 | 57 | defp worker(_changeset, pid, count) when count <= 0 do 58 | Process.send pid, :worker_done, [] 59 | end 60 | 61 | defp worker(changeset, pid, count) do 62 | EctoBench.Repo.insert(changeset) 63 | worker(changeset, pid, count - 1) 64 | end 65 | 66 | defp table1SimpleInsert do 67 | EctoBench.Table1.changeset(%EctoBench.Table1{}, %{truth: true}) 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /lib/ecto_bench/repo.ex: -------------------------------------------------------------------------------- 1 | defmodule EctoBench.Repo do 2 | use Ecto.Repo, otp_app: :exploring_elixir 3 | 4 | def child_spec(opts) do 5 | %{ 6 | id: __MODULE__, 7 | start: {__MODULE__, :start_link, [opts]}, 8 | type: :supervisor 9 | } 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/ecto_bench/table1.ex: -------------------------------------------------------------------------------- 1 | defmodule EctoBench.Table1 do 2 | use Ecto.Schema 3 | 4 | import Ecto.Changeset 5 | 6 | schema "table1" do 7 | field :truth, :boolean 8 | 9 | has_many :table2, EctoBench.Table2 10 | has_many :table3, EctoBench.Table3 11 | end 12 | 13 | def changeset(item, params) do 14 | item 15 | |> cast(params, [:truth]) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/ecto_bench/table2.ex: -------------------------------------------------------------------------------- 1 | defmodule EctoBench.Table2 do 2 | use Ecto.Schema 3 | 4 | import Ecto.Changeset 5 | 6 | schema "table2" do 7 | field :chars, :string 8 | field :textual, :string 9 | field :count, :integer, default: 0 10 | 11 | belongs_to :table1, EctoBench.Table1 12 | end 13 | 14 | def changeset(item, params) do 15 | item 16 | |> cast(params, [:chars, :textual, :count]) 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/ecto_bench/table3.ex: -------------------------------------------------------------------------------- 1 | defmodule EctoBench.Table3 do 2 | use Ecto.Schema 3 | 4 | import Ecto.Changeset 5 | 6 | schema "table3" do 7 | field :c1, :string 8 | field :c2, :string 9 | field :c3, :string 10 | field :c4, :string 11 | field :c5, :string 12 | field :c6, :string 13 | field :c7, :string 14 | field :c8, :string 15 | field :c9, :string 16 | field :c10, :string 17 | field :c11, :string 18 | field :c12, :string 19 | field :c13, :string 20 | field :c14, :string 21 | field :c15, :string 22 | field :c16, :string 23 | field :c17, :string 24 | field :c18, :string 25 | field :c19, :string 26 | field :c20, :string 27 | field :c21, :string 28 | field :c22, :string 29 | field :c23, :string 30 | field :c24, :string 31 | field :c25, :string 32 | field :c26, :string 33 | field :c27, :string 34 | field :c28, :string 35 | field :c29, :string 36 | field :c30, :string 37 | field :c31, :string 38 | field :c32, :string 39 | field :c33, :string 40 | field :c34, :string 41 | field :c35, :string 42 | field :c36, :string 43 | field :c37, :string 44 | field :c38, :string 45 | field :c39, :string 46 | 47 | belongs_to :table1, EctoBench.Table1 48 | end 49 | 50 | def changeset(item, params) do 51 | item 52 | |> cast(params, [:c1, :c2, :c3, :c4, :c5, :c6, :c7, :c8, :c9, :c10, :c11, :c12, :c13, :c14, :c15, :c16, :c17, :c18, :c19, :c20, :c21, :c22, :c23, :c24, :c25, :c26, :c27, :c28, :c29, :c30, :c31, :c32, :c33, :c34, :c35, :c36, :c37, :c38, :c39]) 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/exploring_elixir.ex: -------------------------------------------------------------------------------- 1 | defmodule ExploringElixir do 2 | require Logger 3 | 4 | def episode1 do 5 | # Emulates a hypothetical service (web service, over a TCP socket, 6 | # another OTP process, etc.) that transforms some JSON for us ... 7 | # but which suffers from some bugs? 8 | f = File.read!("data/client.json") 9 | ExploringElixir.JSONFilter.extract self(), f, "data" 10 | Toolbelt.flush() 11 | end 12 | 13 | def episode2 do 14 | # Features 15 | ExploringElixir.OneFive.ping 16 | ExploringElixir.OneFive.unicode_atoms 17 | ExploringElixir.OneFive.rand_jump 18 | 19 | # Benchmarks 20 | ExploringElixir.Benchmark.Map.match 21 | ExploringElixir.Benchmark.Ets.creation 22 | ExploringElixir.Benchmark.Ets.population 23 | end 24 | 25 | def episode3 do 26 | IO.puts "Using child_spec/1, we launched various processes in ExploringElixir.ChildSpec" 27 | IO.puts "Look in lib/exploring_elixir/application.ex to see how clean it is!" 28 | IO.puts "Now lets call into them to show they are indeed running:" 29 | IO.inspect ExploringElixir.ChildSpec.ping ExploringElixir.ChildSpec.Permanent 30 | IO.inspect ExploringElixir.ChildSpec.ping ExploringElixir.ChildSpec.Temporary 31 | ExploringElixir.ChildSpec.RandomJump.rand 100 32 | end 33 | 34 | def episode4 do 35 | IO.puts "Run the property tests with `mix test --only collatz`" 36 | IO.puts "NOTE: this will recompile the project in test mode!" 37 | 38 | count = 10 39 | IO.puts "Run with the first #{count} positive integers:" 40 | ExploringElixir.Collatz.step_count_for Enum.to_list 1..count 41 | end 42 | 43 | def episode5 do 44 | end 45 | 46 | def episode6 do 47 | Application.ensure_all_started :postgrex 48 | Supervisor.start_child ExploringElixir.Supervisor, ExploringElixir.Repo.Tenants.child_spec([]) 49 | ExploringElixir.Tenants.list 50 | end 51 | 52 | def episode7 do 53 | ExploringElixir.AutoCluster.start() 54 | end 55 | 56 | def episode8 do 57 | import OK, only: ["~>>": 2] 58 | alias ExploringElixir.Time, as: T 59 | 60 | Application.ensure_all_started :timex 61 | Application.ensure_all_started :postgrex 62 | Supervisor.start_child ExploringElixir.Supervisor, ExploringElixir.Repo.Tenants.child_spec([]) 63 | 64 | IO.puts "== Timestamps .. so many ==" 65 | IO.puts "This computer believes the timestamp to be #{T.Local.os_timestamp}, but this may drift around on us" 66 | IO.puts "This BEAM vm believes the timestamp to be #{T.Local.os_timestamp}, but this may also drift around on us as well as in relation to the OS time" 67 | IO.puts "Here is a monotonic (always increasing) time: #{T.Local.monotonic_time}" 68 | IO.puts "The monotonic time is offset from the \"real\" time by #{T.Local.monotonic_time_offset}" 69 | IO.puts "So the actual time is something like: #{T.Local.adjusted_monotonic_time}" 70 | IO.puts "" 71 | IO.puts "== Zoneless Times and Dates, aka Naive ==" 72 | IO.puts "A point in time: #{T.Local.current_time}" 73 | IO.puts "A point in the calendar: #{T.Local.current_date}" 74 | IO.puts "Moving a point in the calendar into the past by one day: #{T.Local.yesterday}" 75 | IO.puts "If we are sceptical, here's the difference: #{T.Local.just_a_day_away} day" 76 | IO.puts "" 77 | 78 | IO.puts "== Calendars ==" 79 | IO.puts "In the standard ISO (Gregorian) calendar, today is: #{T.Calendars.today_iso}" 80 | IO.puts "In the Jalaali calendar, today is: #{T.Calendars.today_jalaali}" 81 | IO.puts "Converting from Gregorian to Jalaali is easy: #{T.Calendars.convert_to_jalaali ~D[1975-06-18]}" 82 | 83 | IO.puts "The next week of Gregorian days are: " 84 | T.for_next_week fn date -> date 85 | |> Timex.format("%A", :strftime) 86 | ~>> (fn x -> " " <> x end).() 87 | |> IO.puts 88 | end 89 | 90 | IO.puts "The next week of Jalaali days are: " 91 | T.for_next_week fn date -> date 92 | |> T.Calendars.convert_to_jalaali 93 | |> Timex.format("%A", :strftime) 94 | ~>> (fn x -> " " <> x end).() 95 | |> IO.puts 96 | end 97 | 98 | 99 | dates = [ 100 | {Timex.add(DateTime.utc_now, Timex.Duration.from_days(-1)), "yesterday"}, 101 | {DateTime.utc_now, "today"}, 102 | {Timex.now("America/Vancouver"), "Vancouver"}, 103 | {Timex.add(DateTime.utc_now, Timex.Duration.from_days(1)), "tomorrow"} 104 | ] 105 | 106 | IO.puts "" 107 | IO.puts "== With Ecto ==" 108 | IO.puts "Filing the database with some data..." 109 | Enum.each dates, fn {date, data} -> IO.puts "Inserting -> #{data}"; T.Stored.put date, data end 110 | 111 | DateTime.utc_now 112 | |> ExploringElixir.Time.Stored.get 113 | |> (fn x -> IO.puts "Today's data: #{inspect x}" end).() 114 | end 115 | 116 | def ecto_perf do 117 | Application.ensure_all_started :postgrex 118 | Supervisor.start_child ExploringElixir.Supervisor, EctoBench.Repo.child_spec([]) 119 | Enum.each [10, 100, 1000, 100000], fn x -> EctoBench.simpleWrites x end 120 | end 121 | end 122 | -------------------------------------------------------------------------------- /lib/exploring_elixir/application.ex: -------------------------------------------------------------------------------- 1 | defmodule ExploringElixir.Application do 2 | @moduledoc false 3 | 4 | use Application 5 | 6 | def start(_type, _args) do 7 | children = [ 8 | ExploringElixir.ChildSpec, 9 | {ExploringElixir.ChildSpec, %{type: :forever}}, 10 | {ExploringElixir.ChildSpec, %{type: :random}} 11 | ] 12 | 13 | opts = [strategy: :one_for_one, name: ExploringElixir.Supervisor] 14 | Supervisor.start_link(children, opts) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/exploring_elixir/e001/jsonfilter.ex: -------------------------------------------------------------------------------- 1 | defmodule ExploringElixir.JSONFilter do 2 | def extract(pid, json, key) when is_pid(pid) and is_binary(json) and is_binary(key) do 3 | {_worker_pid, _monitor_ref} = spawn_monitor(__MODULE__, :extract_data, [self(), json, key]) 4 | wait_for_response pid 5 | end 6 | 7 | def wait_for_response(pid) do 8 | receive do 9 | {:DOWN, _monitor, _func, _pid, :normal} -> Process.send pid, "Processing successful!", [] 10 | {:DOWN, _monitor, _func, _pid, reason} -> Process.send pid, {:error, "Processing failed: #{inspect reason}"}, [] 11 | data -> 12 | Process.send pid, data, [] 13 | wait_for_response pid 14 | after 15 | 1_000 -> Process.send pid, {:error, "Timeout"}, [] 16 | end 17 | end 18 | 19 | def extract_data(pid, json, key) when is_pid(pid) and is_binary(json) and is_binary(key) do 20 | {:ok, term} = Poison.decode json 21 | Process.send pid, {:progress, 50}, [] 22 | Process.send pid, term[key], [] 23 | Process.send pid, {:progress, 100}, [] 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/exploring_elixir/e002/benchmark_ets.ex: -------------------------------------------------------------------------------- 1 | defmodule ExploringElixir.Benchmark.Ets do 2 | def creation do 3 | Benchee.run %{ 4 | "Create and destroy 10_000 ets tables" => 5 | fn -> 6 | Enum.each(1..10_000, fn count -> :ets.new(String.to_atom(Integer.to_string(count)), [:named_table]) end) 7 | Enum.each(1..10_000, fn count -> :ets.delete(String.to_atom(Integer.to_string(count))) end) 8 | end, 9 | "Create and destroy 10_000 ets tables in parallel" => 10 | fn -> 11 | Flow.from_enumerable(1..10_000) 12 | |> Flow.each(fn number -> :ets.new(String.to_atom(Integer.to_string(number)), [:named_table, :public]) end) 13 | |> Flow.run 14 | end, 15 | }, formatters: [&Benchee.Formatters.HTML.output/1], 16 | formatter_options: [html: [file: "benchmarks/ets_creation.html"]] 17 | end 18 | 19 | @table_name :large_table_test 20 | def population do 21 | %{dates: dates, atoms: atoms} = ExploringElixir.Benchmark.Map.init_maps() 22 | sizes = %{ 23 | "Few rows, large data" => {100, dates, atoms}, 24 | "Few rows, small data" => {100, self(), self()}, 25 | "Medium row count, small data" => {10_000, self(), self()}, 26 | "Medium row count, larger data" => {10_000, dates, self()}, 27 | "Large row count, small data" => {100_000, self(), self()} 28 | } 29 | 30 | Benchee.run %{ 31 | "Inserting rows" => 32 | fn {rows, data_set_a, data_set_b} -> 33 | :ets.new(@table_name, [:named_table, :public]) 34 | fill_ets_table(rows, data_set_a, data_set_b) 35 | :ets.delete(@table_name) 36 | end, 37 | "Lookup up random rows, including some misses" => 38 | fn {rows, data_set_a, data_set_b} -> 39 | :ets.new(@table_name, [:named_table, :public]) 40 | fill_ets_table(rows, data_set_a, data_set_b) 41 | rand_range = round(rows * 1.2) 42 | Enum.each(1..rows, 43 | fn _x -> :ets.lookup(@table_name, :rand.uniform(rand_range)) end) 44 | :ets.delete(@table_name) 45 | end 46 | }, inputs: sizes, 47 | formatters: [&Benchee.Formatters.HTML.output/1], 48 | formatter_options: [html: [file: "benchmarks/ets_tables.html"]] 49 | end 50 | 51 | defp fill_ets_table(rows, data_set_a, data_set_b) do 52 | half = Integer.floor_div(rows, 2) 53 | Enum.each(1..half, 54 | fn x -> :ets.insert(@table_name, {x, data_set_a}) end) 55 | Enum.each((half + 1)..rows, 56 | fn x -> :ets.insert(@table_name, {x, data_set_b}) end) 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/exploring_elixir/e002/benchmark_map.ex: -------------------------------------------------------------------------------- 1 | defmodule ExploringElixir.Benchmark.Map do 2 | def match do 3 | %{dates: dates, atoms: atoms} = init_maps() 4 | count = 1000 5 | 6 | Benchee.run %{ 7 | "Function header matching" => 8 | fn -> function_headers(dates, atoms, count, count) end, 9 | "Inline matching" => 10 | fn -> inline_match(dates, atoms, count, count) end, 11 | "Record matching" => 12 | fn -> record_matching(perhaps(), count) end 13 | }, formatters: [&Benchee.Formatters.HTML.output/1], 14 | formatter_options: [html: [file: "benchmarks/map_match.html"]] 15 | end 16 | 17 | def function_headers(_dates, _atoms, garbage, 0) do 18 | garbage 19 | end 20 | 21 | def function_headers(dates, atoms, _garbage, count) do 22 | date = match(dates) 23 | uuid = match(atoms) 24 | function_headers(dates, atoms, {date, uuid}, count - 1) 25 | end 26 | 27 | def match(%{"2018-07-02 00:00:00Z": date}), do: date 28 | def match(%{:to_uniq_entries => a, :comprehension_filter => b, :"Australia/Hobart" => c}), do: {a, b, c} 29 | def match(%{:"MACRO-unquote" => uuid}), do: uuid 30 | def match(%{imports_from_env: uuid}), do: uuid 31 | def match(%{ctime: uuid}), do: uuid 32 | def match(%{"2017-07-02 00:00:00Z": date}), do: date 33 | def match(_), do: :not_found 34 | 35 | def inline_match(_dates, _atoms, garbage, 0) do 36 | garbage 37 | end 38 | 39 | def inline_match(dates, atoms, _garbage, count) do 40 | %{today: date} = dates 41 | %{:to_uniq_entries => a, :comprehension_filter => b, :"Australia/Hobart" => c} = atoms 42 | inline_match(dates, atoms, {date, a, b, c}, count - 1) 43 | end 44 | 45 | def record_matching(_, 0), do: :ok 46 | def record_matching({:ok, _x}, count), do: record_matching(perhaps(), count - 1) 47 | def record_matching({:error, _x}, count), do: record_matching(perhaps(), count - 1) 48 | 49 | def perhaps, do: perhaps(:rand.uniform(100)) 50 | def perhaps(x) when x > 50, do: {:ok, x} 51 | def perhaps(x), do: {:error, x} 52 | 53 | def init_maps() do 54 | now = Timex.today 55 | date_map = 56 | Enum.reduce(1..(365*2), %{today: now}, 57 | fn(days, date_map) -> 58 | then = Timex.shift(now, days: days) 59 | Map.put(date_map, DateTime.to_string(Timex.to_datetime(then)), then) 60 | end) 61 | 62 | atom_map = 63 | Enum.reduce(:all_atoms.all_atoms, %{}, 64 | fn(atom, atom_map) -> 65 | Map.put(atom_map, atom, UUID.uuid4) 66 | end) 67 | 68 | %{ 69 | dates: date_map, 70 | atoms: atom_map 71 | } 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /lib/exploring_elixir/e002/onefive.ex: -------------------------------------------------------------------------------- 1 | defmodule ExploringElixir.OneFive do 2 | use GenServer 3 | 4 | def child_spec(args) do 5 | %{id: __MODULE__, start: {__MODULE__, :start_link, [args]}, restart: :permanent, type: :worker} 6 | end 7 | 8 | def start_link(args) do 9 | GenServer.start_link __MODULE__, args, name: __MODULE__ 10 | end 11 | 12 | def ping do 13 | GenServer.cast __MODULE__, {:ping, self()} 14 | end 15 | 16 | @impl true 17 | def handle_cast({:ping, pid}, state) do 18 | Process.send pid, :pong, [] 19 | {:noreply, state} 20 | end 21 | 22 | def unicode_atoms do 23 | IO.puts :こんにちは世界 24 | IO.puts :Zürich 25 | end 26 | 27 | def rand_jump do 28 | Enum.each(1..5, fn _ -> IO.puts :rand.uniform(100_000) end) 29 | 30 | IO.puts ".. and now with jump!" 31 | state = :rand.jump() 32 | Enum.reduce(1..5, state, fn _, state -> {number, state} = :rand.uniform_s(100_000, state); IO.puts(number); state end) 33 | 34 | IO.puts ".. and now in a flow!" 35 | iterations = 5000 36 | concurrency = 50 37 | Flow.from_enumerable(1..iterations) 38 | |> Flow.partition(stages: concurrency) 39 | |> Flow.reduce(fn -> :rand.jump() end, fn _input, state -> {number, state} = :rand.uniform_s(100_000, state); IO.puts("#{inspect self()}: #{number}"); state end) 40 | |> Flow.run 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/exploring_elixir/e003/childspec.ex: -------------------------------------------------------------------------------- 1 | defmodule ExploringElixir.ChildSpec do 2 | use GenServer 3 | 4 | def child_spec(%{type: :random}) do 5 | %{id: ExploringElixir.ChildSpec.RandomJump, 6 | start: {ExploringElixir.ChildSpec.RandomJump, :start_link, []}, 7 | restart: :permanent, type: :worker} 8 | end 9 | 10 | def child_spec(%{type: :forever}) do 11 | name = Module.concat(__MODULE__, :Permanent) 12 | %{id: name, start: {__MODULE__, :start_link, [name]}, restart: :permanent, type: :worker} 13 | end 14 | 15 | def child_spec(_args) do 16 | name = Module.concat(__MODULE__, :Temporary) 17 | %{id: name, start: {__MODULE__, :start_link, [name]}, restart: :temporary, type: :worker} 18 | end 19 | 20 | def start_link(name) do 21 | GenServer.start_link __MODULE__, [], name: name 22 | end 23 | 24 | def ping name do 25 | GenServer.cast name, {:ping, self()} 26 | end 27 | 28 | @impl true 29 | def handle_cast({:ping, pid}, state) do 30 | Process.send pid, :pong, [] 31 | {:noreply, state} 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/exploring_elixir/e003/randomjump.ex: -------------------------------------------------------------------------------- 1 | defmodule ExploringElixir.ChildSpec.RandomJump do 2 | use GenServer 3 | 4 | def start_link() do 5 | GenServer.start_link __MODULE__, :rand.jump, name: __MODULE__ 6 | end 7 | 8 | def rand(max) when is_integer(max) do 9 | GenServer.call __MODULE__, {:rand, max} 10 | end 11 | 12 | def handle_call({:rand, max}, _from, state) when is_integer(max) do 13 | {number, state} = :rand.uniform_s(max, state); 14 | {:reply, number, state} 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/exploring_elixir/e004/collatz.ex: -------------------------------------------------------------------------------- 1 | defmodule ExploringElixir.Collatz do 2 | @moduledoc """ 3 | A very simple implementation of code to demonstrate the Collatz 4 | conjecture over the valid range of integers 5 | 6 | See https://en.wikipedia.org/wiki/Collatz_conjecture 7 | """ 8 | 9 | import Integer 10 | 11 | @spec step_count_for(values :: list|integer) :: list 12 | def step_count_for([]), do: [] 13 | 14 | def step_count_for(values) when is_list(values) do 15 | values 16 | |> Enum.sort 17 | |> Enum.uniq 18 | |> Enum.map(fn value -> {value, steps value} end) 19 | end 20 | 21 | def step_count_for(value) when is_integer(value) and value > 0 do 22 | [{value, steps value}] 23 | end 24 | 25 | @spec steps(value :: integer, step_count :: integer) :: integer 26 | defp steps(value, step_count \\ 0) # function head declaration only 27 | 28 | defp steps(1, step_count), do: step_count 29 | 30 | defp steps(value, step_count) when is_even(value) do 31 | steps(floor_div(value, 2), step_count + 1) 32 | end 33 | 34 | defp steps(value, step_count) do 35 | steps(3 * value + 1, step_count + 1) 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/exploring_elixir/e006/schemas/order.ex: -------------------------------------------------------------------------------- 1 | 2 | defmodule ExploringElixir.Tenants.Schemas.Order do 3 | use Ecto.Schema 4 | import Ecto.Changeset 5 | 6 | schema "orders" do 7 | field :name, :string 8 | timestamps() 9 | end 10 | 11 | def changeset(order, params) do 12 | order 13 | |> cast(params, [:name]) 14 | end 15 | end 16 | 17 | -------------------------------------------------------------------------------- /lib/exploring_elixir/e006/schemas/orderitem.ex: -------------------------------------------------------------------------------- 1 | 2 | defmodule ExploringElixir.Tenants.Schemas.OrderItem do 3 | use Ecto.Schema 4 | import Ecto.Changeset 5 | 6 | @primary_key false 7 | 8 | schema "orderitems" do 9 | field :item_id, :integer 10 | field :amount, :integer, default: 1 11 | 12 | timestamps() 13 | 14 | belongs_to :order, ExploringElixir.Tenants.Schemas.Order 15 | end 16 | 17 | def changeset(order, params) do 18 | order 19 | |> cast(params, [:item_id, :amount, :order_id]) 20 | |> validate_required([:item_id, :order_id]) 21 | end 22 | end 23 | 24 | -------------------------------------------------------------------------------- /lib/exploring_elixir/e006/tenant_orders.ex: -------------------------------------------------------------------------------- 1 | defmodule ExploringElixir.Tenants.Orders do 2 | use GenServer 3 | require Logger 4 | import Ecto.Query 5 | 6 | alias ExploringElixir.Repo.Tenants, as: Repo 7 | alias ExploringElixir.Tenants.Schemas.Order, as: OrderSchema 8 | alias ExploringElixir.Tenants.Schemas.OrderItem, as: OrderItemSchema 9 | 10 | @schema_meta_fields [:__meta__, :__struct__] 11 | 12 | def create_order(tenant, order_name) do 13 | if Triplex.exists? tenant, Repo do 14 | GenServer.start_link __MODULE__, {tenant, order_name} 15 | else 16 | {:error, :no_such_tenant} 17 | end 18 | end 19 | 20 | def fetch_order(tenant, order_id) do 21 | if Triplex.exists? tenant, Repo do 22 | GenServer.start_link __MODULE__, {tenant, order_id} 23 | else 24 | {:error, :no_such_tenant} 25 | end 26 | end 27 | 28 | def list_items(order) do 29 | GenServer.call(order, :list) 30 | end 31 | 32 | def add_item(order, item_id, amount) do 33 | GenServer.cast(order, {:add, item_id, amount}) 34 | end 35 | 36 | def delete_item(order, item_id) do 37 | GenServer.cast(order, {:delete, item_id}) 38 | end 39 | 40 | def delete_item(order, item_id, amount) do 41 | GenServer.cast(order, {:delete, item_id, amount}) 42 | end 43 | 44 | def list_orders(tenant) do 45 | query = from order in OrderSchema 46 | orders = Repo.all query, prefix: Triplex.to_prefix(tenant) 47 | for order <- orders, do: Map.drop(order, @schema_meta_fields) 48 | end 49 | 50 | def id(order), do: GenServer.call order, :id 51 | 52 | def init({tenant, order_name}) when is_bitstring(order_name) do 53 | changeset = OrderSchema.changeset(%OrderSchema{}, %{name: order_name}) 54 | 55 | case Repo.insert changeset, prefix: Triplex.to_prefix(tenant) do 56 | {:ok, %OrderSchema{id: order_id}} -> 57 | init({tenant, order_id}) 58 | 59 | {:error, changeset} -> 60 | Logger.warn fn -> "Failed to create template with changeset: #{inspect changeset}" end 61 | {:stop, :order_creation_failed} 62 | end 63 | end 64 | 65 | def init({tenant, order_id}) when is_integer(order_id) do 66 | query = from o in "orders", 67 | where: o.id == ^order_id, 68 | select: o.id 69 | case Repo.one query, prefix: Triplex.to_prefix(tenant) do 70 | nil -> {:stop, :no_such_order} 71 | _ -> {:ok, %{tenant: tenant, order_id: order_id}} 72 | end 73 | end 74 | 75 | def handle_call(:list, _from, 76 | %{tenant: tenant, order_id: order_id} = state) do 77 | items = 78 | from(i in "orderitems", 79 | where: i.order_id == ^order_id, 80 | select: %{item: i.item_id, order: i.order_id, amount: i.amount}) 81 | |> Repo.all(prefix: Triplex.to_prefix(tenant)) 82 | 83 | {:reply, items, state} 84 | end 85 | 86 | def handle_call(:id, _from, %{order_id: order_id} = state) do 87 | {:reply, order_id, state} 88 | end 89 | 90 | def handle_cast({:add, item_id, amount}, 91 | %{tenant: tenant, order_id: order_id} = state) do 92 | changeset = OrderItemSchema.changeset %OrderItemSchema{}, 93 | %{item_id: item_id, 94 | order_id: order_id, 95 | amount: amount} 96 | 97 | Repo.insert! changeset, 98 | prefix: Triplex.to_prefix(tenant), 99 | conflict_target: [:order_id, :item_id], 100 | on_conflict: [inc: [amount: amount]] 101 | 102 | {:noreply, state} 103 | end 104 | 105 | def handle_cast({:delete, item_id}, 106 | %{tenant: tenant, order_id: order_id} = state) do 107 | from(i in "orderitems", 108 | where: [order_id: ^order_id, item_id: ^item_id]) 109 | |> Repo.delete_all(prefix: Triplex.to_prefix(tenant)) 110 | {:noreply, state} 111 | end 112 | 113 | def handle_cast({:delete, item_id, amount}, 114 | %{tenant: tenant, order_id: order_id} = state) do 115 | from(i in "orderitems", 116 | where: [order_id: ^order_id, item_id: ^item_id]) 117 | |> Repo.update_all([inc: [amount: -amount]], [prefix: Triplex.to_prefix(tenant)]) 118 | 119 | {:noreply, state} 120 | end 121 | end 122 | -------------------------------------------------------------------------------- /lib/exploring_elixir/e006/tenants.ex: -------------------------------------------------------------------------------- 1 | defmodule ExploringElixir.Tenants do 2 | alias ExploringElixir.Repo.Tenants, as: Repo 3 | 4 | def list do 5 | Triplex.all Repo 6 | end 7 | 8 | def new(name) when is_bitstring(name) do 9 | if Triplex.exists? name, Repo do 10 | {:error, :tenant_exists} 11 | else 12 | Triplex.create name, Repo 13 | end 14 | end 15 | 16 | def remove(name) when is_bitstring(name) do 17 | Triplex.drop name, Repo 18 | end 19 | 20 | def rename(current_name, new_name) do 21 | if Triplex.exists? new_name, Repo do 22 | {:error, :tenant_exists} 23 | else 24 | Triplex.rename current_name, new_name, Repo 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/exploring_elixir/e006/tenants_repo.ex: -------------------------------------------------------------------------------- 1 | defmodule ExploringElixir.Repo.Tenants do 2 | use Ecto.Repo, otp_app: :exploring_elixir 3 | 4 | def child_spec(opts) do 5 | %{ 6 | id: __MODULE__, 7 | start: {__MODULE__, :start_link, [opts]}, 8 | type: :supervisor 9 | } 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/exploring_elixir/e007/autocluster.ex: -------------------------------------------------------------------------------- 1 | defmodule ExploringElixir.AutoCluster do 2 | def visible_nodes do 3 | Node.list() 4 | |> display_nodes("Visible Nodes") 5 | end 6 | 7 | def hidden_nodes do 8 | Node.list(:hidden) 9 | |> display_nodes("Hidden Nodes") 10 | end 11 | 12 | def all_nodes do 13 | Node.list(:known) 14 | |> display_nodes("All Nodes") 15 | end 16 | 17 | def ping_node(node) when is_atom(node), do: Node.ping node 18 | 19 | def start do 20 | monitor() 21 | Application.ensure_all_started(:libcluster) 22 | end 23 | 24 | def connect_node(node) do 25 | IO.puts "Going to connect up node #{inspect node}..." 26 | :net_kernel.connect(node) 27 | end 28 | 29 | def disconnect_node(node) do 30 | IO.puts "Going to disconnect node #{inspect node}..." 31 | :net_kernel.disconnect(node) 32 | end 33 | 34 | def monitor, do: monitor Process.whereis(:cluster_monitor) 35 | 36 | def monitor(nil) do 37 | pid = spawn( 38 | fn -> 39 | IO.puts "Starting node monitor process" 40 | :net_kernel.monitor_nodes true 41 | monitor_cluster() 42 | end 43 | ) 44 | 45 | Process.register(pid, :cluster_monitor) 46 | pid 47 | end 48 | 49 | def monitor(_), do: IO.puts "Already monitoring!" 50 | 51 | defp monitor_cluster do 52 | ExploringElixir.AutoCluster.visible_nodes() 53 | receive do 54 | {:nodeup, node} -> 55 | IO.puts good_news_marker() <> " Node joined: #{inspect node}" 56 | monitor_cluster() 57 | {:nodedown, node} -> 58 | IO.puts bad_news_marker() <> " Node departed: #{inspect node}" 59 | monitor_cluster() 60 | x -> IO.puts "Outa here with #{x}" 61 | :ok 62 | end 63 | end 64 | 65 | defp display_nodes(nodes, title) do 66 | IO.puts "#{stars()} #{title} #{stars()}" 67 | display_nodes(nodes) 68 | end 69 | 70 | defp display_nodes([]), do: IO.puts "Not connected to any cluster. We are alone." 71 | defp display_nodes(nodes) when is_list(nodes) do 72 | IO.puts "Nodes in our cluster, including ourselves:" 73 | 74 | [Node.self()|nodes] 75 | |> Enum.sort 76 | |> Enum.dedup 77 | |> Enum.each(fn node -> IO.puts " #{inspect node}" end) 78 | end 79 | 80 | defp good_news_marker, do: IO.ANSI.green() <> String.duplicate(<<0x1F603 :: utf8>>, 5) <> IO.ANSI.reset() 81 | defp bad_news_marker, do: IO.ANSI.red() <> String.duplicate(<<0x1F630 :: utf8>>, 5) <> IO.ANSI.reset() 82 | defp stars, do: String.duplicate "*", 10 83 | end 84 | -------------------------------------------------------------------------------- /lib/exploring_elixir/e007/dist.ex: -------------------------------------------------------------------------------- 1 | # Shamelessly borrowed from Magnus Henoch's fantastic Erlang Solutions article: 2 | # https://www.erlang-solutions.com/blog/erlang-and-elixir-distribution-without-epmd.html 3 | # You want to read that! :) 4 | # To use this, you will want to run dist.sh from the top level directory of this project 5 | 6 | defmodule ExploringElixir.Dist.Service do 7 | def port(name) when is_atom(name) do 8 | port Atom.to_string name 9 | end 10 | 11 | def port(name) when is_list(name) do 12 | port List.to_string name 13 | end 14 | 15 | def port(name) when is_binary(name) do 16 | # Figure out the base port. If not specified using the 17 | # inet_dist_base_port kernel environment variable, default to 18 | # 4370, one above the epmd port. 19 | base_port = :application.get_env :kernel, :inet_dist_base_port, 4370 20 | 21 | # Now, figure out our "offset" on top of the base port. The 22 | # offset is the integer just to the left of the @ sign in our node 23 | # name. If there is no such number, the offset is 0. 24 | # 25 | # Also handle the case when no hostname was specified. 26 | node_name = Regex.replace ~r/@.*$/, name, "" 27 | offset = 28 | case Regex.run ~r/[0-9]+$/, node_name do 29 | nil -> 30 | 0 31 | [offset_as_string] -> 32 | String.to_integer offset_as_string 33 | end 34 | 35 | base_port + offset 36 | end 37 | end 38 | 39 | defmodule ExploringElixir.Dist.Service_dist do 40 | 41 | def listen(name) do 42 | # Here we figure out what port we want to listen on. 43 | 44 | port = ExploringElixir.Dist.Service.port name 45 | 46 | # Set both "min" and "max" variables, to force the port number to 47 | # this one. 48 | :ok = :application.set_env :kernel, :inet_dist_listen_min, port 49 | :ok = :application.set_env :kernel, :inet_dist_listen_max, port 50 | 51 | # Finally run the real function! 52 | :inet_tcp_dist.listen name 53 | end 54 | 55 | def select(node) do 56 | :inet_tcp_dist.select node 57 | end 58 | 59 | def accept(listen) do 60 | :inet_tcp_dist.accept listen 61 | end 62 | 63 | def accept_connection(accept_pid, socket, my_node, allowed, setup_time) do 64 | IO.puts "Accepting connection! #{inspect my_node}" 65 | :inet_tcp_dist.accept_connection accept_pid, socket, my_node, allowed, setup_time 66 | end 67 | 68 | def setup(node, type, my_node, long_or_short_names, setup_time) do 69 | :inet_tcp_dist.setup node, type, my_node, long_or_short_names, setup_time 70 | end 71 | 72 | def close(listen) do 73 | :inet_tcp_dist.close listen 74 | end 75 | 76 | def childspecs do 77 | #:inet_tcp_dist.childspecs 78 | [] 79 | end 80 | end 81 | 82 | defmodule ExploringElixir.Dist.Client do 83 | # erl_distribution wants us to start a worker process. We don't 84 | # need one, though. 85 | def start_link do 86 | :ignore 87 | end 88 | 89 | # As of Erlang/OTP 19.1, register_node/3 is used instead of 90 | # register_node/2, passing along the address family, 'inet_tcp' or 91 | # 'inet6_tcp'. This makes no difference for our purposes. 92 | def register_node(name, port, _family) do 93 | register_node(name, port) 94 | end 95 | 96 | def register_node(_name, _port) do 97 | # This is where we would connect to epmd and tell it which port 98 | # we're listening on, but since we're epmd-less, we don't do that. 99 | 100 | # Need to return a "creation" number between 1 and 3. 101 | creation = :rand.uniform 3 102 | {:ok, creation} 103 | end 104 | 105 | def port_please(name, _ip) do 106 | port = ExploringElixir.Dist.Service.port name 107 | # The distribution protocol version number has been 5 ever since 108 | # Erlang/OTP R6. 109 | version = 5 110 | {:port, port, version} 111 | end 112 | 113 | def names(_hostname) do 114 | # Since we don't have epmd, we don't really know what other nodes 115 | # there are. 116 | {:error, :address} 117 | end 118 | end 119 | -------------------------------------------------------------------------------- /lib/exploring_elixir/e008/time.ex: -------------------------------------------------------------------------------- 1 | defmodule ExploringElixir.Time do 2 | def for_next_week(fun) when is_function(fun, 1) do 3 | today = Date.utc_today 4 | next_week = Date.add today, 7 5 | Date.range(today, next_week) |> Enum.each(fun) 6 | :ok 7 | end 8 | 9 | def seconds_per_day, do: 60 * 60 * 24 10 | end 11 | 12 | defmodule ExploringElixir.Time.Local do 13 | def beam_timestamp do 14 | System.system_time 15 | end 16 | 17 | def os_timestamp do 18 | System.os_time 19 | end 20 | 21 | def monotonic_time do 22 | System.monotonic_time 23 | end 24 | 25 | def monotonic_time_offset do 26 | System.time_offset 27 | end 28 | 29 | def adjusted_monotonic_time do 30 | System.monotonic_time + System.time_offset 31 | end 32 | 33 | def current_time do 34 | NaiveDateTime.to_time current_date() 35 | end 36 | 37 | def current_date do 38 | NaiveDateTime.utc_now 39 | end 40 | 41 | def yesterday do 42 | NaiveDateTime.add current_date(), -(ExploringElixir.Time.seconds_per_day()), :seconds 43 | end 44 | 45 | def just_a_day_away do 46 | Date.diff current_date(), yesterday() 47 | end 48 | end 49 | 50 | defmodule ExploringElixir.Time.Calendars do 51 | def today_iso do 52 | Date.utc_today 53 | end 54 | 55 | def today_jalaali do 56 | Date.utc_today Jalaali.Calendar 57 | end 58 | 59 | def convert_to_jalaali(date) do 60 | {:ok, date} = Date.convert date, Jalaali.Calendar 61 | date 62 | end 63 | end 64 | 65 | defmodule ExploringElixir.Time.Stored do 66 | require ExploringElixir.Repo.Tenants, as: Repo 67 | import Ecto.Query 68 | 69 | @spec put(date_time :: DateTime.t(), data :: String.t()) :: id :: integer() 70 | def put(date_time, data) when is_bitstring(data) do 71 | utc = Timex.to_datetime date_time, "Etc/UTC" 72 | Repo.insert_all "dates_and_times", [%{ 73 | a_date: DateTime.to_date(utc), 74 | a_time: DateTime.to_time(utc), 75 | with_tz: utc, 76 | data: data 77 | }] 78 | end 79 | 80 | @spec get(date :: DateTime.t()) :: [{id :: integer, data :: String.t()}] 81 | def get(date_time) do 82 | date = DateTime.to_date(date_time) 83 | query = from d in "dates_and_times", 84 | where: d.a_date == ^date, 85 | select: {d.id, d.data} 86 | Repo.all query 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /lib/toolbelt.ex: -------------------------------------------------------------------------------- 1 | defmodule Toolbelt do 2 | 3 | @moduledoc "A collection of useful snippets of Elixir" 4 | 5 | @doc """ 6 | Prints the full set of metadata for a module, as known to Module:__info__. 7 | 8 | Returns: `:ok`. 9 | """ 10 | @spec print_module_info(atom) :: :ok 11 | def print_module_info(modulename) do 12 | info_attrs = [:attributes, :compile, :exports, :functions, :macros, :md5, :module, :native_addresses] 13 | attr_printer = fn x -> IO.puts "== #{x}"; IO.inspect apply(modulename, :__info__, [x]) end 14 | 15 | Enum.each info_attrs, attr_printer 16 | end 17 | 18 | @doc "Prints all pending messages in the process' mailbox to console" 19 | @spec flush() :: :ok 20 | def flush() do 21 | receive do 22 | msg -> IO.inspect msg; flush() 23 | after 24 | 10 -> :ok 25 | end 26 | end 27 | 28 | @doc "Times a function" 29 | @spec time(fun) :: integer 30 | def time(function) do 31 | :timer.tc(function) 32 | |> elem(0) 33 | |> Kernel./(1_000_000) 34 | end 35 | 36 | @spec time(fun, list) :: integer 37 | def time(function, args) do 38 | :timer.tc(function, args) 39 | |> elem(0) 40 | |> Kernel./(1_000_000) 41 | end 42 | 43 | @spec time(atom, atom, list) :: integer 44 | def time(module, function, args) do 45 | :timer.tc(module, function, args) 46 | |> elem(0) 47 | |> Kernel./(1_000_000) 48 | end 49 | 50 | def maybe(nil, _keys), do: nil 51 | def maybe(val, []), do: val 52 | def maybe(map, [h|t]) when is_map(map), do: maybe(Map.get(map, h), t) 53 | def maybe(_, _), do: nil 54 | 55 | def random_string(num_bytes, str_length \\ 0) 56 | def random_string(num_bytes, 0), do: :crypto.strong_rand_bytes(num_bytes) |> Base.url_encode64 57 | def random_string(num_bytes, str_length), do: :crypto.strong_rand_bytes(num_bytes) |> Base.url_encode64 |> binary_part(0, str_length) 58 | end 59 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule ExploringElixir.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :exploring_elixir, 6 | version: "0.1.0", 7 | elixir: "~> 1.4", 8 | build_embedded: Mix.env == :prod, 9 | start_permanent: Mix.env == :prod, 10 | deps: deps(), 11 | aliases: aliases()] 12 | end 13 | 14 | def application do 15 | [ 16 | # as a fist-full-of-demos, let's manually start our applications 17 | # note that this is NOT best practice; normally one would not 18 | # have an applications: entry at all and just let mix do its magic 19 | # on our behalf 20 | applications: applications(Mix.env), 21 | extra_applications: [:logger], 22 | mod: {ExploringElixir.Application, []} 23 | ] 24 | end 25 | 26 | defp applications(:dev), do: [:remix] 27 | defp applications(:test), do: [:remix] 28 | defp applications(_), do: [] 29 | 30 | defp deps do 31 | [ 32 | {:benchee, "~> 0.9.0"}, 33 | {:benchee_html, "~> 0.3"}, 34 | {:ecto, "~> 2.1.6"}, 35 | {:flow, "~> 0.12" }, 36 | {:libcluster, "~> 2.2.3"}, 37 | {:poison, "~> 3.1.0"}, 38 | {:postgrex, "~> 0.13.3"}, 39 | {:timex, "~> 3.0"}, 40 | {:triplex, "~> 0.9.0"}, 41 | {:uuid, "~> 1.1.7"}, 42 | {:jalaali, "~> 0.2.1"}, 43 | {:ok, "~> 1.9"}, 44 | 45 | # dev and test dependencies 46 | {:quixir, "~>0.9", only: :test}, 47 | {:remix, "~> 0.0.2", only: [:dev, :test]}, 48 | {:credo, "~> 0.8.6", only: [:dev, :test]} 49 | ] 50 | end 51 | 52 | defp aliases do 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{"benchee": {:hex, :benchee, "0.9.0", "433d946b0e4755e186fe564568ead4f593b0d15337fcffa95ed7d5b8a6612670", [:mix], [{:deep_merge, "~> 0.1", [hex: :deep_merge, repo: "hexpm", optional: false]}], "hexpm"}, 2 | "benchee_html": {:hex, :benchee_html, "0.3.1", "4f784a567f2999e28d36c13356495f455fad4fb88c66a9c2db60ab2b60d11479", [:mix], [{:benchee, "~> 0.8", [hex: :benchee, repo: "hexpm", optional: false]}, {:benchee_json, ">= 0.3.1", [hex: :benchee_json, repo: "hexpm", optional: false]}], "hexpm"}, 3 | "benchee_json": {:hex, :benchee_json, "0.3.1", "f1df8d92041cfd863d980ca40dcba1c852a705190e26cdd41732f76c6bd099d6", [:mix], [{:benchee, "~> 0.8", [hex: :benchee, repo: "hexpm", optional: false]}, {:poison, ">= 1.4.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, 4 | "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"}, 5 | "certifi": {:hex, :certifi, "2.0.0", "a0c0e475107135f76b8c1d5bc7efb33cd3815cb3cf3dea7aefdd174dabead064", [:rebar3], [], "hexpm"}, 6 | "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm"}, 7 | "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm"}, 8 | "credo": {:hex, :credo, "0.8.6", "335f723772d35da499b5ebfdaf6b426bfb73590b6fcbc8908d476b75f8cbca3f", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}], "hexpm"}, 9 | "db_connection": {:hex, :db_connection, "1.1.2", "2865c2a4bae0714e2213a0ce60a1b12d76a6efba0c51fbda59c9ab8d1accc7a8", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"}, 10 | "decimal": {:hex, :decimal, "1.4.0", "fac965ce71a46aab53d3a6ce45662806bdd708a4a95a65cde8a12eb0124a1333", [:mix], [], "hexpm"}, 11 | "deep_merge": {:hex, :deep_merge, "0.1.1", "c27866a7524a337b6a039eeb8dd4f17d458fd40fbbcb8c54661b71a22fffe846", [:mix], [], "hexpm"}, 12 | "ecto": {:hex, :ecto, "2.1.6", "29b45f393c2ecd99f83e418ea9b0a2af6078ecb30f401481abac8a473c490f84", [:mix], [{:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: true]}, {:decimal, "~> 1.2", [hex: :decimal, repo: "hexpm", optional: false]}, {:mariaex, "~> 0.8.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.13.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"}, 13 | "flow": {:hex, :flow, "0.12.0", "32c5a5f3ff6693e004b6c17a8c64dce2f8cdaf9564912d79427176013a586ab6", [:mix], [{:gen_stage, "~> 0.12.0", [hex: :gen_stage, repo: "hexpm", optional: false]}], "hexpm"}, 14 | "gen_stage": {:hex, :gen_stage, "0.12.2", "e0e347cbb1ceb5f4e68a526aec4d64b54ad721f0a8b30aa9d28e0ad749419cbb", [:mix], [], "hexpm"}, 15 | "gettext": {:hex, :gettext, "0.13.1", "5e0daf4e7636d771c4c71ad5f3f53ba09a9ae5c250e1ab9c42ba9edccc476263", [:mix], [], "hexpm"}, 16 | "hackney": {:hex, :hackney, "1.9.0", "51c506afc0a365868469dcfc79a9d0b94d896ec741cfd5bd338f49a5ec515bfe", [:rebar3], [{:certifi, "2.0.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "5.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, 17 | "idna": {:hex, :idna, "5.1.0", "d72b4effeb324ad5da3cab1767cb16b17939004e789d8c0ad5b70f3cea20c89a", [:rebar3], [{:unicode_util_compat, "0.3.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"}, 18 | "jalaali": {:hex, :jalaali, "0.2.1", "1b2ae18b1b8c8167405bc137faf0aed39f6764dfad0bf62176c9fff50b34d49c", [], [], "hexpm"}, 19 | "libcluster": {:hex, :libcluster, "2.2.3", "fdd7366532799b1a4d4a4cdd113c6d51a593783879a43f862ccfae92423f843f", [:mix], [{:poison, "~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, 20 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"}, 21 | "mime": {:hex, :mime, "1.1.0", "01c1d6f4083d8aa5c7b8c246ade95139620ef8effb009edde934e0ec3b28090a", [:mix], [], "hexpm"}, 22 | "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm"}, 23 | "ok": {:hex, :ok, "1.9.1", "e7310382536f365b974aaa0c07263b7e5a561a0f73c7c3f902bfcd7da1ec2ead", [], [], "hexpm"}, 24 | "plug": {:hex, :plug, "1.3.5", "7503bfcd7091df2a9761ef8cecea666d1f2cc454cbbaf0afa0b6e259203b7031", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1", [hex: :cowboy, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm"}, 25 | "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"}, 26 | "pollution": {:hex, :pollution, "0.9.2", "3f67542631071c99f807d2a8f9da799c07cd983c902f5357b9e1569c20a26e76", [:mix], [], "hexpm"}, 27 | "poolboy": {:hex, :poolboy, "1.5.1", "6b46163901cfd0a1b43d692657ed9d7e599853b3b21b95ae5ae0a777cf9b6ca8", [:rebar], [], "hexpm"}, 28 | "postgrex": {:hex, :postgrex, "0.13.3", "c277cfb2a9c5034d445a722494c13359e361d344ef6f25d604c2353185682bfc", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm"}, 29 | "quixir": {:hex, :quixir, "0.9.3", "f01c37386b9e1d0526f01a8734a6d7884af294a0ec360f05c24c7171d74632bd", [:mix], [{:pollution, "~> 0.9.2", [hex: :pollution, repo: "hexpm", optional: false]}], "hexpm"}, 30 | "remix": {:hex, :remix, "0.0.2", "f06115659d8ede8d725fae1708920ef73353a1b39efe6a232d2a38b1f2902109", [:mix], [], "hexpm"}, 31 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], [], "hexpm"}, 32 | "timex": {:hex, :timex, "3.1.24", "d198ae9783ac807721cca0c5535384ebdf99da4976be8cefb9665a9262a1e9e3", [:mix], [{:combine, "~> 0.7", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"}, 33 | "triplex": {:hex, :triplex, "0.9.0", "d602756e31a31325c747d5f2cbd08cd3a59c182bc9312cc0c51101c15a555292", [:mix], [{:ecto, "~> 2.1", [hex: :ecto, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.5", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, ">= 0.11.0", [hex: :postgrex, repo: "hexpm", optional: false]}], "hexpm"}, 34 | "tzdata": {:hex, :tzdata, "0.5.12", "1c17b68692c6ba5b6ab15db3d64cc8baa0f182043d5ae9d4b6d35d70af76f67b", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, 35 | "unicode_util_compat": {:hex, :unicode_util_compat, "0.3.1", "a1f612a7b512638634a603c8f401892afbf99b8ce93a45041f8aaca99cadb85e", [:rebar3], [], "hexpm"}, 36 | "uuid": {:hex, :uuid, "1.1.7", "007afd58273bc0bc7f849c3bdc763e2f8124e83b957e515368c498b641f7ab69", [:mix], [], "hexpm"}} 37 | -------------------------------------------------------------------------------- /priv/repo/migrations/20170628102407_add_ectobench_tables.exs: -------------------------------------------------------------------------------- 1 | defmodule EctoBench.Repo.Migrations.AddEctobenchTables do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:table1) do 6 | add :truth, :boolean 7 | end 8 | 9 | create table(:table2) do 10 | add :chars, :string, size: 32 11 | add :textual, :text 12 | add :count, :integer 13 | add :ref, references(:table1, on_delete: :delete_all) 14 | end 15 | 16 | create table(:table3) do 17 | add :c1, :string 18 | add :c2, :string 19 | add :c3, :string 20 | add :c4, :string 21 | add :c5, :string 22 | add :c6, :string 23 | add :c7, :string 24 | add :c8, :string 25 | add :c9, :string 26 | add :c10, :string 27 | add :c11, :string 28 | add :c12, :string 29 | add :c13, :string 30 | add :c14, :string 31 | add :c15, :string 32 | add :c16, :string 33 | add :c17, :string 34 | add :c18, :string 35 | add :c19, :string 36 | add :c20, :string 37 | add :c21, :string 38 | add :c22, :string 39 | add :c23, :string 40 | add :c24, :string 41 | add :c25, :string 42 | add :c26, :string 43 | add :c27, :string 44 | add :c28, :string 45 | add :c29, :string 46 | add :c30, :string 47 | add :c31, :string 48 | add :c32, :string 49 | add :c33, :string 50 | add :c34, :string 51 | add :c35, :string 52 | add :c36, :string 53 | add :c37, :string 54 | add :c38, :string 55 | add :c39, :string 56 | add :ref, references(:table1, on_delete: :delete_all) 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /priv/tenants/migrations/20170801141717_items.exs: -------------------------------------------------------------------------------- 1 | defmodule ExploringElixir.Repo.Tenants.Migrations.Items do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:items) do 6 | add :name, :text 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/tenants/migrations/20171001104601_dates_and_times.exs: -------------------------------------------------------------------------------- 1 | defmodule ExploringElixir.Repo.Tenants.Migrations.DatesAndTimes do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:dates_and_times) do 6 | add :a_date, :date 7 | add :a_time, :time 8 | add :with_tz, :timestamptz 9 | add :data, :text 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /priv/tenants/tenant_migrations/20170801085445_tenant_orders.exs: -------------------------------------------------------------------------------- 1 | defmodule EctoBench.Repo.Migrations.TenantOrders do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:orders) do 6 | add :name, :text 7 | 8 | timestamps() 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /priv/tenants/tenant_migrations/20170801141007_tenant_orderitems.exs: -------------------------------------------------------------------------------- 1 | defmodule ExploringElixir.Repo.Tenants.Migrations.TenantOrderitems do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:orderitems, primary_key: false) do 6 | add :item_id, :integer 7 | add :amount, :integer 8 | add :order_id, references(:orders, on_delete: :delete_all) 9 | 10 | timestamps() 11 | end 12 | 13 | create unique_index(:orderitems, [:order_id, :item_id]) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /priv/tenants/tenant_migrations/20170801141947_reference_shared_items.exs: -------------------------------------------------------------------------------- 1 | defmodule ExploringElixir.Repo.Tenants.Migrations.ReferenceSharedItems do 2 | use Ecto.Migration 3 | 4 | @fk_name "order_items_fkey" 5 | @repo ExploringElixir.Repo.Tenants 6 | 7 | def up do 8 | prefix = Ecto.Migration.prefix 9 | query = "alter table #{prefix}.orderitems 10 | add constraint #{@fk_name} foreign key (item_id) 11 | references public.items(id)" 12 | 13 | Ecto.Adapters.SQL.query!(@repo, query, []) 14 | end 15 | 16 | def down do 17 | %{prefix: prefix} = Process.get(:ecto_migration) 18 | query = "ALTER TABLE #{prefix}.orderitems 19 | DROP CONSTRAINT IF EXISTS #{@fk_name}" 20 | 21 | Ecto.Adapters.SQL.query!(@repo, query, []) 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /priv/tenants/tenant_migrations/20170804140705_constrain_item_counts.exs: -------------------------------------------------------------------------------- 1 | defmodule ExploringElixir.Repo.Tenants.Migrations.ConstrainItemCounts do 2 | use Ecto.Migration 3 | 4 | @repo ExploringElixir.Repo.Tenants 5 | 6 | def up do 7 | prefix = Ecto.Migration.prefix 8 | query = "CREATE OR REPLACE FUNCTION remove_empties_from_orders() 9 | RETURNS trigger AS 10 | $$ 11 | BEGIN 12 | DELETE FROM #{prefix}.orderitems WHERE id = NEW.id AND item_id = NEW.item_id; 13 | RETURN NULL; 14 | END 15 | $$ LANGUAGE plpgsql" 16 | 17 | Ecto.Adapters.SQL.query!(@repo, query, []) 18 | 19 | query = "CREATE TRIGGER remove_empty_items 20 | AFTER UPDATE OF amount 21 | ON #{prefix}.orderitems 22 | FOR EACH ROW 23 | WHEN (NEW.amount < 1) 24 | EXECUTE PROCEDURE remove_empties_from_orders()" 25 | 26 | Ecto.Adapters.SQL.query!(@repo, query, []) 27 | end 28 | 29 | def down do 30 | query = "DROP FUNCTION remove_empties_from_orders() CASCADE" 31 | Ecto.Adapters.SQL.query!(@repo, query, []) 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /src/all_atoms.erl: -------------------------------------------------------------------------------- 1 | -module(all_atoms). 2 | 3 | -export([all_atoms/0]). 4 | 5 | atom_by_number(N) -> 6 | binary_to_term(<<131,75,N:24>>). 7 | 8 | all_atoms() -> 9 | atoms_starting_at(0). 10 | 11 | atoms_starting_at(N) -> 12 | try atom_by_number(N) of 13 | Atom -> 14 | [Atom] ++ atoms_starting_at(N + 1) 15 | catch 16 | error:badarg -> 17 | [] 18 | end. 19 | -------------------------------------------------------------------------------- /test/collatz_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExploringElixir.CollatzTest do 2 | use ExUnit.Case 3 | use Quixir 4 | 5 | import ExploringElixir.Collatz 6 | 7 | @moduletag :collatz 8 | require Logger 9 | 10 | test "an empty list results in an empty list" do 11 | assert [] == step_count_for [] 12 | end 13 | 14 | test "the results are well formed" do 15 | assert [{1, 0}] = step_count_for 1 16 | # we assert the results of the Collatz module to be: 17 | # -> a list 18 | # -> in order from smallest to largest 19 | # -> of 2-tuples 20 | # -> which contain the input number and the result 21 | # -> the number of steps should be 0 for 1 22 | # and greater than 0 for any other positive int 23 | ptest input: choose(from: [ 24 | list(of: int(min: 2), min: 1), 25 | int(min: 2, must_have: [256, 512]), 26 | ], repeat_for: 10_000) 27 | do 28 | results = step_count_for input 29 | 30 | assert is_list(results) 31 | 32 | Enum.reduce results, 0, 33 | fn {input, _answer}, last_value -> 34 | assert last_value < input 35 | input 36 | end 37 | 38 | Enum.each results, fn {_input, answer} -> assert 0 < answer end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /test/exploring_elixir_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExploringElixirTest do 2 | use ExUnit.Case 3 | doctest ExploringElixir 4 | 5 | test "episode 1" do 6 | assert ExploringElixir.episode1() == :ok 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------