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

Playing with Elixir and Telling Stories

Greg Vaughn
November 08, 2019

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
Tweet

More Decks by Greg Vaughn

Other Decks in Technology

Transcript

  1. • @gvaughn or @gregvaughn on GitHub, Twitter, Slack, ElixirForum, etc.

    • Elixiring since 2013 • Professionally since 2017 Who Dat
  2. 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!") )]) )])
  3. # 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!") )) ))
  4. # 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!") ) )
  5. # 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
  6. # 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
  7. 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
  8. 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
  9. 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)
  10. 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
  11. 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
  12. 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
  13. 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
  14. 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>
  15. I ❤ for comprehensions iex> for n <- 1 ..4,

    do: n * 2 [2, 4, 6, 8] Generator
  16. I ❤ for comprehensions iex> for x <- [1, 2],

    y <- [2, 3], x != y, do: x * y [2, 3, 6] Generator Generator Filter
  17. 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
  18. 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]
  19. 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/
  20. 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(","))
  21. 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","
  22. 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!
  23. 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}
  24. 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
  25. 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]) || []
  26. 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?
  27. 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
  28. 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])