├── .gitignore ├── README.md ├── config └── config.exs ├── lib └── pattern_tap.ex ├── mix.exs ├── mix.lock └── test ├── pattern_tap_test.exs └── test_helper.exs /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /deps 3 | erl_crash.dump 4 | *.ez 5 | doc 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | PatternTap 2 | ========== 3 | 4 | ##### The pipe operator `|>` is an awesome feature of Elixir. Keep using it. 5 | 6 | But when your result cannot be directly input into the next function, you have to stop, pattern match out the value you want and start piping again! 7 | 8 | It is a common pattern to return data like `{:ok, result}` or `{:error, reason}`. When you want to handle both cases, something like [elixir-pipes](https://github.com/batate/elixir-pipes) may be a better use case for you. But otherwise, for simple destructuring of data and returning it in one line (or to just **let it fail**) you can `use PatternTap`! 9 | 10 | #### Not fun way 11 | 12 | ```elixir 13 | defmodule Foo do 14 | def get_stuff(input) do 15 | {:ok, intermediate_result} = input 16 | |> Enum.map(&(to_string(&1))) 17 | |> Foo.HardWorker.work 18 | {:ok, result} = intermediate_result 19 | |> Enum.map(&(Foo.IntermediateResult.handle(&1))) 20 | result 21 | end 22 | end 23 | ``` 24 | 25 | Anytime where the object you want requires pattern matching but you want to either return on one line or continue piping, you can `use PatternTap`! 26 | 27 | ```elixir 28 | def my_function do 29 | {:ok, result} = something |> something_else 30 | result 31 | end 32 | ``` 33 | 34 | #### Pattern Tap 35 | 36 | Heres the above example using `PatternTap` 37 | 38 | ```elixir 39 | defmodule Foo do 40 | def get_stuff(input) do 41 | input 42 | |> Enum.map(&(to_string(&1))) 43 | |> Foo.HardWorker.work 44 | |> tap({:ok, r1} ~> r1) # tap({:ok, r1}, r1) is also a supported format 45 | |> Enum.map(&(Foo.IntermediateResult.handle(&1))) 46 | |> tap({:ok, r2} ~> r2) # tap({:ok, r2}, r2) is also a supported format 47 | end 48 | end 49 | ``` 50 | 51 | And the second example 52 | 53 | ```elixir 54 | # tap({:ok, result}, result) also supported 55 | def my_function do 56 | something |> something_else |> tap({:ok, result} ~> result) 57 | end 58 | ``` 59 | 60 | ### Variable Leakage 61 | 62 | **PatternTap** makes use of `case` in order to prevent leaking the variables you create. So after using `tap`, you won't have access to the patterns you create. This means if you bind more than one variable in your pattern, you won't have access to it. 63 | 64 | Take the following example: 65 | 66 | ```elixir 67 | my_data = {:data1, :data2} |> tap({d1, d2} ~> d1) 68 | d2 # => ** (CompileError) ...: function d2/0 undefined 69 | ``` 70 | 71 | Instead you can use `destruct` to destructure the data you want. This does the same thing but with the side effect of keeping the binding you created in your patterns. 72 | 73 | ```elixir 74 | {:data1, :data2} |> destruct({d1, d2} ~> d1) |> some_func(d2) 75 | ``` 76 | 77 | To simply save a partial result for later use, consider using `leak/2`: 78 | 79 | ```elixir 80 | iex> [:data1, :data2] |> Enum.reverse |> leak(reversed) |> hd 81 | :data2 82 | iex> reversed 83 | [:data2, :data1] 84 | ``` 85 | 86 | Note that `|> leak(variable_name)` is equivalent to `|> destruct(variable_name ~> variable_name)`. 87 | 88 | 89 | ### Unmatched results 90 | 91 | #### Tap 92 | 93 | Because `tap/3` uses `case` you will get a `CaseClauseError` with the data which did not match in the error report. 94 | 95 | ```elixir 96 | {:error, "reason"} |> tap({:ok, result} ~> result) 97 | # ** (CaseClauseError) no case clause matching: {:error, "reason"} 98 | ``` 99 | 100 | 101 | #### Destruct 102 | 103 | Since `destruct/3` and `leak/2` use `=` you will instead get a `MatchError` with the data which did not match in the error report. 104 | 105 | ```elixir 106 | {:error, "reason"} |> destruct({:ok, result} ~> result) 107 | # ** (MatchError) no match of right hand side value: {:error, "reason"} 108 | ``` 109 | 110 | #### Leak 111 | 112 | `leak(data, variable)` expands to `variable = data`, so in a simple use case, 113 | leak can never fail, though it may override an existing variable: 114 | 115 | ```elixir 116 | iex> old_var = 5 117 | iex> [1, 2] |> leak(old_var) |> length 118 | 2 119 | iex> old_var 120 | [1, 2] 121 | ``` 122 | 123 | Because `leak(data, variable)` expands to `variable = data`, we can do all of our 124 | favorite Elixir pattern-matching tricks here, e.g.: 125 | 126 | ```elixir 127 | iex> %{a: 1, b: 2} |> leak(%{b: b}) 128 | %{a: 1, b: 2} 129 | iex> b 130 | 2 131 | ``` 132 | 133 | This flexibility allows `leak` to fail just like `destruct`: 134 | 135 | ```elixir 136 | iex> %{a: 1, b: 2} |> leak(%{c: c}) 137 | ** (MatchError) no match of right hand side value: %{a: 1, b: 2} 138 | ``` 139 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for third- 9 | # party users, it should be done in your mix.exs file. 10 | 11 | # Sample configuration: 12 | # 13 | # config :logger, :console, 14 | # level: :info, 15 | # format: "$date $time [$level] $metadata$message\n", 16 | # metadata: [:user_id] 17 | 18 | # It is also possible to import configuration files, relative to this 19 | # directory. For example, you can emulate configuration per environment 20 | # by uncommenting the line below and defining dev.exs, test.exs and such. 21 | # Configuration from the imported file will override the ones defined 22 | # here (which is why it is important to import them last). 23 | # 24 | # import_config "#{Mix.env}.exs" 25 | -------------------------------------------------------------------------------- /lib/pattern_tap.ex: -------------------------------------------------------------------------------- 1 | defmodule PatternTap do 2 | @moduledoc """ 3 | The pipe operator `|>` is an awesome feature of Elixir. Keep using it. 4 | 5 | But when your result cannot be directly input into the next function, you have to stop, pattern match out the value you want and start piping again! 6 | 7 | It is a common pattern to return data like `{:ok, result}` or `{:error, reason}`. When you want to handle both cases, something like [elixir-pipes](https://github.com/batate/elixir-pipes) may be a better use case for you. But otherwise, for simple destructuring of data and returning it in one line (or to just **let it fail**) you can `use PatternTap`! 8 | 9 | #### Not fun way 10 | 11 | ```elixir 12 | defmodule Foo do 13 | def get_stuff(input) do 14 | {:ok, intermediate_result} = input 15 | |> Enum.map(&(to_string(&1))) 16 | |> Foo.HardWorker.work 17 | {:ok, result} = intermediate_result 18 | |> Enum.map(&(Foo.IntermediateResult.handle(&1))) 19 | result 20 | end 21 | end 22 | ``` 23 | 24 | Anytime where the object you want requires pattern matching but you want to either return on one line or continue piping, you can `use PatternTap`! 25 | 26 | ```elixir 27 | def my_function do 28 | {:ok, result} = something |> something_else 29 | result 30 | end 31 | ``` 32 | 33 | #### Pattern Tap 34 | 35 | Heres the above example using `PatternTap` 36 | 37 | ```elixir 38 | defmodule Foo do 39 | use PatternTap 40 | 41 | def get_stuff(input) do 42 | input 43 | |> Enum.map(&(to_string(&1))) 44 | |> Foo.HardWorker.work 45 | |> tap({:ok, r1} ~> r1) # tap({:ok, r1}, r1) is also a supported format 46 | |> Enum.map(&(Foo.IntermediateResult.handle(&1))) 47 | |> tap({:ok, r2} ~> r2) # tap({:ok, r2}, r2) is also a supported format 48 | end 49 | end 50 | ``` 51 | 52 | And the second example 53 | 54 | ```elixir 55 | # tap({:ok, result}, result) also supported 56 | def my_function do 57 | something |> something_else |> tap({:ok, result} ~> result) 58 | end 59 | ``` 60 | """ 61 | 62 | defmacro __using__(_) do 63 | quote do 64 | import PatternTap 65 | end 66 | end 67 | 68 | @doc """ 69 | Use within pipes to pull out data inside and continue piping. 70 | 71 | Example: 72 | 73 | iex> use PatternTap 74 | iex> [1,2,3,4] |> tap([_a, _b | c] ~> c) |> inspect() 75 | "[3, 4]" 76 | """ 77 | defmacro tap(data, pattern, return_var) do 78 | quote do 79 | case unquote(data) do 80 | unquote(pattern) -> unquote(return_var) 81 | end 82 | end 83 | end 84 | 85 | @doc false 86 | defmacro tap(data, {:~>, _, [pattern, var]}) do 87 | quote do 88 | tap(unquote(data), unquote(pattern), unquote(var)) 89 | end 90 | end 91 | 92 | @doc """ 93 | `tap/3` will not leak variable scope, and so any variables created within it are sure 94 | not to accidentally harm outside bindings by replacing them with values you didn't intend to. 95 | 96 | For that reason, it can only return one value, which means other variables in the binding will 97 | go unused (and warn you about it). `destruct/3` will act as `tap/3` however leak the variable 98 | scope outside, allowing you to return one value and then use another. 99 | 100 | Example: (doc test does not like this for some reason, but it works I promise...) 101 | 102 | ``` 103 | use PatternTap 104 | [1,2,3,4] |> destruct([a, b | c] ~> c) |> Enum.concat([a, b]) 105 | [3,4,1,2] 106 | ``` 107 | """ 108 | defmacro destruct(data, pattern, return_var) do 109 | quote do 110 | unquote(pattern) = unquote(data) 111 | unquote(return_var) 112 | end 113 | end 114 | 115 | @doc false 116 | defmacro destruct(data, {:~>, _, [pattern, var]}) do 117 | quote do 118 | destruct(unquote(data), unquote(pattern), unquote(var)) 119 | end 120 | end 121 | 122 | @doc """ 123 | Leaks a pipelined value into the surrounding context as a new variable. 124 | 125 | ## Examples: 126 | 127 | iex> "hey" |> String.upcase |> leak(uppercase) |> String.to_atom 128 | :HEY 129 | iex> uppercase 130 | "HEY" 131 | iex> {:ok, "the result"} |> leak({:ok, result}) 132 | {:ok, "the result"} 133 | iex> result 134 | "the result" 135 | """ 136 | defmacro leak(data, var) do 137 | quote do 138 | unquote(var) = unquote(data) 139 | end 140 | end 141 | end 142 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule PatternTap.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :pattern_tap, 6 | version: "0.4.0", 7 | elixir: "~> 1.0", 8 | description: """ 9 | Macro for tapping into a pattern match while using the pipe operator 10 | """, 11 | package: [ 12 | maintainers: ["Matt Widmann"], 13 | licenses: ["MIT"], 14 | links: %{:"Github" => "https://github.com/mgwidmann/elixir-pattern_tap"} 15 | ], 16 | deps: deps()] 17 | end 18 | 19 | # Configuration for the OTP application 20 | # 21 | # Type `mix help compile.app` for more information 22 | def application do 23 | [applications: [:logger]] 24 | end 25 | 26 | # Dependencies can be Hex packages: 27 | # 28 | # {:mydep, "~> 0.3.0"} 29 | # 30 | # Or git/path repositories: 31 | # 32 | # {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"} 33 | # 34 | # Type `mix help deps` for more examples and options 35 | defp deps do 36 | [ 37 | {:ex_doc, ">= 0.0.0", only: :dev} 38 | ] 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{"earmark": {:hex, :earmark, "1.2.2", "f718159d6b65068e8daeef709ccddae5f7fdc770707d82e7d126f584cd925b74", [:mix], [], "hexpm"}, 2 | "ex_doc": {:hex, :ex_doc, "0.16.2", "3b3e210ebcd85a7c76b4e73f85c5640c011d2a0b2f06dcdf5acdb2ae904e5084", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}], "hexpm"}} 3 | -------------------------------------------------------------------------------- /test/pattern_tap_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PatternTapTest do 2 | use ExUnit.Case 3 | use PatternTap 4 | doctest PatternTap 5 | test "can do simple pattern matching" do 6 | assert tap(:ok, :ok, nil) == nil 7 | assert tap(:ok, :ok ~> nil) == nil 8 | assert destruct(:ok, :ok, nil) == nil 9 | assert destruct(:ok, :ok ~> nil) == nil 10 | end 11 | 12 | test "can do list pattern matching" do 13 | assert tap([:a], [a], a) == :a 14 | assert tap([:a], [a] ~> a) == :a 15 | assert destruct([:a], [a], a) == :a 16 | assert destruct([:a], [a] ~> a) == :a 17 | end 18 | 19 | test "variables are not available after" do 20 | tap([:foo], [f], f) 21 | assert binding()[:f] == nil 22 | tap([:foo], [f] ~> f) 23 | assert binding()[:f] == nil 24 | end 25 | 26 | test "can do tuple pattern matching" do 27 | assert tap({:b}, {b}, b) == :b 28 | assert tap({:b}, {b} ~> b) == :b 29 | assert destruct({:b}, {b}, b) == :b 30 | assert destruct({:b}, {b} ~> b) == :b 31 | end 32 | 33 | @data [:a, :b, :c] 34 | test "can match with the |> operator" do 35 | assert @data |> Enum.map(&(to_string(&1))) |> tap([_, b, _], b) == "b" 36 | assert @data |> Enum.map(&(to_string(&1))) |> tap([_, b, _] ~> b) == "b" 37 | assert @data |> Enum.map(&(to_string(&1))) |> destruct([_, b, _], b) == "b" 38 | assert @data |> Enum.map(&(to_string(&1))) |> destruct([_, b, _] ~> b) == "b" 39 | end 40 | 41 | @data [key: :val, key2: :val2] 42 | test "can match |> with keyword lists" do 43 | assert @data |> tap([_, {:key2, v}], v) == :val2 44 | assert @data |> tap([_, {:key2, v}] ~> v) == :val2 45 | assert @data |> destruct([_, {:key2, v}], v) == :val2 46 | assert @data |> destruct([_, {:key2, v}] ~> v) == :val2 47 | end 48 | 49 | test "can match typical {:ok, result}" do 50 | assert {:ok, 1} |> tap({:ok, result}, result) == 1 51 | assert {:ok, 1} |> tap({:ok, result} ~> result) == 1 52 | assert {:ok, 1} |> destruct({:ok, result}, result) == 1 53 | assert {:ok, 1} |> destruct({:ok, result} ~> result) == 1 54 | end 55 | 56 | test "failure matches result in the correct error message" do 57 | assert_raise CaseClauseError, "no case clause matching: {:error, \"reason\"}", fn -> 58 | {:error, "reason"} |> tap({:ok, result}, result) 59 | end 60 | assert_raise CaseClauseError, "no case clause matching: {:error, \"reason\"}", fn -> 61 | {:error, "reason"} |> tap({:ok, result} ~> result) 62 | end 63 | assert_raise MatchError, "no match of right hand side value: {:error, \"reason\"}", fn -> 64 | {:error, "reason"} |> destruct({:ok, result}, result) 65 | end 66 | assert_raise MatchError, "no match of right hand side value: {:error, \"reason\"}", fn -> 67 | {:error, "reason"} |> destruct({:ok, result} ~> result) 68 | end 69 | end 70 | 71 | test "destruct keeps variables around" do 72 | destruct({:a, :b}, {a, _b}, a) 73 | destruct({:a, :b}, {a, b} ~> a) 74 | assert a == :a 75 | assert b == :b 76 | end 77 | 78 | describe "leak" do 79 | test "creates a new variable" do 80 | [1, 2] |> Enum.reverse |> leak(backwards) 81 | assert backwards == [2, 1] 82 | end 83 | 84 | test "supports internal pattern matches" do 85 | [1, 2] |> Enum.reverse |> leak([h|t]) 86 | assert h == 2 87 | assert t == [1] 88 | end 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------