├── .formatter.exs ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── lib └── capture_pipe.ex ├── mix.exs ├── mix.lock └── test ├── capture_pipe_test.exs └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.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 third-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 | capture_pipe-*.tar 24 | 25 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: bionic 2 | language: elixir 3 | matrix: 4 | include: 5 | - otp_release: 22.0 6 | elixir: 1.9 7 | - otp_release: 22.0 8 | elixir: 1.10 9 | 10 | cache: 11 | directories: 12 | - ~/.mix 13 | - ~/.hex 14 | - _build 15 | 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Qqwy / Wiebe-Marten 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CapturePipe 2 | 3 | 4 | [![hex.pm version](https://img.shields.io/hexpm/v/capture_pipe.svg)](https://hex.pm/packages/capture_pipe) 5 | [![Build Status](https://travis-ci.com/Qqwy/elixir-capture_pipe.svg?branch=master)](https://travis-ci.com/Qqwy/elixir-capture_pipe) 6 | [![Documentation](https://img.shields.io/badge/hexdocs-latest-blue.svg)](https://hexdocs.pm/capture_pipe/index.html) 7 | 8 | CapturePipe exposes an extended pipe-operator that allows the usage of bare function captures. 9 | 10 | This is useful to insert the pipe's results into a datastructure 11 | such as a tuple. 12 | 13 | What this macro does, is if it encounters a `&` capture, 14 | it wraps the whole operand in `(...).()` which is the 15 | anonymous-function-call syntax that Elixir's normal pipe accepts, 16 | that (argubably) is less easy on the eyes. 17 | 18 | For instance, `10 |> &{:ok, &1}` is turned into `10 |> (&{:ok, &1}).()` 19 | 20 | ## Examples 21 | 22 | Still works as normal: 23 | 24 | ```elixir 25 | iex> [1,2,3] |> Enum.map(fn x -> x + 1 end) 26 | [2,3,4] 27 | ``` 28 | 29 | Insert the result of an operation into a tuple 30 | 31 | ```elixir 32 | iex> 42 |> &{:ok, &1} 33 | {:ok, 42} 34 | ``` 35 | 36 | It also works multiple times in a row. 37 | 38 | ```elixir 39 | iex> 20 |> &{:ok, &1} |> &[&1, 2, 3] 40 | [{:ok, 20}, 2, 3] 41 | ``` 42 | 43 | Besides the function-capture syntax 44 | CapturePipe also enables you to use anonymnous functions 45 | directly inside a pipe, performing similar wrapping: 46 | 47 | ```elixir 48 | iex> 42 |> fn val -> to_string(val) end 49 | "42" 50 | ``` 51 | 52 | Even if the pipes are nested deeply 53 | and interspersed with 'normal' pipe calls: 54 | 55 | ```elixir 56 | iex> (10 57 | iex> |> &Kernel.div(20, &1) 58 | iex> |> Kernel.-() 59 | iex> |> to_string() 60 | iex> |> &"The answer is: \#{&1}!" 61 | iex> |> String.upcase() 62 | iex> |> &{:ok, &1} 63 | iex> ) 64 | {:ok, "THE ANSWER IS: -2!"} 65 | ``` 66 | 67 | 68 | ## Installation 69 | 70 | Capturepipe can be installed 71 | by adding `capture_pipe` to your list of dependencies in `mix.exs`: 72 | 73 | ```elixir 74 | def deps do 75 | [ 76 | {:capture_pipe, "~> 0.1.0"} 77 | ] 78 | end 79 | ``` 80 | 81 | Documentation can be found at [https://hexdocs.pm/capture_pipe](https://hexdocs.pm/capture_pipe). 82 | 83 | -------------------------------------------------------------------------------- /lib/capture_pipe.ex: -------------------------------------------------------------------------------- 1 | defmodule CapturePipe do 2 | import Kernel, except: [|>: 2] 3 | @moduledoc """ 4 | To use this operator in one of your modules, you need to add the following to it: 5 | 6 | use CapturePipe 7 | 8 | This does the same thing as explicitly writing: 9 | 10 | import Capturepipe 11 | import Kernel, except: [|>: 2] 12 | """ 13 | 14 | defmacro __using__(_opts) do 15 | quote do 16 | import CapturePipe 17 | import Kernel, except: [|>: 2] 18 | end 19 | end 20 | 21 | @doc """ 22 | Extended pipe-operator that allows the usage of bare function captures. 23 | 24 | This is useful to insert the pipe's results into a datastructure 25 | such as a tuple. 26 | 27 | What this macro does, is if it encounters a `&` capture, 28 | it wraps the whole operand in `(...).()` which is the 29 | anonymous-function-call syntax that Elixir's normal pipe accepts, 30 | that (argubably) is less easy on the eyes. 31 | 32 | For instance, `10 |> &{:ok, &1}` is turned into `10 |> (&{:ok, &1}).()` 33 | 34 | ## Examples 35 | 36 | Still works as normal: 37 | 38 | iex> [1,2,3] |> Enum.map(fn x -> x + 1 end) 39 | [2,3,4] 40 | 41 | Insert the result of an operation into a tuple 42 | 43 | iex> 42 |> &{:ok, &1} 44 | {:ok, 42} 45 | 46 | It also works multiple times in a row. 47 | 48 | iex> 20 |> &{:ok, &1} |> &[&1, 2, 3] 49 | [{:ok, 20}, 2, 3] 50 | 51 | Besides the function-capture syntax 52 | CapturePipe also enables you to use anonymnous functions 53 | directly inside a pipe, performing similar wrapping: 54 | 55 | iex> 42 |> fn val -> to_string(val) end 56 | "42" 57 | 58 | Even if the pipes are nested deeply 59 | and interspersed with 'normal' pipe calls: 60 | 61 | iex> (10 62 | iex> |> &Kernel.div(20, &1) 63 | iex> |> Kernel.-() 64 | iex> |> to_string() 65 | iex> |> &"The answer is: \#{&1}!" 66 | iex> |> String.upcase() 67 | iex> |> &{:ok, &1} 68 | iex> ) 69 | {:ok, "THE ANSWER IS: -2!"} 70 | 71 | Currently, the implementation raises errors when 72 | captures or anonymous functions with a bad arity are used 73 | _at runtime_. This might be improved in the future 74 | to raise compile-time errors whenever possible instead. 75 | """ 76 | # The implementation is identical to 77 | # Elixir's builtin one, 78 | # except that we are using our overridden 79 | # `pipe` and `unpipe` functions. 80 | # (rather than the ones in the `Macro` module) 81 | defmacro left |> right do 82 | [{h, _} | t] = unpipe({:|>, [], [left, right]}) 83 | 84 | fun = fn {x, pos}, acc -> 85 | pipe(acc, x, pos) 86 | end 87 | 88 | :lists.foldl(fun, h, t) 89 | end 90 | 91 | # Implementation is _very_ similar to Elixir's builtin 92 | # `Macro.unpipe/1` 93 | defp unpipe(expr) do 94 | :lists.reverse(unpipe(expr, [])) 95 | end 96 | 97 | defp unpipe({:|>, _, [left, right]}, acc) do 98 | unpipe(right, unpipe(left, acc)) 99 | end 100 | 101 | # The magic happens here: 102 | # We rewrite `a |> (&b |> c)` 103 | # into `a |> (&b) |> c` 104 | # and then recurse 105 | defp unpipe({:&, meta1, [{:|>, meta2, [left, right]}]}, acc) do 106 | unpipe({:|>, meta2, [{:&, meta1, [left]}, right]}, acc) 107 | end 108 | 109 | defp unpipe(other, acc) do 110 | [{other, 0} | acc] 111 | end 112 | 113 | # Bare captures are wrapped to become 114 | # (&(...)).() 115 | defp pipe(expr, {:&, _, _} = call_args, position) do 116 | pipe(expr, quote do (unquote(call_args)).() end, position) 117 | end 118 | 119 | defp pipe(expr, {:fn, _ ,_} = call_args, position) do 120 | pipe(expr, quote do (unquote(call_args)).() end, position) 121 | end 122 | 123 | # Defer the rest of the implementation 124 | # to the one that already ships with Elixir 125 | defp pipe(expr, other, position) do 126 | Macro.pipe(expr, other, position) 127 | end 128 | end 129 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule CapturePipe.MixProject do 2 | use Mix.Project 3 | 4 | @source_url "https://github.com/Qqwy/elixir-capture_pipe" 5 | 6 | def project do 7 | [ 8 | app: :capture_pipe, 9 | version: "0.1.0", 10 | elixir: "~> 1.9", 11 | start_permanent: Mix.env() == :prod, 12 | deps: deps(), 13 | description: description(), 14 | name: "CapturePipe", 15 | package: package(), 16 | source_url: @source_url, 17 | homepage_url: "https://github.com/Qqwy/elixir-capture_pipe", 18 | ] 19 | end 20 | 21 | # Run "mix help compile.app" to learn about applications. 22 | def application do 23 | [ 24 | extra_applications: [:logger] 25 | ] 26 | end 27 | 28 | # Run "mix help deps" to learn about dependencies. 29 | defp deps do 30 | [ 31 | # {:dep_from_hexpm, "~> 0.3.0"}, 32 | # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} 33 | {:ex_doc, "~> 0.22", only: :dev, runtime: false}, 34 | ] 35 | end 36 | 37 | 38 | defp description do 39 | """ 40 | CapturePipe exposes an extended pipe-operator that allows the usage of bare function captures. 41 | """ 42 | end 43 | 44 | defp package do 45 | [ 46 | name: :capture_pipe, 47 | files: ["lib", "mix.exs", "README*", "LICENSE"], 48 | maintainers: ["Wiebe-Marten Wijnja/Qqwy"], 49 | licenses: ["MIT"], 50 | links: %{"GitHub" => @source_url} 51 | ] 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "earmark_parser": {:hex, :earmark_parser, "1.4.10", "6603d7a603b9c18d3d20db69921527f82ef09990885ed7525003c7fe7dc86c56", [:mix], [], "hexpm", "8e2d5370b732385db2c9b22215c3f59c84ac7dda7ed7e544d7c459496ae519c0"}, 3 | "ex_doc": {:hex, :ex_doc, "0.22.2", "03a2a58bdd2ba0d83d004507c4ee113b9c521956938298eba16e55cc4aba4a6c", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "cf60e1b3e2efe317095b6bb79651f83a2c1b3edcb4d319c421d7fcda8b3aff26"}, 4 | "makeup": {:hex, :makeup, "1.0.3", "e339e2f766d12e7260e6672dd4047405963c5ec99661abdc432e6ec67d29ef95", [:mix], [{:nimble_parsec, "~> 0.5", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "2e9b4996d11832947731f7608fed7ad2f9443011b3b479ae288011265cdd3dad"}, 5 | "makeup_elixir": {:hex, :makeup_elixir, "0.14.1", "4f0e96847c63c17841d42c08107405a005a2680eb9c7ccadfd757bd31dabccfb", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f2438b1a80eaec9ede832b5c41cd4f373b38fd7aa33e3b22d9db79e640cbde11"}, 6 | "nimble_parsec": {:hex, :nimble_parsec, "0.6.0", "32111b3bf39137144abd7ba1cce0914533b2d16ef35e8abc5ec8be6122944263", [:mix], [], "hexpm", "27eac315a94909d4dc68bc07a4a83e06c8379237c5ea528a9acff4ca1c873c52"}, 7 | } 8 | -------------------------------------------------------------------------------- /test/capture_pipe_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CapturePipeTest do 2 | use ExUnit.Case 3 | 4 | import CapturePipe 5 | import Kernel, except: [|>: 2] 6 | doctest CapturePipe 7 | 8 | describe "Backwards compatibility with existing Elixir code" do 9 | test "We can still assign a capture containing a pipe to a variable" do 10 | x = & &1 |> to_string() 11 | 12 | assert x.(10) == "10" 13 | end 14 | end 15 | 16 | 17 | describe "More complicated sequences of pipes with captures" do 18 | def speak(name, greeting) do 19 | name 20 | # * function that requires different order 21 | |> &greet(greeting, &1) 22 | # regular function still works fine 23 | |> String.upcase() 24 | # does not disrupt adding ellipses via original method 25 | |> (&(&1 <> "...")).() 26 | # * example of adding ellipses with enhanced pipe 27 | # could also be &("..." <> &1) if you prefer 28 | |> &"...#{&1}" 29 | # * returning a tuple 30 | |> &{:ok, &1} 31 | end 32 | 33 | defp greet(greeting, name) do 34 | "#{greeting}, #{name}" 35 | end 36 | 37 | test "capture pipe usage" do 38 | assert speak("Jane", "Hello") == {:ok, "...HELLO, JANE..."} 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------