Share
Embed
Playing with Elixir and Telling Stories
From The Big Elixir conference in New Orleans, Nov. 8, 2019
Children instinctively learn through playing, so let's play with our Elixir. We'll use golfing exercises to help prime our curiosity about how Elixir works. We'll compare and contrast different parts of the standard library, and we'll explore some syntactic cul-de-sacs. You'll see some code that blows your mind, and some that repulses you (perhaps both at the same time). You'll leave this talk with (hopefully) a smile, a practical understanding of parts of Elixir that will immediately help you, and some coding techniques that should *never* be put into production.
Greg Vaughn
November 08, 2019
Transcript
33 slides
-
• @gvaughn or @gregvaughn on GitHub, Twitter, Slack, ElixirForum, etc.
• Elixiring since 2013 • Professionally since 2017 Who Dat -
Semi-Sweet Elixir defmodule(:"Elixir.Hello", [{:do, ( def(big_elixir, [{:do, ( IO.puts("BEIGNETS! BEIGNETS!
BEIGNETS!") )}]) )}]) # keyword list sugar defmodule(:"Elixir.Hello", [do: ( def(big_elixir, [do: ( IO.puts("BEIGNETS! BEIGNETS! BEIGNETS!") )]) )]) -
# keyword list sugar defmodule(:"Elixir.Hello", [do: ( def(big_elixir, [do: (
IO.puts("BEIGNETS! BEIGNETS! BEIGNETS!") )]) )]) Semi-Sweet Elixir # Last parameter of function call can be keyword list # without [] sugar defmodule(:"Elixir.Hello", do: ( def(big_elixir, do: ( IO.puts("BEIGNETS! BEIGNETS! BEIGNETS!") )) )) -
# Last element of function call can be keyword list
# without [] sugar defmodule(:"Elixir.Hello", do: ( def(big_elixir, do: ( IO.puts("BEIGNETS! BEIGNETS! BEIGNETS!") )) )) Semi-Sweet Elixir # Uppercase bare-words means Elixir prefixed atom plus optional parens for function calls sugar defmodule Hello, do: ( def big_elixir, do: ( IO.puts("BEIGNETS! BEIGNETS! BEIGNETS!") ) ) -
# Uppercase bare-words means Elixir namespaced atom # plus optional
parens for function calls sugar defmodule Hello, do: ( def big_elixir, do: ( IO.puts("BEIGNETS! BEIGNETS! BEIGNETS!") ) ) Semi-Sweet Elixir # do/end (do blocks) can be used instead of a do with a # parenthetical expression # (note this also requires removal of the comma between params) defmodule Hello do def big_elixir do IO.puts("BEIGNETS! BEIGNETS! BEIGNETS!") end end -
# Before defmodule(:"Elixir.Hello", [{:do, ( def(big_elixir, [{:do, ( IO.puts("BEIGNETS! BEIGNETS!
BEIGNETS!") )}]) )}]) Semi-Sweet Elixir # After (keywords, atoms, parens, do blocks) defmodule Hello do def big_elixir do IO.puts("BEIGNETS! BEIGNETS! BEIGNETS!") end end -
Helpful Error Messages ** (SyntaxError) iex:164: unexpected token: do. In
case you wanted to write a "do" expression, you must either use do-blocks or separate the keyword argument with comma. For example, you should either write: if some_condition? do :this else :that end or the equivalent construct: if(some_condition?, do: :this, else: :that) where "some_condition?" is the first argument and the second argument is a keyword list -
Multiple Ways to "Say" It We want `product` from first
uncanceled line of the invoice invoice = %{id: "1a", lines: [ %{id: 1, cancelled: true, product: %{sku: "p1"}}, %{id: 2, cancelled: false, product: %{sku: "p2"}} ]} Approaches 1. Recursion 2. Enum.filter |> List.first |> Map.get 3. Enum.reduce_while 4. Enum.find 5. Enum.find_value 6. for comprehension -
1. Recursion We want `product` from first uncanceled line of
the invoice invoice = %{id: "1a", lines: [ %{id: 1, cancelled: true, product: %{sku: "p1"}}, %{id: 2, cancelled: false, product: %{sku: "p2"}} ]} def recursive(invoice), do: recursive(invoice.lines, nil) defp recursive(_, val) when val != nil, do: val defp recursive([%{cancelled: false} = match | _], _) do recursive(nil, match.product) end defp recursive([_ | t], _), do: recursive(t, nil) -
2. Filter Pipeline We want `product` from first uncanceled line
of the invoice invoice = %{id: "1a", lines: [ %{id: 1, cancelled: true, product: %{sku: "p1"}}, %{id: 2, cancelled: false, product: %{sku: "p2"}} ]} def filter_pipeline(invoice) do invoice.lines |> Enum.filter(fn line -> !line.cancelled end) |> List.first() |> Map.get(:product) end -
3. Enum.reduce_while We want `product` from first uncanceled line of
the invoice invoice = %{id: "1a", lines: [ %{id: 1, cancelled: true, product: %{sku: "p1"}}, %{id: 2, cancelled: false, product: %{sku: "p2"}} ]} def reduce_while(invoice) do Enum.reduce_while(invoice.lines, nil, fn line, acc -> if match?(%{cancelled: false}, line) do {:halt, line.product} else {:cont, acc} end end) end -
4,5. Enum.{find, find_value} We want `product` from first uncanceled line
of the invoice invoice = %{id: "1a", lines: [ %{id: 1, cancelled: true, product: %{sku: "p1"}}, %{id: 2, cancelled: false, product: %{sku: "p2"}} ]} def find(invoice) do invoice.lines |> Enum.find(fn l -> !l.cancelled end) |> Map.get(:product) end def find_value(invoice) do Enum.find_value(invoice.lines, &if(!&1.cancelled, do: &1.product)) end -
6. for comprehension We want `product` from first uncanceled line
of the invoice invoice = %{id: "1a", lines: [ %{id: 1, cancelled: true, product: %{sku: "p1"}}, %{id: 2, cancelled: false, product: %{sku: "p2"}} ]} def comprehension(invoice) do hd(for %{cancelled: false, product: p} <- invoice.lines, do: p) end -
I ❤ for comprehensions <div class="phx-hero"> <p><%= handler_info(@conn) %> </p>
<h3>Keys for the conn Struct </h3> <%= for key <- connection_keys(@conn) do %> <p><%= key %> </p> <% end %> </div> -
I ❤ for comprehensions iex> for n <- 1 ..4,
do: n * 2 [2, 4, 6, 8] Generator -
I ❤ for comprehensions iex> for x <- [1, 2],
y <- [2, 3], x != y, do: x * y [2, 3, 6] Generator Generator Filter -
I ❤ for comprehensions # Every combination of 2 elements
in list, order independent iex> list = [1, 2, 3, 4] iex> for x <- list, y <- list, x < y, do: [x, y] [[1, 2], [1, 3], [1, 4], [2, 3], [2, 4], [3, 4]] # What if the list items are not of the same type? iex> list = [:a, 3.14, -5, "Z"] iex> for x <- list, y <- list, x < y, do: [x, y] [[:a, "Z"], [3.14, :a], [3.14, "Z"], [-5, :a], [-5, 3.14], [-5, "Z"]] Total Term Ordering: number < atom < reference < function < port < pid < tuple < map < list < bitstring -
I ❤ for comprehensions # Want a keyword list of
{type, id} only for active entries pmt_options = [ stripe: [id: 1, active: true, key: "abc"], paypal: [id: 2, active: false], braintree: [id: 3, active: true, key: "def"], ] for {k, kw} <- pmt_options, {:active, true} <- kw do {k, kw[:id]} end [stripe: 1, braintree: 3] -
I ❤ for comprehensions •More Features •bitstring generators •into: Collectable
•uniq: true •reduce option -
Caesar Cipher Golf Write an elixir Caesar Cipher program that
will encrypt a message. Your program should prompt the user for input (IO.gets), and they will enter the following: number,and a message number - This is the number of characters to shift the Caesar Cipher e.g. 3 will shift the alphabet to xyzabcdefghijklmnopqrstuvw message - This is alphabetic, lowercase and whitespace is permitted (whitespace will simply map to whitespace of the encoded text). The output will be lowercase with whitespace. From: http://elixirgolf.com/articles/the-elixir-caesar-cipher/ -
Caesar Cipher Golf defmodule Caesar do import Stream @a String.codepoints
"abcdefghijklmnopqrstuvwxyz" def encode(n, p) do d = @a |> cycle |> drop(String.to_integer(n)) |> take(26) |> zip(@a) |> Map.new Enum.map_join(String.codepoints(p), fn c -> d[c] || c end) end end IO.puts apply(Caesar, :encode, IO.gets(" --enter your code here --\n") |> String.split(",")) -
Caesar Cipher Golf defmodule Caesar do import Stream @a String.codepoints
"abcdefghijklmnopqrstuvwxyz" def encode(n, p) do d = @a |> cycle |> drop(String.to_integer(n)) |> take(26) |> zip(@a) |> Map.new Enum.map_join(String.codepoints(p), fn c -> d[c] || c end) end end IO.puts apply(Caesar, :encode, IO.gets(" --enter your code here --\n") |> String.split(",")) import String; IO.puts apply fn n,p -> Enum.map_join( codepoints(p), &(for c <-0 ..25,into: %{},do: { <<97+rem(c+to_integer(n), 26) >>, <<97+c >>})[&1] ||&1) end,IO.gets("") |>split"," -
Caesar Cipher Golf import String; IO.puts apply fn n,p ->
Enum.map_join( codepoints(p), &(for c <-0 ..25,into: %{},do: { <<97+rem(c+to_integer(n), 26) >>, <<97+c >>})[&1] ||&1) end,IO.gets("") |>split"," [n,p]=IO.gets("") |>String.split",";IO.puts for <<c <-p >>,do: c<97 &&c ||97+rem c-71-String.to_integer(n),26 103 chars! -
Playful Tip: with # If you want to always get
{:ok, val} or # {:error, :keynotfound} from a Map, you could do it the # long way: def my_fetch(map, key) do case Map.fetch(map, key) do :error -> {:error, :keynotfound} value_pair -> value_pair end end # or use the `else` part of `with` for the "happy path" with :error <- Map.fetch(map, key), do: {:error, :keynotfound} -
Playful Tip: Map.new/2 Code of the form: x |> Enum.map(f)
|> Enum.into(%{}) Makes Map.new/2 cry Map.new(x, f) is both shorter and more intention-revealing -
Playful Tip: get_in date_transactions = if is_nil(statement[transaction.date]) do [] else
statement[transaction.date].transactions end Map.get(statement, transaction.date, %{transactions: []}).transactions get_in(statement, [transaction.date, :transactions]) || [] -
Playful Tip: Map.put_new/3 if !my_map["some_key"] do Map.put(my_map, "some_key", []) else
my_map end Map.put_new(my_map, "some_key", []) -
Thank You • Takeaways • PLAY WITH YOUR FOOD ELIXIR!
• See multiple ways of writing the same code • Consider what those ways communicate to the reader • [email protected] • Twitter: @gregvaughn • Elixir Slack: @gregvaughn • Elixir Forum: gregvaughn • Questions? -
Playful Tip: List.wrap/1 @status_mapping %{pending: 1, confirmed: 2, delivered: 3}
def status_query_str(status) when is_atom(status) do to_string(Map.get(@status_mapping, status)) end def status_query_str(statuses) when is_list(statuses) do Enum.map_join(",", &Map.get(@status_mapping, &1)) end def status_query_str(status_or_statuses) do status_or_statuses |> List.wrap() |> Enum.map_join(",", &Map.get(@status_mapping, &1)) end -
Playful Tip: Ecto # Long from(f in Foo, update: [inc:
[count: 1]], where: f.id == ^foo.id ) |> Repo.update_all([]) # Medium from(f in Foo, where: f.id == ^foo.id) |> Repo.update_all(inc: [count: 1]) # Short Repo.update_all(where(Foo, id: ^foo.id), inc: [count: 1])