Deep Dive into Elixir

Deep Dive into Elixir

Doing a deep dive into one of my favourite abstractions in Elixir.

7693460f070c2e00c78e56806d4fdfc0?s=128

Jonathan Yeong

September 04, 2020
Tweet

Transcript

  1. Deep Dive into Elixir Well really, just one abstraction @jonoyeong

    | ElixirConf 2020
  2. Excited to be here @jonoyeong | ElixirConf 2020

  3. Pipe Operator @jonoyeong | ElixirConf 2020

  4. @jonoyeong | ElixirConf 2020 Documentation

  5. “This operator introduces the expression on the left-hand side as

    the first argument to the function call on the right-hand side.” @jonoyeong | ElixirConf 2020
  6. @jonoyeong | ElixirConf 2020

  7. Ruby @jonoyeong | ElixirConf 2020

  8. Let’s see it in action @jonoyeong | ElixirConf 2020

  9. String.downcase(String.reverse("DLROW OLLEH”)) # "hello world” @jonoyeong | ElixirConf 2020 "DLROW

    OLLEH" |> String.reverse() |> String.downcase() # "hello world”
  10. String.downcase(String.reverse("DLROW OLLEH”)) # "hello world” @jonoyeong | ElixirConf 2020 "DLROW

    OLLEH" |> String.reverse() |> String.downcase() # "hello world”
  11. But how tho? @jonoyeong | ElixirConf 2020

  12. Pipeline ??? Nested Function @jonoyeong | ElixirConf 2020

  13. How is |> defined? Where does it specify the position

    of the argument? How does it nest/call functions in the pipeline? @jonoyeong | ElixirConf 2020
  14. Going down the rabbit hole @jonoyeong | ElixirConf 2020

  15. defmacro left |> right do [{h, _} | t] =

    Macro.unpipe({:|>, [], [left, right]}) fun = fn {x, pos}, acc -> Macro.pipe(acc, x, pos) end :lists.foldl(fun, h, t) end @jonoyeong | ElixirConf 2020
  16. defmacro left |> right do @jonoyeong | ElixirConf 2020

  17. defmacro left |> right do @jonoyeong | ElixirConf 2020 defmacro

  18. Quotes @jonoyeong | ElixirConf 2020

  19. Macros take & return quoted Expressions @jonoyeong | ElixirConf 2020

  20. quote do: "hello" # "hello" quote do: :world # :world

    quote do: 1+1 # {:+, [], [1, 1]} quote do: div(a, b) # {:div, [], [{:a, [], Elixir}, {:b, [], Elixir}]} @jonoyeong | ElixirConf 2020
  21. How do we inject code? @jonoyeong | ElixirConf 2020

  22. Unquoting @jonoyeong | ElixirConf 2020

  23. a = 10 b = 2 quote do div(a,b) end

    # {:div, [], [{:a, [], Elixir}, {:b, [], Elixir}]} quote do div(unquote(a), unquote(b)) end # {:div, [], [10, 2]} @jonoyeong | ElixirConf 2020 a = 10 b = 2 quote do div(a,b) end # {:div, [], [{:a, [], Elixir}, {:b, [], Elixir}]}
  24. a = 10 b = 2 quote do div(a,b) end

    # {:div, [], [{:a, [], Elixir}, {:b, [], Elixir}]} quote do div(unquote(a), unquote(b)) end # {:div, [], [10, 2]} @jonoyeong | ElixirConf 2020 quote do div(unquote(a), unquote(b)) end # {:div, [], [10, 2]}
  25. Evaluating a quote? @jonoyeong | ElixirConf 2020

  26. Code.eval_quoted(quote do div(unquote(a), unquote(b)) end) # {5, []} Code.eval_quoted(quote do

    a=1 end) # {1, [{{:a, Elixir}, 1}]} Code.eval_quoted(quote do div(a, b) end) # ** (CompileError) nofile:1: undefined function a/0 @jonoyeong | ElixirConf 2020 Code.eval_quoted(quote do div(unquote(a), unquote(b)) end) # {5, []} Code.eval_quoted(quote do a=1 end) # {1, [{{:a, Elixir}, 1}]}
  27. Code.eval_quoted(quote do div(unquote(a), unquote(b)) end) # {5, []} Code.eval_quoted(quote do

    a=1 end) # {1, [{{:a, Elixir}, 1}]} Code.eval_quoted(quote do div(a, b) end) # ** (CompileError) nofile:1: undefined function a/0 @jonoyeong | ElixirConf 2020 Code.eval_quoted(quote do div(a, b) end) # ** (CompileError) nofile:1: undefined function a/0
  28. The textual representation @jonoyeong | ElixirConf 2020

  29. Macro.to_string({:+, [], [1, 1]} # "1 + 1” Macro.to_string(quote do

    div(unquote(a), unquote(b)) end) # "div(10, 2)" @jonoyeong | ElixirConf 2020
  30. Macro Recap @jonoyeong | ElixirConf 2020

  31. Macros take quoted expressions @jonoyeong | ElixirConf 2020

  32. Macros return quoted expressions @jonoyeong | ElixirConf 2020

  33. Macros evaluated during compile time @jonoyeong | ElixirConf 2020

  34. @jonoyeong | ElixirConf 2020 defmacro left |> right do

  35. "DLROW OLLEH" |> String.reverse() |> String.downcase() # === Left ===

    # {:|>, [], # [ # "DLROW OLLEH", # {{:., [], [{:__aliases__, [], [:String]}, :reverse]}, # [], []} # ]} # === Right === # {{:., [], [{:__aliases__, [], [:String]}, :downcase]}, [], # []} @jonoyeong | ElixirConf 2020
  36. quote do: "DLROW OLLEH" |> String.reverse() |> String.downcase() {:|>, [],

    [ {:|>, [], [ "DLROW OLLEH", {{:., [], [{:__aliases__, [alias: false], [:String]}, :reverse]}, [], []} ]}, {{:., [], [{:__aliases__, [alias: false], [:String]}, :downcase]}, [], []} ]} @jonoyeong | ElixirConf 2020
  37. quote do: "DLROW OLLEH" |> String.reverse() |> String.downcase() {:|>, [],

    [ {:|>, [], [ "DLROW OLLEH", {{:., [], [{:__aliases__, [alias: false], [:String]}, :reverse]}, [], []} ]}, {{:., [], [{:__aliases__, [alias: false], [:String]}, :downcase]}, [], []} ]} @jonoyeong | ElixirConf 2020 {:|>, [], [ "DLROW OLLEH", {{:., [], [{:__aliases__, [alias: false], [:String]}, :reverse]}, [], []} ]},
  38. quote do: "DLROW OLLEH" |> String.reverse() |> String.downcase() {:|>, [],

    [ {:|>, [], [ "DLROW OLLEH", {{:., [], [{:__aliases__, [alias: false], [:String]}, :reverse]}, [], []} ]}, {{:., [], [{:__aliases__, [alias: false], [:String]}, :downcase]}, [], []} ]} @jonoyeong | ElixirConf 2020 {{:., [], [{:__aliases__, [alias: false], [:String]}, :downcase]}, [], []}
  39. How is |> defined? Where does it specify the position

    of the argument? How does it nest/call functions in the pipeline? @jonoyeong | ElixirConf 2020
  40. How is |> defined? Where does it specify the position

    of the argument? How does it nest/call functions in the pipeline? @jonoyeong | ElixirConf 2020 ✅
  41. defmacro left |> right do [{h, _} | t] =

    Macro.unpipe({:|>, [], [left, right]}) fun = fn {x, pos}, acc -> Macro.pipe(acc, x, pos) end :lists.foldl(fun, h, t) end @jonoyeong | ElixirConf 2020 [{h, _} | t] = Macro.unpipe({:|>, [], [left, right]}) fun = fn {x, pos}, acc -> Macro.pipe(acc, x, pos) end :lists.foldl(fun, h, t) end
  42. [{h, _} | t] = Macro.unpipe({:|>, [], [left, right]}) @jonoyeong

    | ElixirConf 2020
  43. [{h, _} | t] = Macro.unpipe({:|>, [], [left, right]}) @jonoyeong

    | ElixirConf 2020 Macro.unpipe({:|>, [], [left, right]})
  44. def unpipe(expr) do :lists.reverse(unpipe(expr, [])) end defp unpipe({:|>, _, [left,

    right]}, acc) do unpipe(right, unpipe(left, acc)) end defp unpipe(other, acc) do [{other, 0} | acc] end @jonoyeong | ElixirConf 2020
  45. def unpipe(expr) do :lists.reverse(unpipe(expr, [])) end defp unpipe({:|>, _, [left,

    right]}, acc) do unpipe(right, unpipe(left, acc)) end defp unpipe(other, acc) do [{other, 0} | acc] end @jonoyeong | ElixirConf 2020 defp unpipe(other, acc) do [{other, 0} | acc] end
  46. Macro.unpipe(quote do: 100 |> div(5) |> div(2)) # [{100, 0},

    {{:div, [], [5]}, 0}, {{:div, [], [2]}, 0}] @jonoyeong | ElixirConf 2020
  47. We don’t know how… Yet! @jonoyeong | ElixirConf 2020

  48. How is |> defined? Where does it specify the position

    of the argument? How does it nest/call functions in the pipeline? @jonoyeong | ElixirConf 2020 ✅
  49. How is |> defined? Where does it specify the position

    of the argument? How does it nest/call functions in the pipeline? @jonoyeong | ElixirConf 2020 ✅ ✅
  50. defmacro left |> right do [{h, _} | t] =

    Macro.unpipe({:|>, [], [left, right]}) fun = fn {x, pos}, acc -> Macro.pipe(acc, x, pos) end :lists.foldl(fun, h, t) end @jonoyeong | ElixirConf 2020 fun = fn {x, pos}, acc -> Macro.pipe(acc, x, pos) end :lists.foldl(fun, h, t) end
  51. fun = fn {x, pos}, acc -> Macro.pipe(acc, x, pos)

    end @jonoyeong | ElixirConf 2020
  52. def pipe(expr, {op, line, args} = op_args, integer) when is_list(args)

    do cond do is_atom(op) and Identifier.unary_op(op) != :error -> raise ArgumentError, "cannot pipe #{to_string(expr)} into #{to_string(op_args)}, " <> "the #{to_string(op)} operator can only take one argument" is_atom(op) and Identifier.binary_op(op) != :error -> raise ArgumentError, "cannot pipe #{to_string(expr)} into #{to_string(op_args)}, " <> "the #{to_string(op)} operator can only take two arguments" true -> {op, line, List.insert_at(args, integer, expr)} end end @jonoyeong | ElixirConf 2020
  53. def pipe(expr, {op, line, args} = op_args, integer) when is_list(args)

    do cond do is_atom(op) and Identifier.unary_op(op) != :error -> raise ArgumentError, "cannot pipe #{to_string(expr)} into #{to_string(op_args)}, " <> "the #{to_string(op)} operator can only take one argument" is_atom(op) and Identifier.binary_op(op) != :error -> raise ArgumentError, "cannot pipe #{to_string(expr)} into #{to_string(op_args)}, " <> "the #{to_string(op)} operator can only take two arguments" true -> {op, line, List.insert_at(args, integer, expr)} end end @jonoyeong | ElixirConf 2020 true -> {op, line, List.insert_at(args, integer, expr)}
  54. Macro.pipe(100, {:div, [], [5]}, 0) # {:div, [], [100, 5]}

    Macro.pipe({:div, [], [100, 5]}, {:div, [], [2]}, 0) # {:div, [], [{:div, [], [100, 5]}, 2]} @jonoyeong | ElixirConf 2020
  55. I can feel it in my fingers @jonoyeong | ElixirConf

    2020
  56. :lists.foldl(fun, h, t) @jonoyeong | ElixirConf 2020

  57. Atom? What are you doing here? @jonoyeong | ElixirConf 2020

  58. Oh this is Erlang @jonoyeong | ElixirConf 2020

  59. def left - right do :erlang.-(left, right) end def flatten(list)

    do :lists.flatten(list) end def mkdir(path) do :file.make_dir(IO.chardata_to_string(path)) end @jonoyeong | ElixirConf 2020
  60. i :lists # Term # :lists # Data type #

    Atom # Description # Call :lists.module_info() to access metadata. # Raw representation # :"lists" # Reference modules # Module, Atom # Implemented protocols # IEx.Info, Inspect, List.Chars, String.Chars @jonoyeong | ElixirConf 2020
  61. i :hello # Term # :hello # Data type #

    Atom # Reference modules # Atom # Implemented protocols # IEx.Info, Inspect, List.Chars, String.Chars @jonoyeong | ElixirConf 2020
  62. Erlang/OTP @jonoyeong | ElixirConf 2020

  63. Erlang/OTP @jonoyeong | ElixirConf 2020

  64. “[Open Telecom Platform] is a set of Erlang libraries and

    design principles providing middleware to develop systems” Erlang.com @jonoyeong | ElixirConf 2020
  65. @jonoyeong | ElixirConf 2020

  66. Let’s get back to it @jonoyeong | ElixirConf 2020

  67. foldl(Fun, Acc0, List) -> Acc1 lists:foldl(fun(X, Sum) -> X +

    Sum end, 0, [1,2,3,4,5]). % 15 @jonoyeong | ElixirConf 2020
  68. :lists.foldl(fun, h, t) @jonoyeong | ElixirConf 2020 (fun, h, t)

  69. fun = fn {x, pos}, acc -> Macro.pipe(acc, x, pos)

    end @jonoyeong | ElixirConf 2020
  70. [{h, _} | t] = Macro.unpipe({:|>, [], [left, right]}) @jonoyeong

    | ElixirConf 2020 [{h, _} | t]
  71. All together now @jonoyeong | ElixirConf 2020

  72. @jonoyeong | ElixirConf 2020 100 |> div(5) |> div(2) #

    [{100, 0}, {{:div, [], [5]}, 0}, {{:div, [], [2]}, 0}] fun = fn {x, pos}, acc -> Macro.pipe(acc, x, pos) end :lists.foldl(fun, 100, [{{:div, [], [5]}, 0}, {{:div, [], [2]}, 0}]) # {:div, [], [{:div, [], [100, 5]}, 2]} Macro.to_string({:div, [], [{:div, [], [100, 5]}, 2]}) # "div(div(100, 5), 2)"
  73. @jonoyeong | ElixirConf 2020 100 |> div(5) |> div(2) #

    [{100, 0}, {{:div, [], [5]}, 0}, {{:div, [], [2]}, 0}] fun = fn {x, pos}, acc -> Macro.pipe(acc, x, pos) end :lists.foldl(fun, 100, [{{:div, [], [5]}, 0}, {{:div, [], [2]}, 0}]) # {:div, [], [{:div, [], [100, 5]}, 2]} Macro.to_string({:div, [], [{:div, [], [100, 5]}, 2]}) # "div(div(100, 5), 2)"
  74. @jonoyeong | ElixirConf 2020 100 |> div(5) |> div(2) #

    [{100, 0}, {{:div, [], [5]}, 0}, {{:div, [], [2]}, 0}] fun = fn {x, pos}, acc -> Macro.pipe(acc, x, pos) end :lists.foldl(fun, 100, [{{:div, [], [5]}, 0}, {{:div, [], [2]}, 0}]) # {:div, [], [{:div, [], [100, 5]}, 2]} Macro.to_string({:div, [], [{:div, [], [100, 5]}, 2]}) # "div(div(100, 5), 2)"
  75. @jonoyeong | ElixirConf 2020 100 |> div(5) |> div(2) #

    [{100, 0}, {{:div, [], [5]}, 0}, {{:div, [], [2]}, 0}] fun = fn {x, pos}, acc -> Macro.pipe(acc, x, pos) end :lists.foldl(fun, 100, [{{:div, [], [5]}, 0}, {{:div, [], [2]}, 0}]) # {:div, [], [{:div, [], [100, 5]}, 2]} Macro.to_string({:div, [], [{:div, [], [100, 5]}, 2]}) # "div(div(100, 5), 2)"
  76. How is |> defined? Where does it specify the position

    of the argument? How does it nest/call functions in the pipeline? @jonoyeong | ElixirConf 2020 ✅ ✅
  77. How is |> defined? Where does it specify the position

    of the argument? How does it nest/call functions in the pipeline? @jonoyeong | ElixirConf 2020 ✅ ✅ ✅
  78. Why did I do this again? @jonoyeong | ElixirConf 2020

  79. Get a better understanding @jonoyeong | ElixirConf 2020

  80. Dispel the magic @jonoyeong | ElixirConf 2020

  81. @jonoyeong | ElixirConf 2020 2 |> Integer.to_string(10, ...) # “1010”

    defp unpipe(ast, acc, %Macro.Env{line: line, file: file}) do case find_pos(ast) do {:ok, new_ast, pos} -> [{new_ast, pos} | acc] {:error, {:already_found, _, _}} -> raise CompileError, file: file, line: line, description: "Doubled placeholder in #{Macro.to_string(ast)}" end end Magritte.ex defp unpipe(ast, acc, %Macro.Env{line: line, file: file}) do case find_pos(ast) do {:ok, new_ast, pos} -> [{new_ast, pos} | acc] end end 2 |> Integer.to_string(10, ...) # “1010”
  82. Scratch that itch @jonoyeong | ElixirConf 2020

  83. Thank you! @jonoyeong