├── .formatter.exs ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── HOW_IT_WORKS.md ├── LICENSE ├── README.md ├── config └── config.exs ├── lib ├── expat.ex └── expat │ └── macro.ex ├── mix.exs ├── mix.lock └── test ├── ast_test.exs ├── expat_def_test.exs ├── expat_defmodule_test.exs ├── expat_either_test.exs ├── expat_expansion_test.exs ├── expat_maybe_test.exs ├── expat_named_test.exs ├── expat_nat_test.exs ├── expat_positional_test.exs ├── expat_result_test.exs ├── expat_test.exs ├── expat_union_test.exs ├── foo_test.exs ├── readme_test.exs ├── support ├── my_patterns.ex ├── named.ex └── readme.ex └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"], 4 | locals_without_parens: [expat: 1, expat: 2, defpat: 1, defpatp: 1], 5 | export: [ 6 | locals_without_parens: [expat: 1, expat: 2, defpat: 1, defpatp: 1] 7 | ] 8 | ] 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | expat-*.tar 24 | 25 | .DS* 26 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: elixir 2 | env: 3 | - MIX_ENV=test 4 | before_script: 5 | - mix deps.compile 6 | script: 7 | - mix test 8 | 9 | matrix: 10 | include: 11 | - elixir: 1.6.1 12 | otp_release: 20.0 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v1.0.5 4 | 5 | ##### Enhancements 6 | 7 | * Add `Result` type example in tests. 8 | 9 | ##### Bug Fixes 10 | 11 | * Binding non guarded variables to expressions just replaces those vars. 12 | 13 | ## v1.0.4 14 | 15 | ##### Enhancements 16 | 17 | * Add union patterns with examples for `nats`, `maybe`, `either`, struct. 18 | * Report lines at expansion point not as pattern definition line. 19 | 20 | ##### Bug Fixes 21 | 22 | * nil values were not able to be bound. 23 | * vars should be made unique on each expansion. 24 | * guarded vars were being counted first 25 | 26 | ## v1.0.3 27 | 28 | ##### Enhancements 29 | 30 | * Add HOW_IT_WORKS.md to add some details on how guards are expanded. 31 | * Add bang `!` macro for named pattern that can be used as constructor. 32 | 33 | ##### Bug Fixes 34 | 35 | * Pattern variables starting with `_` were able to be bound. 36 | 37 | ## v1.0.2 38 | 39 | ##### Enhancements 40 | 41 | * Allow positional arguments in adition to named bindings. 42 | * Document generated macros 43 | * Support `expat with` 44 | 45 | 46 | ## v1.0.1 47 | 48 | ##### Enhancements 49 | 50 | * Added a usage guide in README.md 51 | 52 | ##### Bug Fixes 53 | 54 | * Ensure pattern expassion happens in all arguments of a `def` 55 | 56 | 57 | ## v1.0.0 58 | 59 | ##### Enhancements 60 | 61 | * Major rewrite to let `defpat` support guards being defined on it 62 | * Introduced `expat` to expand patterns inside Elixir expressions 63 | 64 | ##### Deprecations 65 | 66 | * Removed the `...` syntax that introduced all pattern variables into scope. 67 | Now you must be explicit on what variables you want to bind. 68 | 69 | * Removed passing `_` to ignore all variables in pattern, now you must use 70 | the macro generated with zero arity, or bind all vars to `_` 71 | 72 | -------------------------------------------------------------------------------- /HOW_IT_WORKS.md: -------------------------------------------------------------------------------- 1 | # How Pattern Expansion works in Expat. 2 | 3 | When you define `defpat foo(1)`, the ast inside foo (here `1`) is the code that will be placed at call-site. eg. `foo() = 1` expands to `1 = 1`. 4 | 5 | That means it's actually possible to place *any* elixir code in there, and the `foo` macro will just expand it when called. 6 | 7 | Now, since `expat`'s purpose in life is to help with pattern matching, 8 | the ast inside foo is treated specially in the following cases: 9 | 10 | - if it contains a variable like `defpat foo(x)` then x is _boundable_ by the caller of foo. The caller can bind it by name, like: 11 | `foo(x: 1) = 1` => `1 = 1` 12 | If x is not bound by the caller, like `foo()`, x will be replaced with an `_` , so `foo() = 1` is `_ = 1` 13 | 14 | - if it contains a guard like `defpat bar(y) when y > 10` then, the code of the guard will also be expanded, for example: 15 | `bar(y: 2)` will expand to `y = 2 when y > 10`. 16 | 17 | Note however that since we have a guard to check, and `y` is being used in it, the variable `y` is preserved in expansion,, however this `y` is higenic (elixir's counter distingishes it from others) and will not bind any other y in your own scope. 18 | 19 | To bind in your scope you do something like 20 | `bar(y: u)` expands to `u = y when y > 10` and `u` is a variable you provided from your scope. 21 | 22 | So, you could bind bar's `y` with any expression, even other pattern expansions (just regular function calls) 23 | 24 | `bar(y: z = foo(x: 20))` will expand to `y = z = 20 when y > 20` 25 | this will also work: `bar(z = foo(20))` since expat now supports positional arguments (variables get bound in the order they appear on the pattern) 26 | 27 | - If it contains a nested pattern expansion. For example, if you had 28 | `defpat t2({x, y}) when x > y` and later did 29 | `defpat teens(t2(bar(a), bar(b))) when a < 20 and b < 20` 30 | 31 | Then `teens` has two _bindable_ names, `:a` and `:b` and it will get expanded into a pattern like: 32 | `{a, b} when a < 20 and b < 20 and a > b and a > 10 and b > 10` 33 | That means inner guards get propagated into the calling expansion. 34 | 35 | 36 | Now since `defpat` just captures the code inside the pattern for expanding it later, `defpat named(%{"name" => name})` allows you to expand `named` anywhere you can place a pattern in elixir, like on the left hand side of `=` 37 | 38 | `named(x) = %{"name" => "vic"}` will expand to 39 | `%{"name" => x} = %{"name" => "vic"}`, that's why you can use it on a function definition like: 40 | 41 | `def index(conn, params = named(name)), do: ...` 42 | 43 | However, for those containing guards, 44 | 45 | `def lalala(teens(m, n))` would by itself expand into: 46 | `def lalala({m, n} when m < 20 and n < 20 and m > n and m > 10 and n > 10)` 47 | 48 | *Of course* having a `when` in that context fails. 49 | as it would do if you try: 50 | 51 | ``` 52 | iex(14)> {m, n} when m > n and m > 10 and n > 10 = {30, 20} 53 | ** (CompileError) iex:14: undefined function when/2 54 | ``` 55 | So, having guards was what introduced the `expat def` syntax: 56 | 57 | `expat def lalala(teens(m, n))` expands correctly into: 58 | `def lalala({m, n}) when m < 20 and n < 20 and m > n and m > 10 and n > 10`. 59 | 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2018 Victor Borja 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Expat - Reusable, composable patterns in Elixir. 2 | 3 | [![Travis](https://img.shields.io/travis/vic/expat.svg)](https://travis-ci.org/vic/expat) 4 | [![Hex.pm](https://img.shields.io/hexpm/v/expat.svg?style=flat-square)](https://hexdocs.pm/expat) 5 | 6 | ## About 7 | 8 | Expat is a library for creating composable pattern matchers. 9 | 10 | That means, whenever you find yourself writing complex or long 11 | patterns in your functions, `expat` can be handy by allowing 12 | you to split your pattern into re-usable and composable bits. 13 | 14 | These named pattern matchers defined with `expat` can be used, 15 | for example, to match over large phoenix parameters and keep 16 | your action definitions short and concise. Since programmers 17 | read code all the time, their code should be optimized for 18 | *communicating their intent*, so instead of having your brain 19 | to parse all the way down the large structure pattern it 20 | would be better to abstract that pattern with a name. 21 | 22 | Also, as patterns get abstracted and split into re-usable 23 | pieces they could be exported so other libraries (or your 24 | own umbrella applications) can communicate the rules for 25 | matching data being passed between them. 26 | 27 | To read more about the motivation and where this library comes from, 28 | you can read [the v0 README](https://github.com/vic/expat/blob/v0/README.md) 29 | 30 | ## `use Expat` 31 | 32 | ### Named Patterns 33 | 34 | Let's start with some basic data examples. In Erlang/Elixir it's very 35 | common to use tagged tuples to communicate between functions. 36 | For example, a function that can fail might return `{:error, reason}` 37 | or `{:ok, result}`. 38 | 39 | Of course these two element tuples are so small, that 40 | most of the time it's better to use them as they *communicate the intent* 41 | they are being used for. 42 | 43 | But, using them can help us understand the basics of how `expat` works, 44 | just remember that `expat` takes patterns, and is not limited 45 | to some particular data structure. 46 | 47 | ```elixir 48 | defmodule MyPatterns do 49 | use Expat 50 | 51 | defpat ok({:ok, result}) 52 | defpat error({:error, reason}) 53 | end 54 | ``` 55 | 56 | So, just like you'd be able to use `{:ok, result} = expr` to match 57 | some expression, you can give the name `ok` to the `{:ok, result}` pattern. 58 | 59 | Later on, at some other module, you can use those named patterns. 60 | 61 | ```elixir 62 | iex> import MyPatterns 63 | iex> Kernel.match?(ok(), {:ok, :hey}) 64 | true 65 | ``` 66 | 67 | In the previous example, the `ok()` macro actually expanded to: 68 | 69 | 70 | ```elixir 71 | iex> Kernel.match?({:ok, _}, {:ok, :hey}) 72 | true 73 | ``` 74 | 75 | Notice that even when the `ok` pattern definition says it 76 | has an inner `result`, we didn't actually were interested in it, 77 | so `ok()` just ensures the data is matched with the structure 78 | mandated by its pattern and didn't bind any variable for us. 79 | 80 | If we do need access to some of the pattern variables, we can bind 81 | them by giving the pattern a `Keyword` of names to variables, 82 | for example: 83 | 84 | ```elixir 85 | # One nice thing about expat is you can use your patterns 86 | # anywhere you can currently write one, like in tests 87 | iex> assert error(reason: x) = {:error, "does not exist"} 88 | iex> x 89 | "does not exist" 90 | ``` 91 | 92 | And of course, if you bind all the variables in a pattern, you can 93 | use its macro as a data constructor, for example: 94 | 95 | ```elixir 96 | iex> ok(result: "done") 97 | {:ok, "done"} 98 | ``` 99 | 100 | That's it for our tagged tuples example. 101 | 102 | ### Combining patterns 103 | 104 | Now we know the basics of how to define and use named patterns, 105 | let's see how we can combine them to form larger patterns. 106 | 107 | Let's use some structs instead of tuples, as that might be 108 | a more common use case. 109 | 110 | ```elixir 111 | defmodule Pet do 112 | defstruct [:name, :age, :owner, :kind] 113 | end 114 | 115 | defmodule Person do 116 | defstruct [:name, :age, :country] 117 | end 118 | 119 | defmodule MyPatterns do 120 | use Expat 121 | 122 | defpat mexican(%Person{name: name, country: "MX"}) 123 | 124 | defpat mexican_parrot(%Pet{kind: :parrot, name: name, age: age, 125 | owner: mexican(name: owner_name)}) 126 | end 127 | 128 | iex> vic = %Person{name: "vic", country: "MX"} 129 | ...> milo = %Pet{kind: :parrot, name: "Milo", owner: vic, age: 4} 130 | ...> 131 | ...> # here, we are only interested in the owner's name 132 | ...> mexican_parrot(owner_name: name) = milo 133 | ...> name 134 | "vic" 135 | ``` 136 | 137 | And again, if you bind all the variables, it could be used as a data constructor 138 | 139 | ```elixir 140 | iex> mexican_parrot(age: 1, name: "Venus", owner_name: "Alicia") 141 | %Pet{kind: :parrot, name: "Venus", age: 1, owner: %Person{country: "MX", name: "Alicia", age: nil}} 142 | ``` 143 | 144 | Then you could use those patterns in a module of yours 145 | 146 | ```elixir 147 | defmodule Feed do 148 | import MyPatterns 149 | 150 | def with_mexican_food(bird = mexican_parrot(name: name, owner_name: owner)) do 151 | "#{name} is happy now!, thank you #{owner}" 152 | end 153 | end 154 | ``` 155 | 156 | And the function head will actually match using the whole composite pattern, and only 157 | bind those fields you are interested in using. 158 | 159 | 160 | ### Guarding patterns 161 | 162 | Since expat v1.0 it's now possible to use guards on your pattern definitions, and they 163 | will be expanded at the call-site. 164 | 165 | For example, let's build this year's flawed election system. 166 | 167 | ```elixir 168 | defmodule Voting.Patterns do 169 | use Expat 170 | 171 | defpat mexican(%Person{country: "MX"}) 172 | 173 | defpat adult(%{age: age}) when is_integer(age) and age >= 18 174 | end 175 | ``` 176 | 177 | Notice that the `adult` pattern matches anything with an integer age greater than 18 years 178 | (mexico's legal age to vote) by using `when` guards on the definition. 179 | 180 | Notice the `expat def can_vote?` part in the following code: 181 | 182 | ```elixir 183 | defmodule Voting do 184 | use Expat 185 | import Voting.Patterns 186 | 187 | def is_local?(mexican()), do: true 188 | def is_local?(_), do: false 189 | 190 | expat def can_vote?(mexican() = adult()), do: true 191 | def can_vote?(_), do: false 192 | end 193 | ``` 194 | 195 | `expat` stands for `expand pattern` in the following expression, *and* 196 | expand their guards in the correct place. 197 | 198 | So our `can_vote?` function checks that the data given to it looks like 199 | a mexican *and also* (since we are `=`ing two patterns), that the data 200 | represents an adult with legal age to vote by using guards. 201 | 202 | `expat` will work for `def`, `defmacro`, their private variants, `case`, 203 | and `fn`. 204 | 205 | Actually you can give any expression into `expat`. And your patterns will 206 | be expanded correctly within it. 207 | 208 | For example, the previous module could be written like: 209 | 210 | ```elixir 211 | use Expat 212 | import Voting.Patterns 213 | 214 | expat defmodule Voting do 215 | 216 | def is_local?(mexican()), do: true 217 | def is_local?(_), do: false 218 | 219 | def can_vote?(mexican() = adult()), do: true 220 | def can_vote?(_), do: false 221 | end 222 | 223 | # Un-import since its pattern macros 224 | # were used only during compilation. 225 | import Voting.Patterns, only: [] 226 | ``` 227 | 228 | 229 | ### Guarded data constructors 230 | 231 | As mentioned previously, if you expand a pattern and bind all of it's inner 232 | variables (provided the pattern was not defined with any `_` var), then you 233 | are effectively just building data from it. 234 | 235 | However, for patterns that include guards (or those expanding inner patterns 236 | including guards), an special bang function can be used to build data and make 237 | sure the guards are satisfied. 238 | 239 | Bang constructors are positional, that means variables are bound in the order 240 | they appear on your named pattern. 241 | 242 | For example, for our previous `adult` pattern: 243 | 244 | ```elixir 245 | defpat adult(%{age: age}) when is_integer(age) and age >= 18 246 | ``` 247 | 248 | The `adult!(age)` constructor will be generated. 249 | 250 | See [HOW_IT_WORKS](https://github.com/vic/expat/tree/master/HOW_IT_WORKS.md) 251 | for more info on how guards are expanded within Expat. 252 | 253 | ### Union Patterns 254 | 255 | This is an Expat feature that lets you compose many named patterns into a single 256 | *union* pattern. They are explained best with code, see bellow. 257 | 258 | Using unions, you can emulate things like 259 | [Algebraic data types](https://en.wikipedia.org/wiki/Algebraic_data_type) 260 | 261 | For some examples, see: 262 | 263 | - [Natural numbers](https://github.com/vic/expat/tree/master/test/expat_nat_test.exs). 264 | - [Maybe](https://github.com/vic/expat/tree/master/test/expat_maybe_test.exs). 265 | - [Either](https://github.com/vic/expat/tree/master/test/expat_either_test.exs). 266 | - [Result](https://github.com/vic/expat/tree/master/test/expat_result_test.exs). 267 | - [Union on Struct](https://github.com/vic/expat/tree/master/test/expat_union_test.exs). 268 | 269 | 270 | ### Documentation 271 | 272 | Your named pattern macros will be generated with documentation about what variables 273 | they take and what they will expand to. If you are in IEx, be sure to checkout their 274 | documentation using something like: `h Voting.Patterns.adult` 275 | 276 | Also, be sure to read the [documentation](https://hexdocs.pm/expat), and checkout some 277 | of the [tests](https://github.com/vic/expat/tree/master/test/). 278 | 279 | Happy Pattern Matching! 280 | 281 | ## Installation 282 | 283 | ```elixir 284 | def deps do 285 | [ 286 | {:expat, "~> 1.0"} 287 | ] 288 | end 289 | ``` 290 | 291 | -------------------------------------------------------------------------------- /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 9 | # 3rd-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure your application as: 12 | # 13 | # config :expat, key: :value 14 | # 15 | # and access this configuration in your application as: 16 | # 17 | # Application.get_env(:expat, :key) 18 | # 19 | # You can also configure a 3rd-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | # import_config "#{Mix.env}.exs" 31 | -------------------------------------------------------------------------------- /lib/expat.ex: -------------------------------------------------------------------------------- 1 | defmodule Expat do 2 | 3 | @moduledoc ~S""" 4 | Pattern Matching as Macros 5 | 6 | use Expat 7 | 8 | This will import `defpat` and `expat` into scope. 9 | """ 10 | 11 | @doc false 12 | defmacro __using__([]) do 13 | quote do 14 | import Expat 15 | end 16 | end 17 | 18 | defmacro __using__(import: {module, names}) do 19 | quote do 20 | import Expat 21 | require unquote(module) 22 | import unquote(module), only: unquote(Enum.map(names, fn n -> {n, 1} end)) 23 | end 24 | end 25 | 26 | alias Expat.Macro, as: EM 27 | 28 | @type simple_call :: {atom, keyword, list(Macro.t())} 29 | @type guarded_pattern :: {:when, list, [simple_call, ...]} 30 | @type pattern :: simple_call | guarded_pattern 31 | 32 | @doc ~S""" 33 | Define a new named pattern. 34 | 35 | This function takes only the function head as argument. 36 | You may also specify a guard, but never a do block. 37 | 38 | Variables present in the function head can be later bound. 39 | Guards if any are also expanded at call site, for example 40 | in `def`, `case`, `fn` expressions. See `expat/1` for more. 41 | 42 | ## Examples 43 | 44 | defpat person(%Person{name: name}) 45 | defpat adult(%{age: age}) when age > 18 46 | 47 | """ 48 | @spec defpat(pattern) :: Macro.t() 49 | defmacro defpat(pattern) do 50 | EM.define_pattern(:defmacro, pattern) 51 | end 52 | 53 | @doc "Same as defpat but defines private patterns" 54 | @spec defpatp(pattern) :: Macro.t() 55 | defmacro defpatp(pattern) do 56 | EM.define_pattern(:defmacrop, pattern) 57 | end 58 | 59 | @doc ~S""" 60 | Expand an expression using named patterns. 61 | 62 | `expat` stands for `expand pattern` in an expression. 63 | It's also the name of the library :). 64 | 65 | Note that for this to work, the macros that 66 | define the named patterns should already have 67 | been compiled and in scope. For this reason, most of the 68 | time, named patterns should be defined on 69 | separate modules and imported for use. 70 | 71 | ## Example 72 | 73 | You define a module for your named patterns 74 | 75 | defmodule MyPatterns do 76 | use Expat 77 | 78 | @doc "Matches when n is legal age to vote" 79 | defpat adult_age(n) when n > 18 80 | end 81 | 82 | Then you can import it and use it's macros 83 | 84 | defmodule Foo do 85 | use Expat 86 | import MyPatterns 87 | 88 | def foo(x) do 89 | # Tell expat that we want the case 90 | # clauses being able to use guards 91 | # from the named pattern. 92 | # 93 | # foo(20) => :vote 94 | # 95 | expat case x do 96 | adult_age() -> :vote 97 | end 98 | end 99 | 100 | # You can also use expat at the `def` 101 | # level (or defp, defmacro, etc) 102 | # 103 | # In this case, we are asking expat to 104 | # also expand the named patterns it 105 | # sees on our function head, and the 106 | # guards it produces are added to our 107 | # function definition. 108 | # 109 | # vote(20) => {:voted, 20} 110 | # vote(20) => no function match error 111 | # 112 | expat def vote(adult_age(n: x)) do 113 | {:voted, x} 114 | end 115 | end 116 | 117 | You can even use `expat` only once at the module 118 | level, then all it's `def`, `case`, `fn`, ... will 119 | be able to use named patterns. 120 | 121 | 122 | use Expat 123 | import MyPatterns, only: [adult_age: 1] 124 | 125 | expat defmodule Ellections do 126 | 127 | def vote(adult_age(n: x)) do 128 | {:ok, x} 129 | end 130 | 131 | def vote(_), do: :error 132 | end 133 | 134 | """ 135 | @spec expat(Macro.t()) :: Macro.t() 136 | defmacro expat(ast) do 137 | EM.expand_inside(ast, _: [escape: true, env: __CALLER__]) 138 | end 139 | 140 | @doc false 141 | @spec expat(Macro.t(), Macro.t()) :: Macro.t() 142 | defmacro expat({n, m, a}, opts) do 143 | expr = {n, m, a ++ [opts]} 144 | EM.expand_inside(expr, _: [escape: true, env: __CALLER__]) 145 | end 146 | end 147 | -------------------------------------------------------------------------------- /lib/expat/macro.ex: -------------------------------------------------------------------------------- 1 | defmodule Expat.Macro do 2 | @moduledoc """ 3 | Expat internals for working with Macro.t() 4 | """ && false 5 | 6 | alias Expat, as: E 7 | 8 | @doc "Defines a named pattern" 9 | @spec define_pattern(defm :: :defmacro | :defmacrop, E.pattern()) :: E.pattern() 10 | def define_pattern(defm, pattern) 11 | 12 | def define_pattern(defm, {:|, _, [head, alts]}) do 13 | define_union_pattern(defm, union_head(head), collect_alts(alts)) 14 | end 15 | 16 | def define_pattern(defm, pattern) do 17 | define_simple_pattern(defm, pattern) 18 | end 19 | 20 | 21 | @doc "Expands a pattern" 22 | @spec expand(pattern :: E.pattern(), opts :: list) :: Macro.t() 23 | def expand(pattern, opts) when is_list(opts) do 24 | binds = Keyword.delete(opts, :_) 25 | expat_opts = Keyword.get_values(opts, :_) |> Enum.concat() 26 | 27 | name = pattern_name(pattern) 28 | counter = :erlang.unique_integer([:positive]) 29 | pattern = pattern |> remove_line |> set_expansion_counter(counter, name) 30 | guard = pattern_guard(pattern) 31 | 32 | value = 33 | pattern 34 | |> pattern_value 35 | |> bind(binds) 36 | |> make_under 37 | 38 | # remove names bound on this expansion 39 | bounds = bound_names_in_ast(value) 40 | # only delete the first to allow sub expansions take the rest 41 | binds = Enum.reduce(bounds, binds, &Keyword.delete_first(&2, &1)) 42 | 43 | opts = [_: expat_opts] ++ binds 44 | {value, guard} = expand_arg_collecting_guard(value, guard, opts) 45 | 46 | result = (guard && {:when, [context: Elixir], [value, guard]}) || value 47 | 48 | code = cond do 49 | expat_opts[:escape] -> Macro.escape(result) 50 | expat_opts[:build] && guard -> 51 | quote do 52 | fn -> 53 | case unquote(value) do 54 | x when unquote(guard) -> x 55 | end 56 | end.() 57 | end 58 | :else -> result 59 | end 60 | 61 | if expat_opts[:show], do: show(code) 62 | code 63 | end 64 | 65 | def expand_inside(expr, opts) do 66 | Macro.postwalk(expr, &do_expand_inside(&1, opts)) 67 | end 68 | 69 | ## Private parts bellow 70 | 71 | defp do_expand_inside({defn, c, [head, rest]}, opts) 72 | when defn == :def or defn == :defp or defn == :defmacro or defn == :defmacrop do 73 | args = pattern_args(head) 74 | guard = pattern_guard(head) 75 | 76 | {args, guard} = expand_args_collecting_guard(args, guard, opts) 77 | 78 | head = 79 | head 80 | |> update_pattern_guard(fn _ -> guard end) 81 | |> update_pattern_args(fn _ -> args end) 82 | 83 | {defn, c, [head, rest]} 84 | end 85 | 86 | defp do_expand_inside({:fn, c, clauses}, opts) do 87 | clauses = 88 | clauses 89 | |> Enum.map(fn {:->, a, [[e], body]} -> 90 | {:->, a, [[expand_calls_inside(e, opts)], body]} 91 | end) 92 | 93 | {:fn, c, clauses} 94 | end 95 | 96 | defp do_expand_inside({:case, c, [v, [do: clauses]]}, opts) do 97 | clauses = 98 | clauses 99 | |> Enum.map(fn {:->, a, [[e], body]} -> 100 | {:->, a, [[expand_calls_inside(e, opts)], body]} 101 | end) 102 | 103 | {:case, c, [v, [do: clauses]]} 104 | end 105 | 106 | defp do_expand_inside({:with, c, clauses}, opts) do 107 | clauses = 108 | clauses 109 | |> Enum.map(fn 110 | {:<-, a, [p, e]} -> 111 | {:<-, a, [expand_calls_inside(p, opts), e]} 112 | 113 | x -> 114 | x 115 | end) 116 | 117 | {:with, c, clauses} 118 | end 119 | 120 | defp do_expand_inside(ast, _opts), do: ast 121 | 122 | defp expand_args_collecting_guard(args, guard, opts) do 123 | Enum.map_reduce(args, guard, fn arg, guard -> 124 | expand_arg_collecting_guard(arg, guard, opts) 125 | end) 126 | end 127 | 128 | defp expand_arg_collecting_guard(ast, guard, opts) do 129 | expand_calls_collect({ast, {true, guard}}, opts) 130 | end 131 | 132 | defp expand_calls_collect({ast, {false, final}}, _), do: {ast, final} 133 | 134 | defp expand_calls_collect({ast, {true, initial}}, opts) do 135 | env = Keyword.get(opts, :_, []) |> Keyword.get(:env, __ENV__) 136 | 137 | Macro.traverse(ast, {false, initial}, fn x, y -> {x, y} end, fn 138 | x = {c, m, args}, y = {_, acc} when is_list(args) -> 139 | if to_string(c) =~ ~R/^[a-z]/ do 140 | expat_opts = [_: [escape: true]] 141 | 142 | args = 143 | args 144 | |> Enum.reverse() 145 | |> case do 146 | [o | rest] when is_list(o) -> 147 | if Keyword.keyword?(o) do 148 | [expat_opts ++ o] ++ rest 149 | else 150 | [expat_opts, o] ++ rest 151 | end 152 | 153 | x -> 154 | [expat_opts] ++ x 155 | end 156 | |> Enum.reverse() 157 | 158 | {c, m, args} 159 | |> Code.eval_quoted([], env) 160 | |> elem(0) 161 | |> collect_guard(acc) 162 | |> (fn {x, y} -> {x, {true, y}} end).() 163 | else 164 | {x, y} 165 | end 166 | 167 | x, y -> 168 | {x, y} 169 | end) 170 | |> expand_calls_collect(opts) 171 | end 172 | 173 | defp collect_guard({:when, _, [expr, guard]}, prev) do 174 | {expr, and_guard(prev, guard)} 175 | end 176 | 177 | defp collect_guard(expr, guard) do 178 | {expr, guard} 179 | end 180 | 181 | defp and_guard(nil, guard), do: guard 182 | defp and_guard(a, b), do: quote(do: unquote(a) and unquote(b)) 183 | 184 | defp expand_calls_inside(ast, opts) do 185 | {expr, guard} = 186 | case ast do 187 | {:when, _, [ast, guard]} -> 188 | expand_arg_collecting_guard(ast, guard, opts) 189 | 190 | _ -> 191 | expand_arg_collecting_guard(ast, nil, opts) 192 | end 193 | 194 | (guard && {:when, [], [expr, guard]}) || expr 195 | end 196 | 197 | @doc "Make underable variables an underscore to be ignored" && false 198 | defp make_under(pattern) do 199 | Macro.prewalk(pattern, fn 200 | v = {_, m, _} -> 201 | (m[:underable] && {:_, [], nil}) || v 202 | 203 | x -> 204 | x 205 | end) 206 | end 207 | 208 | @doc "Mark variables in pattern that are not used in guards" && false 209 | defp mark_non_guarded(pattern) do 210 | vars_in_guards = pattern |> pattern_guard |> ast_variables 211 | 212 | value = 213 | pattern |> pattern_value 214 | |> Macro.prewalk(fn 215 | v = {_, [{:underable, true} | _], _} -> 216 | v 217 | 218 | v = {n, m, c} when is_atom(n) and is_atom(c) -> 219 | unless Enum.member?(vars_in_guards, v) do 220 | {n, [underable: true] ++ m, c} 221 | else 222 | v 223 | end 224 | 225 | x -> 226 | x 227 | end) 228 | 229 | pattern |> update_pattern_value(fn _ -> value end) 230 | end 231 | 232 | @doc "Marks all variables with the name they can be bound to" && false 233 | defp mark_bindable(pattern) do 234 | Macro.prewalk(pattern, fn 235 | {a, m, c} when is_atom(a) and is_atom(c) -> 236 | if to_string(a) =~ ~r/^_/ do 237 | {a, m, c} 238 | else 239 | {a, [bindable: a] ++ m, c} 240 | end 241 | 242 | x -> 243 | x 244 | end) 245 | end 246 | 247 | defp ast_variables(nil), do: [] 248 | 249 | defp ast_variables(ast) do 250 | {_, acc} = 251 | Macro.traverse(ast, [], fn x, y -> {x, y} end, fn 252 | v = {n, _, c}, acc when is_atom(n) and is_atom(c) -> {v, [v] ++ acc} 253 | ast, acc -> {ast, acc} 254 | end) 255 | 256 | acc |> Stream.uniq_by(fn {a, _, _} -> a end) |> Enum.reverse() 257 | end 258 | 259 | defp bind(pattern, binds) do 260 | pattern 261 | |> Macro.prewalk(fn 262 | x = {_, [{:bound, _} | _], _} -> 263 | x 264 | 265 | {a, m = [{:bindable, b} | _], c} -> 266 | case List.keyfind(binds, b, 0) do 267 | nil -> 268 | {a, m, c} 269 | 270 | {_, var = {vn, vm, vc}} when is_atom(vn) and is_atom(vc) -> 271 | if m[:underable] do 272 | {vn, [bound: b] ++ vm, vc} 273 | else 274 | {:=, [bound: b], [var, {a, [bound: b] ++ m, c}]} 275 | end 276 | 277 | {_, expr} -> 278 | if m[:underable] do 279 | expr 280 | else 281 | {:=, [bound: b], [{a, [bound: b] ++ m, c}, expr]} 282 | end 283 | end 284 | 285 | x -> 286 | x 287 | end) 288 | end 289 | 290 | defp update_pattern_value(pattern, up) when is_function(up, 1) do 291 | update_pattern_head(pattern, fn {name, c, [value]} -> 292 | {name, c, [up.(value)]} 293 | end) 294 | end 295 | 296 | defp pattern_value(pattern) do 297 | {_, _, [value]} = pattern |> pattern_head 298 | value 299 | end 300 | 301 | defp pattern_args(pattern) do 302 | {_, _, args} = pattern |> pattern_head 303 | args 304 | end 305 | 306 | defp update_pattern_args(pattern, up) when is_function(up, 1) do 307 | update_pattern_head(pattern, fn {name, c, args} -> 308 | {name, c, up.(args)} 309 | end) 310 | end 311 | 312 | defp update_pattern_guard(pattern, up) when is_function(up, 1) do 313 | case pattern do 314 | {:when, x, [head, guard]} -> 315 | new_guard = up.(guard) 316 | 317 | if new_guard do 318 | {:when, x, [head, new_guard]} 319 | else 320 | head 321 | end 322 | 323 | head -> 324 | new_guard = up.(nil) 325 | 326 | if new_guard do 327 | {:when, [context: Elixir], [head, new_guard]} 328 | else 329 | head 330 | end 331 | end 332 | end 333 | 334 | defp pattern_guard(pattern) do 335 | case pattern do 336 | {:when, _, [_head, guard]} -> guard 337 | _ -> nil 338 | end 339 | end 340 | 341 | defp update_pattern_head(pattern, up) when is_function(up, 1) do 342 | case pattern do 343 | {:when, x, [head, guard]} -> 344 | {:when, x, [up.(head), guard]} 345 | 346 | head -> 347 | up.(head) 348 | end 349 | end 350 | 351 | defp pattern_head(pattern) do 352 | case pattern do 353 | {:when, _, [head, _guard]} -> head 354 | head -> head 355 | end 356 | end 357 | 358 | defp pattern_name(pattern) do 359 | {name, _, _} = pattern |> pattern_head 360 | name 361 | end 362 | 363 | defp meta_in_ast(ast, key) do 364 | {_, acc} = 365 | Macro.traverse(ast, [], fn 366 | ast = {_, m, _}, acc -> (m[key] && {ast, [m[key]] ++ acc}) || {ast, acc} 367 | ast, acc -> {ast, acc} 368 | end, fn x, y -> {x, y} end) 369 | 370 | acc |> Stream.uniq() |> Enum.reverse() 371 | end 372 | 373 | defp bindable_names_in_ast(ast) do 374 | ast |> meta_in_ast(:bindable) 375 | end 376 | 377 | defp bound_names_in_ast(ast) do 378 | ast |> meta_in_ast(:bound) 379 | end 380 | 381 | def show(ast) do 382 | IO.puts(Macro.to_string(ast)) 383 | ast 384 | end 385 | 386 | defp remove_line(ast) do 387 | Macro.postwalk(ast, &Macro.update_meta(&1, fn x -> Keyword.delete(x, :line) end)) 388 | end 389 | 390 | defp set_expansion_counter(ast, counter, pattern_name) do 391 | Macro.postwalk(ast, fn 392 | s = {x, m, y} when is_atom(x) and is_atom(y) -> 393 | cond do 394 | match?("_" <> _, to_string(x)) -> 395 | s 396 | m[:bindable] && !m[:expat_pattern] -> 397 | {x, m ++ [counter: counter, expat_pattern: pattern_name], y} 398 | :else -> 399 | s 400 | end 401 | 402 | x -> 403 | x 404 | end) 405 | end 406 | 407 | defp pattern_bang(defm, name, escaped, arg_names, opts) do 408 | vars = arg_names |> Enum.map(&Macro.var(&1, __MODULE__)) 409 | argt = vars |> Enum.map(fn name -> quote do: unquote(name) :: any end) 410 | kw = Enum.zip([arg_names, vars]) 411 | bang = :"#{name}!" 412 | 413 | quote do 414 | @doc """ 415 | Builds data using the `#{unquote(name)}` pattern. 416 | 417 | See `#{unquote(name)}/0`. 418 | """ 419 | @spec unquote(bang)(unquote_splicing(argt)) :: any 420 | unquote(defm)(unquote(bang)(unquote_splicing(vars))) do 421 | opts = unquote(kw) ++ [_: [build: true]] ++ unquote(opts) 422 | Expat.Macro.expand(unquote(escaped), opts) 423 | end 424 | end 425 | end 426 | 427 | defp pattern_zero(defm, name, pattern, escaped, arg_names, opts) do 428 | doc = pattern_zero_doc(name, pattern, arg_names) 429 | quote do 430 | @doc unquote(doc) 431 | unquote(defm)(unquote(name)()) do 432 | Expat.Macro.expand(unquote(escaped), unquote(opts)) 433 | end 434 | end 435 | end 436 | 437 | defp pattern_zero_doc(name, pattern, arg_names) do 438 | value = pattern |> pattern_value 439 | guard = pattern |> pattern_guard 440 | 441 | code = 442 | cond do 443 | guard -> {:when, [], [value, guard]} 444 | true -> value 445 | end 446 | |> Macro.to_string() 447 | 448 | first_name = arg_names |> List.first() 449 | 450 | """ 451 | Expands the `#{name}` pattern. 452 | 453 | #{code} 454 | 455 | 456 | ## Binding Variables 457 | 458 | The following variables can be bound by giving them 459 | to `#{name}` as keys on its last argument Keyword. 460 | 461 | #{arg_names |> Enum.map(&":#{&1}") |> Enum.join(", ")} 462 | 463 | For example: 464 | 465 | #{name}(#{first_name}: x) 466 | 467 | Where `x` can be any value, variable in your scope 468 | or another pattern expansion. 469 | Not mentioned variables will be unbound and replaced by 470 | an `_` at expansion site. 471 | Likewise, calling `#{name}()` with no argumens will 472 | replace all its variables with `_`. 473 | 474 | ## Positional Variables 475 | 476 | `#{name}` variables can also be bound by position, 477 | provided the last them is not a Keyword. 478 | 479 | For example: 480 | 481 | #{name}(#{Enum.join(arg_names, ", ")}, bindings = []) 482 | 483 | 484 | ## Bang Constructor 485 | 486 | The `#{name}!` constructor can be used to build data and 487 | make sure the guards are satisfied. 488 | 489 | Note that this macro can only be used as an expression 490 | and not as a matching pattern. 491 | 492 | For example: 493 | 494 | #{name}!(#{Enum.join(arg_names, ", ")}) 495 | 496 | """ 497 | end 498 | 499 | defp pattern_defs(defm, name, escaped, arg_names, opts) do 500 | arities = 0..length(arg_names) 501 | Enum.map(arities, fn n -> 502 | args = arg_names |> Enum.take(n) 503 | last = arg_names |> Enum.at(n) 504 | vars = args |> Enum.map(&Macro.var(&1, __MODULE__)) 505 | argt = vars |> Enum.map(fn name -> quote do: unquote(name) :: any end) 506 | kw = Enum.zip([args, vars]) 507 | 508 | quote do 509 | @doc """ 510 | Expands the `#{unquote(name)}` pattern. 511 | 512 | See `#{unquote(name)}/0`. 513 | """ 514 | @spec unquote(name)(unquote_splicing(argt), bindings :: keyword) :: any 515 | unquote(defm)(unquote(name)(unquote_splicing(vars), bindings)) 516 | 517 | unquote(defm)(unquote(name)(unquote_splicing(vars), opts)) do 518 | opts = (Keyword.keyword?(opts) && opts) || [{unquote(last), opts}] 519 | opts = unquote(kw) ++ opts ++ unquote(opts) 520 | Expat.Macro.expand(unquote(escaped), opts) 521 | end 522 | end 523 | end) 524 | end 525 | 526 | defp define_simple_pattern(defm, pattern) do 527 | name = pattern_name(pattern) 528 | bindable = pattern |> mark_non_guarded |> mark_bindable 529 | escaped = bindable |> Macro.escape() 530 | 531 | opts = 532 | quote do 533 | [_: [env: __CALLER__, name: unquote(name)]] 534 | end 535 | 536 | arg_names = bindable |> pattern_args |> bindable_names_in_ast 537 | 538 | defs = pattern_defs(defm, name, escaped, arg_names, opts) 539 | zero = pattern_zero(defm, name, pattern, escaped, arg_names, opts) 540 | bang = pattern_bang(defm, name, escaped, arg_names, opts) 541 | 542 | defs = [bang, zero] ++ defs 543 | {:__block__, [], defs} 544 | end 545 | 546 | defp collect_alts({:|, _, [a, b]}) do 547 | [a | collect_alts(b)] 548 | end 549 | 550 | defp collect_alts(x) do 551 | [x] 552 | end 553 | 554 | defp union_head({name, _, x}) when is_atom(name) and (is_atom(x) or [] == x) do 555 | # foo({:foo, foo}) 556 | {name, [], [{name, {name, [], nil}}]} 557 | end 558 | 559 | defp union_head(head), do: head 560 | 561 | defp define_union_pattern(defm, head, alts) do 562 | head_name = pattern_name(head) 563 | head_pats = define_simple_pattern(defm, head) 564 | alts_pats = alts |> Enum.map(fn alt -> 565 | alt = update_pattern_args(alt, &[{head_name, [], &1}]) 566 | define_simple_pattern(defm, alt) 567 | end) 568 | {:__block__, [], [head_pats, alts_pats]} 569 | end 570 | 571 | 572 | end 573 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Expat.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :expat, 7 | version: "1.0.5", 8 | elixir: "~> 1.6", 9 | start_permanent: Mix.env() == :prod, 10 | name: "Expat", 11 | description: "Re-usable composable patterns with guards", 12 | elixirc_paths: elixirc_paths(Mix.env()), 13 | package: package(), 14 | docs: [ 15 | main: "Expat" 16 | ], 17 | deps: deps() 18 | ] 19 | end 20 | 21 | defp package do 22 | [ 23 | maintainers: ["Victor Borja "], 24 | licenses: ["Apache-2"], 25 | links: %{"GitHub" => "https://github.com/vic/expat"} 26 | ] 27 | end 28 | 29 | defp elixirc_paths(:test), do: ["lib", "test/support"] 30 | defp elixirc_paths(_), do: ["lib"] 31 | 32 | # Run "mix help compile.app" to learn about applications. 33 | def application do 34 | [ 35 | extra_applications: [:logger] 36 | ] 37 | end 38 | 39 | # Run "mix help deps" to learn about dependencies. 40 | defp deps do 41 | [ 42 | {:ex_doc, "~> 0.0", only: :dev, runtime: false} 43 | # {:dep_from_hexpm, "~> 0.3.0"}, 44 | # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}, 45 | ] 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "earmark": {:hex, :earmark, "1.2.4", "99b637c62a4d65a20a9fb674b8cffb8baa771c04605a80c911c4418c69b75439", [:mix], [], "hexpm"}, 3 | "ex_doc": {:hex, :ex_doc, "0.18.3", "f4b0e4a2ec6f333dccf761838a4b253d75e11f714b85ae271c9ae361367897b7", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}], "hexpm"}, 4 | } 5 | -------------------------------------------------------------------------------- /test/ast_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Expat.AstTest.Patterns do 2 | use Expat 3 | 4 | defpat atom(name) when is_atom(name) 5 | defpat list(items) when is_list(items) 6 | 7 | defpat aliases({:__aliases__, list(meta), list(aliases)}) 8 | 9 | defpat var({atom(name), list(meta), atom(context)}) 10 | 11 | defpat local_call({atom(local), list(meta), list(args)}) 12 | 13 | defpat head({atom(name), list(meta), params}) 14 | defpat guarded({:when, list(meta), [expr, guard]}) 15 | 16 | defpat defun({:def, list(meta), [head, [do: body]]}) 17 | end 18 | 19 | defmodule Expat.AstTest do 20 | use ExUnit.Case 21 | use Expat 22 | 23 | @moduledoc """ 24 | Tests showing how you can use Expat to 25 | work with data like Elixir quoted AST. 26 | """ 27 | 28 | import __MODULE__.Patterns 29 | 30 | test "can match an atom" do 31 | assert atom() = :foo 32 | end 33 | 34 | test "can match and bind atom" do 35 | assert atom(name) = :hello 36 | assert name == :hello 37 | end 38 | 39 | expat test("can match a variable") do 40 | x = with var(name) <- quote(do: hello) do 41 | name 42 | end 43 | assert :hello == x 44 | end 45 | 46 | expat def call_name(local_call(name)) do 47 | name 48 | end 49 | 50 | test "can extract the name of a local call" do 51 | assert :foo = call_name(quote do: foo(1, 2)) 52 | end 53 | 54 | expat test("can match a defun with case") do 55 | q = quote do 56 | def foo(a, b) do 57 | a + b 58 | end 59 | end 60 | 61 | x = case q do 62 | defun(head: head(name: n)) -> n 63 | end 64 | assert x == :foo 65 | end 66 | 67 | # unimport to prevent warning of it being unused 68 | # since it's used only at compilation time. 69 | import __MODULE__.Patterns, only: [] 70 | end 71 | -------------------------------------------------------------------------------- /test/expat_def_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Expat.DefTest do 2 | use ExUnit.Case 3 | use Expat, import: {Expat.Test.Named, [:age_to_vote]} 4 | 5 | expat def vote(age_to_vote()) do 6 | :voted 7 | end 8 | 9 | def vote(_) do 10 | :no_vote 11 | end 12 | 13 | expat def foo(x) do 14 | case x do 15 | age_to_vote() -> :voted 16 | _ -> :no_vote 17 | end 18 | end 19 | 20 | expat def anon() do 21 | fn age_to_vote() -> :voted end 22 | end 23 | 24 | test "expat def should add guard clause" do 25 | assert :voted == vote(20) 26 | end 27 | 28 | test "expat def when not match pattern guard" do 29 | assert :no_vote == vote(10) 30 | end 31 | 32 | test "expat def inner case" do 33 | assert :voted == foo(20) 34 | end 35 | 36 | test "expat def inner anon function" do 37 | assert :voted == anon().(20) 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /test/expat_defmodule_test.exs: -------------------------------------------------------------------------------- 1 | # It should be possible to import named patterns 2 | # only for using them at compile time 3 | use Expat, import: {Expat.Test.Named, [:age_to_vote]} 4 | 5 | # and tell expat to expand named patterns on a 6 | # whole module definiton 7 | expat defmodule(Expat.DefModuleTest) do 8 | use ExUnit.Case 9 | 10 | def vote(age_to_vote()) do 11 | :voted 12 | end 13 | 14 | test "expanded named pattern in function head" do 15 | assert :voted == vote(20) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/expat_either_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Expat.EitherTest do 2 | use ExUnit.Case 3 | use Expat 4 | 5 | @moduledoc ~S""" 6 | Either using Union Patterns. 7 | 8 | In the code bellow note that the head pattern (`either`) is guarded. 9 | Because of this, building any of its tail patterns (`left` or `right`) 10 | needs to be done using their bang macro. 11 | 12 | The code bellow is exactly the same as: 13 | 14 | defpat either({tag, value}) when tag == :left or tag == :right 15 | defpat left(either(:left, value)) 16 | defpat right(either(:right, value)) 17 | 18 | """ 19 | 20 | defpat (either({tag, value}) when tag == :left or tag == :right) 21 | | left(:left, value) 22 | | right(:right, value) 23 | 24 | test "left creates a tagged tuple" do 25 | assert {:left, 22} = left!(22) 26 | end 27 | 28 | test "right creates a tagged tuple" do 29 | assert {:right, 22} = right!(22) 30 | end 31 | 32 | test "either can also be used to pattern match" do 33 | expat case left!(22) do 34 | either(:left, 22) -> assert :ok 35 | end 36 | end 37 | 38 | end 39 | -------------------------------------------------------------------------------- /test/expat_expansion_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Expat.ExpansionTest do 2 | use ExUnit.Case 3 | 4 | import Expat.Test.Named 5 | use Expat 6 | 7 | defpat moo(x) when is_atom(x) 8 | 9 | test "no collission between two x from different expansions of same pattern" do 10 | # expansion must collect the two sub pattern guards 11 | assert e = {:when, _, [expr, guard]} = t2(moo(true), moo(false), _: [escape: true]) 12 | # variable names look the same, have same name and context, but different counter 13 | assert "{x = true, x = false} when is_atom(x) and is_atom(x)" == Macro.to_string(e) 14 | assert {{:=, _, [{:x, xm, _}, true]}, {:=, _, [{:x, ym, _}, false]}} = expr 15 | # ensure their counter is different 16 | assert xm[:counter] != ym[:counter] 17 | # ensure the guarded vars correspond to the previous one 18 | assert {:and, _, [{:is_atom, _, [{:x, gxm, _}]}, {:is_atom, _, [{:x, gym, _}]}]} = guard 19 | assert xm[:counter] == gxm[:counter] 20 | assert ym[:counter] == gym[:counter] 21 | end 22 | 23 | test "defpat expansion returns guarded pattern" do 24 | {:when, _, [pattern, guard]} = age_to_vote(_: [escape: true]) 25 | assert {:n, m, _} = pattern 26 | assert m[:bindable] == :n 27 | assert m[:expat_pattern] == :age_to_vote 28 | assert {:>=, _, [{:n, g, _}, 18]} = guard 29 | assert g[:counter] == m[:counter] 30 | end 31 | 32 | test "can bind variables by their name to any elixir expression" do 33 | assert 22 = foo(bar: 22, _: [escaped: true]) 34 | end 35 | 36 | test "pattern variables do not overwrite those in scope" do 37 | n = 1 38 | assert age_to_vote(n: x) = 20 39 | assert x == 20 40 | assert n == 1 41 | end 42 | 43 | test "meta variable used in guard is bound" do 44 | q = 45 | quote do 46 | assert age_to_vote(n: x) = 20 47 | end 48 | 49 | bound = [{{:x, __MODULE__}, 20}] 50 | assert {20, ^bound} = Code.eval_quoted(q, [], __ENV__) 51 | end 52 | 53 | test "defpat expansion can add bindings to guarded pattern" do 54 | {:when, _, [pattern, guard]} = age_to_vote(n: v, _: [escape: true]) 55 | assert {:=, _, [{:v, _, _}, {:n, _, _}]} = pattern 56 | assert {:>=, _, [{:n, _, _}, 18]} = guard 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /test/expat_maybe_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Expat.MaybeTest do 2 | use ExUnit.Case 3 | use Expat 4 | 5 | @moduledoc ~S""" 6 | Using Unions to implement a simple Maybe type. 7 | 8 | Any non-nil value is an instance of Just. 9 | 10 | On the tests bellow notice that since the `just` 11 | pattern has a guard, we are using the `just!` constructor 12 | to create data from it and make sure the guard is satisfied. 13 | 14 | See also: expat_union_test.exs 15 | """ 16 | 17 | defpat maybe(v) 18 | | nothing(nil) 19 | | ( just(y) when not is_nil(y) ) 20 | 21 | test "nothing is nil" do 22 | assert nil == nothing() 23 | end 24 | 25 | test "just is non nil" do 26 | assert 23 = just!(23) 27 | end 28 | 29 | test "just can be pattern matched" do 30 | assert just() = :jordan 31 | end 32 | 33 | test "just can be pattern matched and extract" do 34 | assert just(j) = :jordan 35 | assert j == :jordan 36 | end 37 | 38 | test "nil cannot be pattern matched with just" do 39 | expat case Keyword.get([], :foo) do 40 | just() -> raise "Should not happen" 41 | nothing() -> assert :ok 42 | end 43 | end 44 | 45 | end 46 | -------------------------------------------------------------------------------- /test/expat_named_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Expat.NamedTest do 2 | use ExUnit.Case 3 | 4 | import Expat.Test.Named 5 | use Expat 6 | 7 | test "defpat defines a macro" do 8 | assert 1 == one() 9 | end 10 | 11 | test "can bind variables by position" do 12 | assert {:oh, :god} = t2(:oh, :god) 13 | end 14 | 15 | test "can bind variables by position for pattern with single variable" do 16 | assert 22 = foo(22) 17 | end 18 | 19 | test "generated macro can be used in left side of pattern match" do 20 | assert t2(b: t2(a: c)) = {1, {3, 4}} 21 | assert 3 == c 22 | end 23 | 24 | test "generated macro with guards can be used in case clause" do 25 | value = 26 | expat case(20) do 27 | age_to_vote(n: x) -> {:voted, x} 28 | _ -> :waited 29 | end 30 | 31 | assert {:voted, 20} = value 32 | end 33 | 34 | test "generated macro with guards can be used in with clause" do 35 | value = 36 | expat with age_to_vote(n: x) <- 20 do 37 | {:voted, x} 38 | end 39 | 40 | assert {:voted, 20} = value 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /test/expat_nat_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Expat.NatTest do 2 | use ExUnit.Case 3 | use Expat 4 | 5 | @moduledoc ~S""" 6 | Natural numbers. 7 | 8 | The union pattern bellow is shorthand for: 9 | 10 | defpat nat({:nat, x}) 11 | defpat zero(nat(0)) 12 | defpat succ(nat(nat() = n)) 13 | 14 | Note that both `zero` and `succ` are just using calling 15 | `nat` with some other pattern. Thus `zero()` builds `{:nat, 0}` and 16 | `succ` takes a single argument `n` which must itself be also a `nat()` 17 | 18 | See also expat_union_test.exs 19 | """ 20 | 21 | defpat nat 22 | | zero(0) 23 | | succ(nat() = n) 24 | 25 | test "zero is a nat" do 26 | assert nat() = zero() 27 | end 28 | 29 | test "succ of zero is a nat" do 30 | assert nat() = succ(zero()) 31 | end 32 | 33 | test "succ takes only nats" do 34 | assert_raise MatchError, ~r/no match of right hand side value: 99/, fn -> 35 | succ(99) 36 | end 37 | end 38 | 39 | test "zero is tagged tuple" do 40 | assert {:nat, 0} = zero() 41 | end 42 | 43 | test "succ of zero is tagged tuple" do 44 | assert {:nat, {:nat, 0}} = succ(zero()) 45 | end 46 | 47 | def to_i(zero()), do: 0 48 | def to_i(succ(n)), do: 1 + to_i(n) 49 | 50 | test "convert a nat to int" do 51 | assert 3 = zero() |> succ |> succ |> succ |> to_i 52 | end 53 | 54 | 55 | end 56 | -------------------------------------------------------------------------------- /test/expat_positional_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Expat.PoisitionalTest do 2 | use ExUnit.Case 3 | use Expat 4 | 5 | @moduledoc """ 6 | Pattern variables can be binded by giving the 7 | macro a single keyword of names to variables. 8 | 9 | However, in some cases it comes handy to bind 10 | the variables positionally. 11 | 12 | If the last argument given to a expat macro is 13 | a keyword list, it's assumed to be used for 14 | bindings. Otherwise, it's used as a positional 15 | argument. 16 | 17 | The position of variables inside a pattern is 18 | it's position inside the pattern. Because of 19 | this, only use positional arguments for really 20 | simple patterns. (see expat_ast_test.exs) 21 | """ 22 | 23 | defpat one(1) 24 | 25 | test "one expands to inner expression" do 26 | assert 1 = one() 27 | end 28 | 29 | defpat foo(bar) 30 | 31 | test "foo can bind inner variable with keyword" do 32 | assert 22 = foo(bar: 22) 33 | end 34 | 35 | test "foo can bind only variable if its not a kw" do 36 | assert 33 = foo(33) 37 | end 38 | 39 | defpat t2({a, b}) 40 | 41 | test "t2 can bind variables by name" do 42 | assert {2, 1} = t2(a: 2, b: 1) 43 | end 44 | 45 | test "t2 can bind variables by position" do 46 | assert {2, 1} = t2(2, 1) 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /test/expat_result_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Expat.ResultTest do 2 | use ExUnit.Case 3 | use Expat 4 | 5 | @moduledoc ~S""" 6 | Example Result as requested by @OvermindDL1 7 | 8 | This example defines a Union Pattern for working 9 | witk ok/error tagged tuples. 10 | 11 | A better Result type would be actually more 12 | similar to Either (see expat_either_test.exs). 13 | Just having two constructors. 14 | 15 | However, since Elixir/Erlang custom is to use 16 | tagged tuples but also the `:ok` and `:error` 17 | atoms by themselves, we are including more 18 | type constructors. 19 | 20 | Our `result` head pattern takes advantage of the 21 | fact that we are using tagged tuples or plain atoms 22 | to restrict values that can be a result. 23 | """ 24 | 25 | 26 | # Atoms or tuples having :ok/:error as first element 27 | # are considered results. 28 | defguard is_result(r) when r == :ok or r == :error or 29 | elem(r, 0) == :ok or elem(r, 0) == :error 30 | 31 | defpat (result(r) when is_result(r)) 32 | | ok_only(:ok) 33 | | error_only(:error) 34 | | ok({:ok, value}) 35 | | error({:error, reason}) 36 | | ok2({:ok, value, meta}) 37 | | error2({:error, reason, meta}) 38 | 39 | test "ok_only is just an atom" do 40 | assert :ok = ok_only!() 41 | end 42 | 43 | test "error is a tagged tuple" do 44 | assert {:error, "Fail"} = error!("Fail") 45 | end 46 | 47 | test "error2 is also a tagged tuple and valid result type" do 48 | assert {:error, _, _} = error2!(:enoent, "/file") 49 | end 50 | 51 | test "ok_only can be used to match" do 52 | expat case :ok do 53 | ok_only() -> assert :good 54 | end 55 | end 56 | 57 | test "ok can be used to match and extract" do 58 | expat case {:ok, "good"} do 59 | ok(res) -> assert "good" == res 60 | end 61 | end 62 | 63 | 64 | test "error2 can be used to match and extract" do 65 | expat case {:error, :enoent, "/file"} do 66 | error2(:enoent, file) -> assert "/file" == file 67 | end 68 | end 69 | 70 | end 71 | -------------------------------------------------------------------------------- /test/expat_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Expat.ExpatTest.Patterns do 2 | use Expat 3 | defpat list(x) when is_list(x) 4 | defpat atom(x) when is_atom(x) 5 | defpat one(1) 6 | defpat aa({a, a}) 7 | defpat t2({a, b}) 8 | defpat call({atom(name), list(meta), list(args)}) 9 | defpat var({atom(name), list(meta), atom(context)}) 10 | 11 | defpat u(_u = 1) 12 | end 13 | 14 | defmodule Expat.ExpatTest do 15 | use ExUnit.Case 16 | use Expat 17 | 18 | alias __MODULE__.Patterns, as: P 19 | import P 20 | 21 | expat def foo(t2(one(), atom(x))) do 22 | x 23 | end 24 | 25 | expat def calling(call(name)) do 26 | name 27 | end 28 | 29 | expat def variable(var(name)) do 30 | name 31 | end 32 | 33 | test "can expand nested pattern" do 34 | assert :foo = foo({1, :foo}) 35 | end 36 | 37 | test "calling home" do 38 | assert :home = calling(quote do: home()) 39 | end 40 | 41 | test "a variable name" do 42 | assert :x = variable({:x, [], nil}) 43 | end 44 | 45 | test "aa places same value twice" do 46 | assert {2, 2} = aa(2) 47 | end 48 | 49 | test "can use t2 as constructor with named vars" do 50 | assert {3, {1, 2}} = t2(b: t2(a: 1, b: 2), a: 3) 51 | end 52 | 53 | test "variables starting with _ cannot be bound" do 54 | assert 1 == u(_u: 2) 55 | end 56 | 57 | test "bang macro can be used to build data with guards" do 58 | assert :hello = atom!(:hello) 59 | end 60 | 61 | require Voting.Patterns 62 | @tag :skip 63 | test "generated documentation for pattern macro" do 64 | doc = 65 | Code.get_docs(Voting.Patterns, :docs) 66 | |> Enum.find_value(fn {{:adult, 2},_, _, _, doc} -> doc; _ -> nil end) 67 | assert doc =~ ~R/Expands the `adult` pattern/ 68 | end 69 | 70 | import P, only: [] 71 | end 72 | -------------------------------------------------------------------------------- /test/expat_union_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Expat.UnionTest do 2 | use ExUnit.Case 3 | use Expat 4 | 5 | @moduledoc ~S""" 6 | Expat Unions. 7 | 8 | Expat has an special syntax for defining pattern 9 | unions: 10 | 11 | defpat head_pattern | tail_patterns 12 | 13 | The following example (see expat_nat_test.exs) 14 | 15 | defpat foo 16 | | bar(:hello) 17 | | baz(:world) 18 | 19 | is just a syntax sugar for: 20 | 21 | defpat foo({:foo, x}) 22 | defpat bar(foo(:hello)) 23 | defpat baz(foo(:world)) 24 | 25 | Note that when the head pattern has no arguments, by default it creates 26 | a tagged tuple with its name. In this example: `{:foo, x}`. 27 | 28 | Calling any of the tail patterns will just pass arguments into the 29 | head pattern. 30 | 31 | See also: 32 | expat_nat_test.exs 33 | expat_maybe_test.exs 34 | expat_either_test.exs 35 | expat_result_test.exs 36 | 37 | """ 38 | 39 | defmodule Person do 40 | defstruct [:mood] 41 | end 42 | 43 | defpat person_in_mood(%Person{mood: mood}) 44 | | sad_person(:sad) 45 | | happy_person(:happy) 46 | 47 | test "sad yields a person" do 48 | assert %Person{mood: :sad} = sad_person() 49 | end 50 | 51 | test "happy can be used to match" do 52 | expat case %Person{mood: :happy} do 53 | happy_person() -> assert :party 54 | end 55 | end 56 | 57 | end 58 | -------------------------------------------------------------------------------- /test/foo_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Expat.FooTest do 2 | use ExUnit.Case 3 | use Expat 4 | 5 | defpat t2({a, b}) when a > b 6 | 7 | test "constructor with guard" do 8 | x = t2(2, 1, _: [build: true]) 9 | assert {2, 1} == x 10 | end 11 | 12 | test "constructor with non matching guard" do 13 | assert_raise CaseClauseError, ~R/no case clause matching: {1, 2}/, fn -> 14 | t2(1, 2, _: [build: true]) 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/readme_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Expat.ReadmeTest do 2 | use ExUnit.Case 3 | use Expat 4 | 5 | require MyPatterns 6 | import MyPatterns 7 | 8 | require Voting 9 | 10 | doctest Expat.Readme 11 | end 12 | -------------------------------------------------------------------------------- /test/support/my_patterns.ex: -------------------------------------------------------------------------------- 1 | defmodule Pet do 2 | defstruct [:name, :age, :owner, :kind] 3 | end 4 | 5 | defmodule Person do 6 | defstruct [:name, :age, :country] 7 | end 8 | 9 | defmodule MyPatterns do 10 | use Expat 11 | defpat ok({:ok, result}) 12 | defpat error({:error, reason}) 13 | 14 | defpat mexican(%Person{name: name, country: "MX"}) 15 | 16 | defpat mexican_parrot(%Pet{ 17 | kind: :parrot, 18 | name: name, 19 | age: age, 20 | owner: mexican(name: owner_name) 21 | }) 22 | end 23 | 24 | defmodule Voting.Patterns do 25 | use Expat 26 | 27 | defpat teenager(%{age: age}) when age > 9 and age < 11 28 | 29 | defpat adult(%{age: age}) when is_integer(age) and age >= 18 30 | end 31 | 32 | defmodule Voting do 33 | use Expat 34 | import MyPatterns 35 | import Voting.Patterns 36 | 37 | # our voting system is flawed, for sure. 38 | def flawed_can_vote?(mexican()), do: true 39 | 40 | expat def adult_can_vote?(mexican() = adult()) do 41 | true 42 | end 43 | 44 | import Voting.Patterns, only: [] 45 | end 46 | -------------------------------------------------------------------------------- /test/support/named.ex: -------------------------------------------------------------------------------- 1 | defmodule Expat.Test.Named do 2 | @moduledoc """ 3 | Example named patterns defined with `defpat` 4 | """ 5 | 6 | use Expat 7 | 8 | defpat one(1) 9 | defpat age_to_vote(n) when n >= 18 10 | defpat t2({a, b}) 11 | defpat foo(bar) 12 | end 13 | -------------------------------------------------------------------------------- /test/support/readme.ex: -------------------------------------------------------------------------------- 1 | defmodule Expat.Readme do 2 | @readme File.read!(Path.expand("../../README.md", __DIR__)) 3 | @external_resource @readme 4 | @moduledoc @readme 5 | end 6 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------