├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── lib └── anaphora.ex ├── mix.exs └── test ├── anaphora_test.exs └── test_helper.exs /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /deps 3 | erl_crash.dump 4 | *.ez 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: elixir 2 | elixir: 3 | - 0.15.0 4 | - 1.0.0 5 | otp_release: 6 | - 17.1 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Sviridov Alexander 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Anaphora [![Build Status](https://travis-ci.org/sviridov/anaphora-elixir.svg)](https://travis-ci.org/sviridov/anaphora-elixir) [![license](http://img.shields.io/badge/license-MIT-brightgreen.svg)](https://github.com/sviridov/anaphora-elixir/blob/master/LICENSE) [![hex.pm](http://img.shields.io/badge/hex.pm-0.1.2-brightgreen.svg)](https://hex.pm/packages/anaphora) 2 | 3 | `Anaphora` is the anaphoric macro collection for [Elixir](https://github.com/elixir-lang/elixir/). An anaphoric macro is one that deliberately captures a variable (typically `it`) from forms supplied to the macro. 4 | 5 | ### Getting Started 6 | 7 | - Add the `Anaphora` dependency to your `mix.exs` file: 8 | 9 | ```elixir 10 | def deps do 11 | [{:anaphora, "~> 0.1.2"}] 12 | end 13 | ``` 14 | 15 | - After you are done, run `mix deps.get` in your shell. 16 | 17 | ### Provided API 18 | 19 | #### alet 20 | 21 | `alet` is basic anaphoric macro. It's not very useful in user code but other anaphoric macros are built on top of it. `alet` binds result of the expression to the `it` variable (via `case`) in the scope of the body: 22 | 23 | ```elixir 24 | defmodule User do 25 | use Anaphora 26 | 27 | ... 28 | def user_email(user_id) do 29 | alet fetch_user(user_id) do 30 | if it, do: it.email, else: raise "Failed to fetch user" 31 | end 32 | end 33 | end 34 | ``` 35 | 36 | #### aif 37 | 38 | Works like `if`, except that result of the condition is bound to `it` (via `alet`) for the scope of the then and else clauses: 39 | 40 | ```elixir 41 | defmodule User do 42 | use Anaphora 43 | 44 | ... 45 | def user_email(user_id) do 46 | aif fetch_user(user_id), do: it.email, else: raise "Failed to fetch user" 47 | end 48 | end 49 | ``` 50 | 51 | #### acond 52 | 53 | Works like `cond`, except that result of each condition is bound to `it` (via `alet`) for the scope of the corresponding body: 54 | 55 | ```elixir 56 | defmodule Notification do 57 | use Anaphora 58 | 59 | ... 60 | def send_notification(user, message) do 61 | acond do 62 | user.email -> send_notification_by_email(it, message) 63 | user.phone -> send_notification_by_sms(it, message) 64 | :else -> raise "Unable to send notification" 65 | end 66 | end 67 | end 68 | ``` 69 | 70 | #### acase 71 | 72 | Works like `case`, except that result of the key expression is bound to `it` (via `alet`) for the scope of the cases. 73 | 74 | #### afn 75 | 76 | Works like `fn`, except that anonymous function is bound to `it` (via `blood magic`) for the scope of the function body: 77 | 78 | ```elixir 79 | import Anaphora 80 | 81 | in_order_tree_traversal = afn do 82 | {left, center, right}, callback -> 83 | it.(left, callback) 84 | callback.(center) 85 | it.(right, callback) 86 | nil, _ -> nil 87 | end 88 | 89 | in_order_tree_traversal.({{nil, 1, nil}, 2, {nil, 3, {nil, 4, nil}}}, &IO.puts/1) 90 | # => 1, 2, 3, 4 91 | ``` 92 | 93 | **warning:** Invocation of `it` takes a lot of time. Don't use `afn` in performance critical code. It will be fixed after [Elixir](https://github.com/elixir-lang/elixir/) `1.0` release. 94 | 95 | #### aand 96 | 97 | Evaluates each clause one at a time and binds result to `it`. As soon as any clause evaluates to `nil` (or `false`), `aand` returns `nil` without evaluating the remaining clauses. If all clauses but the last evaluate to true values, `aand` returns the results produced by evaluating the last clause: 98 | 99 | ```elixir 100 | defmodule Notification do 101 | use Anaphora 102 | 103 | ... 104 | def send_email_to_user(user_id, message) do 105 | aand do 106 | fetch_user(user_id) 107 | it.email 108 | send_email(it, message) 109 | end || raise "Unable to send email" 110 | end 111 | end 112 | ``` 113 | 114 | ### License 115 | 116 | Anaphora source code is released under The MIT License. Check LICENSE file for more information. 117 | -------------------------------------------------------------------------------- /lib/anaphora.ex: -------------------------------------------------------------------------------- 1 | defmodule Anaphora do 2 | @vsn "0.1.2" 3 | 4 | @moduledoc """ 5 | The anaphoric macro collection for Elixir 6 | """ 7 | 8 | defmacro __using__(_) do 9 | quote do 10 | import Anaphora 11 | end 12 | end 13 | 14 | ## Returns the `it` variable defined in user context 15 | defp it do 16 | Macro.var(:it, nil) 17 | end 18 | 19 | @doc """ 20 | Binds the `expression` to `it` (via `case`) in the scope of the `body` 21 | 22 | ## Examples 23 | 24 | iex> Anaphora.alet 2 * 2 + 2, do: it / 2 25 | 3.0 26 | 27 | iex> Anaphora.alet tl([1, 2, 3]) do 28 | ...> hd(it) # do some staff 29 | ...> tl(it) 30 | ...> end 31 | [3] 32 | 33 | """ 34 | defmacro alet(expression, do: body) do 35 | quote do 36 | case unquote(expression) do 37 | unquote(it) -> unquote(body) 38 | end 39 | end 40 | end 41 | 42 | @doc """ 43 | Works like `if`, except that result of the `condition` is bound to `it` (via `alet`) for the 44 | scope of the then and else `clauses` 45 | 46 | ## Examples 47 | 48 | iex> Anaphora.aif :aif_test, do: it 49 | :aif_test 50 | 51 | iex> Anaphora.aif 2 * 2 + 2 do 52 | ...> it / 2 53 | ...> else 54 | ...> :never 55 | ...> end 56 | 3.0 57 | 58 | iex> Anaphora.aif 1 == 2, do: :never, else: it 59 | false 60 | 61 | """ 62 | defmacro aif(condition, clauses) do 63 | quote do 64 | Anaphora.alet unquote(condition) do 65 | if(unquote(it), unquote(clauses)) 66 | end 67 | end 68 | end 69 | 70 | @doc """ 71 | Works like `cond`, except that result of each `condition` is bound to `it` (via `alet`) for the 72 | scope of the corresponding `body` 73 | 74 | ## Examples 75 | 76 | iex> Anaphora.acond do 77 | ...> :acond_test -> it 78 | ...> end 79 | :acond_test 80 | 81 | iex> Anaphora.acond do 82 | ...> 1 + 2 == 4 -> :never 83 | ...> false -> :never 84 | ...> 2 * 2 + 2 -> 85 | ...> it * 2 # do some staff 86 | ...> it / 2 87 | ...> true -> :never 88 | ...> end 89 | 3.0 90 | 91 | iex> Anaphora.acond do 92 | ...> false -> :never 93 | ...> end 94 | nil 95 | 96 | """ 97 | defmacro acond(clauses) 98 | defmacro acond(do: []), do: nil 99 | defmacro acond(do: clauses) do 100 | clauses |> Enum.reverse |> Enum.reduce(nil, &expand_acond_clause/2) 101 | end 102 | 103 | defp expand_acond_clause({:->, _c, [[condition], then_body]}, else_body) do 104 | quote do 105 | Anaphora.aif unquote(condition), do: unquote(then_body), else: unquote(else_body) 106 | end 107 | end 108 | 109 | @doc """ 110 | Works like `case`, except that result of the `key` expression is bound to `it` (via `alet`) for the 111 | scope of the `cases` 112 | 113 | ## Examples 114 | 115 | iex> Anaphora.acase :acase_test do 116 | ...> :acase_test -> it 117 | ...> end 118 | :acase_test 119 | 120 | iex> Anaphora.acase [1, 2, 3] do 121 | ...> {a, b, c} -> :never 122 | ...> [1 | tale] -> it -- tale 123 | ...> _ -> :never 124 | ...> end 125 | [1] 126 | 127 | iex> try do 128 | ...> Anaphora.acase true do 129 | ...> false -> :never 130 | ...> end 131 | ...> rescue 132 | ...> _e in CaseClauseError -> :error 133 | ...> end 134 | :error 135 | 136 | """ 137 | defmacro acase(key, do: cases) do 138 | quote do 139 | Anaphora.alet unquote(key) do 140 | case unquote(it) do 141 | unquote(cases) 142 | end 143 | end 144 | end 145 | end 146 | 147 | @doc """ 148 | Evaluates each `clause` one at a time and binds result to `it`. As soon as any `clause` 149 | evaluates to `nil` (or `false`), `aand` returns `nil` without evaluating the remaining 150 | `clauses`. If all `clauses` but the last evaluate to true values, `aand` returns the 151 | results produced by evaluating the last `clause` 152 | 153 | ## Examples 154 | 155 | iex> Anaphora.aand do 156 | ...> end 157 | true 158 | 159 | iex> Anaphora.aand do 160 | ...> :aand_test 161 | ...> end 162 | :aand_test 163 | 164 | iex> Anaphora.aand do 165 | ...> 2 + 3 166 | ...> 1 + it + 4 167 | ...> it * 20 168 | ...> end 169 | 200 170 | 171 | iex> Anaphora.aand do 172 | ...> 1 == 2 173 | ...> !it 174 | ...> end 175 | nil 176 | 177 | """ 178 | defmacro aand(clauses) 179 | defmacro aand(do: nil), do: true 180 | defmacro aand(do: {:__block__, _c, clauses}) do 181 | clauses |> Enum.reverse |> Enum.reduce(&expand_aand_clause/2) 182 | end 183 | defmacro aand(do: expression), do: expression 184 | 185 | defp expand_aand_clause(clause, body) do 186 | quote do 187 | Anaphora.aif unquote(clause), do: unquote(body) 188 | end 189 | end 190 | 191 | @doc """ 192 | Works like `fn`, except that anonymous function is bound to `it` (via `blood magic`) 193 | 194 | ## Examples 195 | 196 | iex> fact = Anaphora.afn do 197 | ...> 0 -> 1 198 | ...> 1 -> 1 199 | ...> n when n > 0 -> n * it.(n - 1) 200 | ...> end 201 | ...> fact.(5) 202 | 120 203 | 204 | iex> fib = Anaphora.afn do 205 | ...> 0 -> 1 206 | ...> 1 -> 1 207 | ...> n when n > 0 -> it.(n - 1) + it.(n - 2) 208 | ...> end 209 | ...> Enum.map(1..7, fib) 210 | [1, 2, 3, 5, 8, 13, 21] 211 | 212 | iex> (Anaphora.afn do 213 | ...> x, y when x > 0 -> x + it.(x - 1, y) 214 | ...> 0, y when y > 0 -> y + it.(0, y - 1) 215 | ...> 0, 0 -> 0 216 | ...> end).(2, 4) 217 | 13 218 | 219 | """ 220 | defmacro afn(do: definitions) do 221 | ys = generate_z_combinator_ys(hd(definitions)) 222 | 223 | # λx.f (λys.((x x) ys)) 224 | lambda_x = 225 | quote do: fn x -> f.(fn unquote_splicing(ys) -> (x.(x)).(unquote_splicing(ys)) end) end 226 | 227 | lambda_f = 228 | quote do: fn f -> (unquote(lambda_x)).(unquote(lambda_x)) end 229 | 230 | lambda_it = 231 | quote do: fn unquote(it) -> unquote({:fn, [], definitions}) end 232 | 233 | quote do: (unquote(lambda_f)).(unquote(lambda_it)) 234 | end 235 | 236 | defp generate_z_combinator_ys({:->, _c, [arguments, _body]}) do 237 | 1..number_of_afn_arguments(arguments) 238 | |> Enum.map(&(Macro.var(String.to_atom("y#{&1}"), __MODULE__))) 239 | end 240 | 241 | defp number_of_afn_arguments([{:when, _c, arguments}]), do: Enum.count(arguments) - 1 242 | defp number_of_afn_arguments(arguments), do: Enum.count(arguments) 243 | end 244 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Anaphora.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :anaphora, 6 | version: "0.1.2", 7 | deps: deps, 8 | package: package, 9 | description: description] 10 | end 11 | 12 | def application do 13 | [] 14 | end 15 | 16 | defp deps do 17 | [] 18 | end 19 | 20 | defp description do 21 | """ 22 | The anaphoric macro collection for Elixir 23 | """ 24 | end 25 | 26 | defp package do 27 | [maintainers: ["Alexander Sviridov"], 28 | licenses: ["The MIT License"], 29 | links: %{"Github" => "https://github.com/sviridov/anaphora-elixir"}] 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /test/anaphora_test.exs: -------------------------------------------------------------------------------- 1 | defmodule AnaphoraTest do 2 | use ExUnit.Case 3 | use Anaphora 4 | 5 | test "__using__" do 6 | assert(acond do 7 | 2 < 1 -> :never 8 | "Test " -> it <> "__using__" 9 | 1 < 2 -> :never 10 | end == "Test __using__") 11 | end 12 | 13 | doctest Anaphora 14 | end 15 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------