Slide 1

Slide 1 text

Playing with Elixir and Telling Stories Greg Vaughn Golf may be involved

Slide 2

Slide 2 text

• @gvaughn or @gregvaughn on GitHub, Twitter, Slack, ElixirForum, etc. • Elixiring since 2013 • Professionally since 2017 Who Dat

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

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!") )]) )])

Slide 6

Slide 6 text

# 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!") )) ))

Slide 7

Slide 7 text

# 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!") ) )

Slide 8

Slide 8 text

# 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

Slide 9

Slide 9 text

# 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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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)

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

I ❤ for comprehensions

<%= handler_info(@conn) %>

Keys for the conn Struct

<%= for key <- connection_keys(@conn) do %>

<%= key %>

<% end %>

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

I ❤ for comprehensions iex> for x <- [1, 2], y <- [2, 3], x != y, do: x * y [2, 3, 6] Generator Generator Filter

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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]

Slide 22

Slide 22 text

I ❤ for comprehensions •More Features •bitstring generators •into: Collectable •uniq: true •reduce option

Slide 23

Slide 23 text

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/

Slide 24

Slide 24 text

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(","))

Slide 25

Slide 25 text

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","

Slide 26

Slide 26 text

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 <>,do: c<97 &&c ||97+rem c-71-String.to_integer(n),26 103 chars!

Slide 27

Slide 27 text

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}

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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]) || []

Slide 30

Slide 30 text

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", [])

Slide 31

Slide 31 text

Thank You • Takeaways • PLAY WITH YOUR FOOD ELIXIR! • See multiple ways of writing the same code • Consider what those ways communicate to the reader • gvaughn@gmail.com • Twitter: @gregvaughn • Elixir Slack: @gregvaughn • Elixir Forum: gregvaughn • Questions?

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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])