PatternMetonyms (pattern_metonyms v0.8.0) View Source

Reusing the description from the paper:

Pattern synonyms allow to abstract over patterns used in pattern matching, notably by allowing to use computation instead of being limited to concrete data.

Pattern metonyms are an implementations of this, but because of the limitation of the language, it can not be considered the same, so metonyms was chosen as a synonym to synonyms.

Unlike in Haskell, few metonym definitions can be used with case/2, other forms of patterns can only be used in combination with view/2.

Link to this section Summary

Functions

View with named functions

Replaces all occurences of =/2 in the do block by fit/2

=/2 equivalent to view, named form.

View with anonymous functions

Macro used to define pattern metonyms.

Macro substitute for case/2 capable of using pattern metonyms.

Link to this section Functions

Link to this macro

defv(call, expr)

View Source (macro)

View with named functions

use PatternMetonyms

defv id((Function.identity() -> x)), do: x

Replaces all occurences of =/2 in the do block by fit/2

iex> import PatternMetonyms
...> fit do
...>   (Kernel.+(1) -> x) = 1
...>   (Kernel.-(1) -> y) = 41
...> end
...> x + y
42

iex> import PatternMetonyms
...> fit(do: (Kernel.+(1) -> x) = (Kernel.-(1) -> y) = 21)
...> x + y
42
Link to this macro

fit(pat, expr)

View Source (macro)

=/2 equivalent to view, named form.

iex> import PatternMetonyms
...> pat = fn {:Just, x} -> x end
...> fit((pat.() -> n), {:Just, 3})
...> n + 1
4

Underscore prefixed variables are ignored, they are therefore unusable outside fit/2's first argument.

Because of the lack of operator with the same associativity and precedence as =/2, no operator form of fit/2 is planned. It would otherwise result in unintuitive behaviors.

Left nesting of fit/2 will result in a compilation error.

View with anonymous functions

import PatternMetonyms

id = fnv do (Function.identity() -> x) -> x end
Link to this macro

matchv?(pat, expr)

View Source (macro)

view/2's equivalent to match?/2

iex> import PatternMetonyms
...> matchv?((Function.identity() -> 1), 1)
true

iex> import PatternMetonyms
...> matchv?((Function.identity() -> {1, _}), {1, 2})
true

iex> import PatternMetonyms
...> map = %{a: 1, b: 2}
...> matchv?((Map.take([:a, :b, :c]) -> %{a: _}), map)
true

iex> import PatternMetonyms
...> map = %{a: 1, b: 2}
...> matchv?((Map.take([:b, :c]) -> %{a: _}), map)
false

iex> import PatternMetonyms
...> a = 1
...> matchv?(^a, 1)
true

iex> import PatternMetonyms
...> a = 1
...> matchv?((Function.identity() -> ^a), 1)
true

iex> import PatternMetonyms
...> list = [a: 1, b: 2, a: 3]
...> Enum.filter(list, &matchv?((Function.identity() -> {:a, _}), &1))
[a: 1, a: 3]

iex> import PatternMetonyms
...> list = [a: 1, b: 2, a: 3]
...> Enum.filter(list, &matchv?((Function.identity() -> {:a, x}) when x < 2, &1))
[a: 1]

iex> import PatternMetonyms
...> matchv?(_x, 1)
...> binding()
[]
Link to this macro

pattern(metonym)

View Source (macro)

Macro used to define pattern metonyms.

There are three types of pattern, listed below with examples:

  1. Implicitly Bidirectional

The simplest type of pattern, because of its symmetry requirement, it can only be defined using concrete data (or adapted macro). Therefore no computation is allowed, and they are thus compatible with case and function heads. They take the form:

pattern <name>(<[variables]>) = <pattern>

where pattern reuses the variables

iex> defmodule DoctestTPX do
...>   import PatternMetonyms
...>
...>   pattern ok(x)    = {:ok, x}
...>   pattern error(x) = {:error, x}
...>
...>   pattern cons(x, xs) = [x | xs]
...>
...>   def foo(x) do
...>     view x do
...>       ok(a)    -> a
...>       error(b) -> b
...>     end
...>   end
...>
...>   def bar(x) do
...>     case x do
...>       ok(a)    -> a
...>       error(b) -> b
...>     end
...>   end
...>
...>   def baz(ok(a)   ), do: a
...>   def baz(error(b)), do: b
...>
...>   def mk_ok(x), do: ok(x)
...>
...>   def blorg(xs) do
...>     view xs do
...>       cons(x, xs) -> cons(x, Enum.map(xs, fn x -> -x end))
...>     end
...>   end
...> end
iex> DoctestTPX.foo({:ok, :banana})
:banana
iex> DoctestTPX.foo({:error, :split})
:split
iex> DoctestTPX.bar({:ok, :peach})
:peach
iex> DoctestTPX.baz({:error, :melba})
:melba
iex> DoctestTPX.mk_ok(:melba)
{:ok, :melba}
iex> DoctestTPX.blorg([1, 2, 3])
[1, -2, -3]
  1. Unidirectional

This type of pattern is read only, it may be used as abstraction over pattern matching on concrete data type that can not be reused to construct data, or as abstraction over views, as explained in view/2. They take the form:

pattern <name>(<[variables]>) <- <pattern>
pattern <name>(<[variables]>) <- (<function> -> <pattern>)

where pattern reuses the variables. (function -> pattern) is called a view

iex> defmodule DoctestTPY do
...>   import PatternMetonyms
...>
...>   pattern head(x) <- [x | _]
...>
...>   pattern rev_head(x) <- (reverse() -> head(x))
...>
...>   def reverse(xs), do: Enum.reverse(xs)
...>
...>   def foo(xs) do
...>     view xs do
...>       head(x) -> x
...>       []      -> []
...>     end
...>   end
...>
...>   def bar(x) do
...>     case x do
...>       head(a) -> a
...>       []      -> []
...>     end
...>   end
...>
...>   def baz(head(a)), do: a
...>
...>   def blorg(xs) do
...>     view xs do
...>       rev_head(x) -> x
...>     end
...>   end
...> end
iex> DoctestTPY.foo([1, 2, 3])
1
iex> DoctestTPY.bar([1, 2, 3])
1
iex> DoctestTPY.baz([1, 2, 3])
1
iex> DoctestTPY.blorg([1, 2, 3])
3
  1. Explicitly bidirectional

This type of pattern allows the same kind of abstraction as unidirectional one, but also permit defining how to construct data from computation (if necessary). They take the form:

pattern (<name>(<[variables]>) <- (<function> -> <pattern>)) when <name>(<[variables]>) = <builder>

where pattern and builder reuse the variables. (function -> pattern) is called a view

iex> defmodule DoctestTPZ do
...>   import PatternMetonyms
...>
...>   pattern (snoc(x, xs) <- (unsnoc() -> {x, xs}))
...>     when snoc(x, xs) = Enum.reverse([x | Enum.reverse(xs)])
...>
...>   defp unsnoc([]), do: :error
...>   defp unsnoc(xs) do
...>     [x | rev_tail] = Enum.reverse(xs)
...>     {x, Enum.reverse(rev_tail)}
...>   end
...>
...>   def foo(xs) do
...>     view xs do
...>       snoc(x, _) -> x
...>       []      -> []
...>     end
...>   end
...>
...>   def bar(xs) do
...>     view xs do
...>       snoc(x, xs) -> snoc(-x, xs)
...>       []      -> []
...>     end
...>   end
...> end
iex> DoctestTPZ.foo([1, 2, 3])
3
iex> DoctestTPZ.bar([1, 2, 3])
[1, 2, -3]

Patterns using a view can not be used with case.

Remote function can be used within a view, but the __MODULE__ alias won't work because the expansion is not done at the usage site. It is not yet determined which behavior is desired.

iex> defmodule DoctestTPA do
...>   import PatternMetonyms
...>
...>   pattern rev_head(x) <- (Enum.reverse -> [x | _])
...>
...>   def blorg(xs) do
...>     view xs do
...>       rev_head(x) -> x
...>     end
...>   end
...> end
iex> DoctestTPA.blorg([1, 2, 3])
3

Unknown yet if anonymous functions can be supported.

Guards within a pattern definition is considered undefined behavior, it may work, but it depends on the context. Consider that if the behavior gets a specification, it would be the removal of the possibility of using them. Patterns using a view pattern are the recommend approach. For example:

pattern heart(n) <- (less_than_3 -> {:ok, n})

Patterns can be documented:

@doc """
heart matches when the number is heartfelt <3
"""
pattern heart(n) <- (less_than_3() -> {:ok, n})

You can then access the doc as usual: h heart, or h Module.heart.

Link to this macro

view(expr, clauses)

View Source (macro)

Macro substitute for case/2 capable of using pattern metonyms.

Custom case able to use pattern metonyms defined with this module. Largely unoptimized, try to avoid side effect in your pattern definitions as using them multiple time in view will repeat them, but might not later on.

View pattern ((function -> pattern)) may be used raw in here.

View patterns are simply a pair of a function associated with a pattern where the function will be applied to the data passed to view and the result will be matched with the pattern.

iex> import PatternMetonyms
iex> view self() do
...>   (is_pid() -> true) -> :ok
...>   _ -> :ko
...> end
:ok

Guards can be used outside of the view pattern or the pattern metonym.

iex> import PatternMetonyms
iex> view -3 - :rand.uniform(2) do
...>   (abs() -> x) when x > 3 -> :ok
...>   (abs() -> x) when x < 3 -> :ko
...>   x -> x
...> end
:ok

Remote calls can be used directly within a view pattern.

iex> import PatternMetonyms
iex> view :banana do
...>   (Atom.to_string() -> "ba" <> _) -> :ok
...>   _ -> :ko
...> end
:ok

Anonymous functions can be used within a view pattern. They can be either used as stored within a variable:

iex> import PatternMetonyms
iex> fun = &inspect(&1, pretty: &2)
iex> view :banana do
...>   (fun.(true) -> str) -> str
...> end
":banana"

Or defined directly using Kernel.SpecialForms.fn/1 only:

iex> import PatternMetonyms
iex> view 3 do
...>   (fn x -> x + 2 end -> n) -> n + 1
...> end
6