Expat v1.0.1 Expat

Expat - Reusable, composable patterns in Elixir.

Travis Hex.pm

About

Expat is a library for creating composable pattern matchers.

That means, whenever you find yourself writing complex or long patterns in your functions, expat can be handy by allowing you to split your pattern into re-usable and composable bits.

These named pattern matchers defined with expat can be used, for example, to match over large phoenix parameters and keep your action definitions short and concise. Since programmers read code all the time, their code should be optimized for communicating their intent, so instead of having your brain to parse all the way down the large structure pattern it would be better to abstract that pattern with a name.

Also, as patterns get abstracted and split into re-usable pieces they could be exported so other libraries (or your own umbrella applications) can communicate the rules for matching data being passed between them.

To read more about the motivation and where this library comes from, you can read the v0 README

use Expat

Named Patterns

Let’s start with some basic data examples. In Erlang/Elixir it’s very common to use tagged tuples to communicate between functions. For example, a function that can fail might return {:error, reason} or {:ok, result}.

Of course these two element tuples are so small, that most of the time it’s better to use them as they communicate the intent they are being used for.

But, using them can help us understand the basics of how expat works, just remember that expat takes patterns, and is not limited to some particular data structure.

    defmodule MyPatterns do
      use Expat

      defpat ok({:ok, result})
      defpat error({:error, reason})
    end

So, just like you’d be able to use {:ok, result} = expr to match some expression, you can give the name ok to the {:ok, result} pattern.

Later on, at some other module, you can use those named patterns.

     iex> import MyPatterns
     iex> Kernel.match?(ok(), {:ok, :hey})
     true

In the previous example, the ok() macro actually expanded to:

     iex> Kernel.match?({:ok, _}, {:ok, :hey})
     true

Notice that even when the ok pattern definition says it has an inner result, we didn’t actually were interested in it, so ok() just ensures the data is matched with the structure mandated by its pattern and didn’t bind any variable for us.

If we do need access to some of the pattern variables, we can bind them by giving the pattern a Keyword of names to variables, for example:

     # One nice thing about expat is you can use your patterns
     # anywhere you can currently write one, like in tests
     iex> assert error(reason: x) = {:error, "does not exist"}
     iex> x
     "does not exist"

And of course, if you bind all the variables in a pattern, you can use its macro as a data constructor, for example:

     iex> ok(result: "done")
     {:ok, "done"}

That’s it for our tagged tuples example.

Combining patterns

Now we know the basics of how to define and use named patterns, let’s see how we can combine them to form larger patterns.

Let’s use some structs instead of tuples, as that might be a more common use case.

     defmodule Pet do
        defstruct [:name, :age, :owner, :kind]
     end

     defmodule Person do
        defstruct [:name, :age, :country]
     end

     defmodule MyPatterns do
       use Expat

       defpat mexican(%Person{name: name, country: "MX"})

       defpat mexican_parrot(%Pet{kind: :parrot, name: name,  age: age,
                                     owner: mexican(name: owner_name)})
     end

     iex> vic  = %Person{name: "vic", country: "MX"}
     ...> milo = %Pet{kind: :parrot, name: "Milo", owner: vic, age: 4}
     ...>
     ...> # here, we are only interested in the owner's name
     ...> mexican_parrot(owner_name: name) = milo
     ...> name
     "vic"

And again, if you bind all the variables, it could be used as a data constructor

     iex> mexican_parrot(age: 1, name: "Venus", owner_name: "Alicia")
     %Pet{kind: :parrot, name: "Venus", age: 1, owner: %Person{country: "MX", name: "Alicia", age: nil}}

Then you could use those patterns in a module of yours

      defmodule Feed do
         import MyPatterns

         def with_mexican_food(bird = mexican_parrot(name: name, owner_name: owner)) do
           "#{name} is happy now!, thank you #{owner}"
         end
      end

And the function head will actually match using the whole composite pattern, and only bind those fields you are interested in using.

Guarding patterns

Since expat v1.0 it’s now possible to use guards on your pattern definitions, and they will be expanded at the call-site.

For example, let’s build this year’s flawed election system.

      defmodule Voting.Patterns do
        use Expat

        defpat mexican(%Person{country: "MX"})

        defpat adult(%{age: age}) when is_integer(age) and age >= 18
      end

Notice that the adult pattern matches anything with an integer age greater than 18 years (mexico’s legal age to vote) by using when guards on the definition.

Notice the expat def can_vote? part in the following code:

       defmodule Voting do
          use Expat
          import Voting.Patterns
          
          def is_local?(mexican()), do: true
          def is_local?(_), do: false
          
          expat def can_vote?(mexican() = adult()), do: true
          def can_vote?(_), do: false
       end

expat stands for expand pattern in the following expression, and expand their guards in the correct place.

So our can_vote? function checks that the data given to it looks like a mexican and also (since we are =ing two patterns), that the data represents an adult with legal age to vote by using guards.

expat will work for def, defmacro, their private variants, case, and fn.

Actually you can give any expression into expat. And your patterns will be expanded correctly within it.

For example, the previous module could be written like:

          # Since expat works at compile time, it and your pattern
          # macros need to be available if you want to expand them.
          use Expat, import: {Voting.Patterns, [:mexican, :adult]}

          expat defmodule Voting do

            def is_local?(mexican()), do: true
            def is_local?(_), do: false

            def can_vote?(mexican() = adult()), do: true
            def can_vote?(_), do: false
          end

Be sure to read the documentation and look at some of the tests.

Installation

def deps do
  [
    {:expat, "~> 1.0"}
  ]
end

Link to this section Summary

Functions

Imports defpat and expat into scope

Define a new named pattern

Same as defpat but defines private patterns

Expand an expression using named patterns

Link to this section Types

Link to this type guarded_pattern()
guarded_pattern() :: {:when, list(), [simple_call(), ...]}
Link to this type pattern()
pattern() :: simple_call() | guarded_pattern()
Link to this type simple_call()
simple_call() :: {atom(), keyword(), [Macro.t()]}

Link to this section Functions

Link to this macro __using__(list) (macro)

Imports defpat and expat into scope.

use Expat

Since named patterns are just macros, they must be required and in scope at compile for using them. For this reason, the following syntax can be used to automatically require MyPatterns and import a list of named patterns from it.

use Expat, import: {MyPatterns, [:age_to_vote]}

This has the advantage of importing Expat and requiring the module (because we are going to use their macros) and importing just the given named patterns, because all the generated pattern macros have the same arity, they can be imported in a single step.

Link to this macro defpat(pattern) (macro)
defpat(pattern()) :: Macro.t()

Define a new named pattern.

This function takes only the function head as argument. You may also specify a guard, but never a do block.

Variables present in the function head can be later bound. Guards if any are also expanded at call site, for example in def, case, fn expressions. See expat/1 for more.

Examples

defpat person(%Person{name: name})
defpat adult(%{age: age}) when age > 18
Link to this macro defpatp(pattern) (macro)
defpatp(pattern()) :: Macro.t()

Same as defpat but defines private patterns

Link to this macro expat(ast) (macro)
expat(Macro.t()) :: Macro.t()

Expand an expression using named patterns.

expat stands for expand pattern in an expression. It’s also the name of the library :).

Note that for this to work, the macros that define the named patterns should already have been compiled and in scope. For this reason, most of the time, named patterns should be defined on separate modules and imported for use.

Example

You define a module for your named patterns

defmodule MyPatterns do
  use Expat

  @doc "Matches when n is legal age to vote"
  defpat adult_age(n) when n > 18
end

Then you can import it and use it’s macros

defmodule Foo do
   use Expat
   import MyPatterns

   def foo(x) do
     # Tell expat that we want the case
     # clauses being able to use guards
     # from the named pattern.
     #
     # foo(20) => :vote
     #
     expat case x do
       adult_age() -> :vote
     end
   end

   # You can also use expat at the `def`
   # level (or defp, defmacro, etc)
   #
   # In this case, we are asking expat to
   # also expand the named patterns it
   # sees on our function head, and the
   # guards it produces are added to our
   # function definition.
   #
   # vote(20) => {:voted, 20}
   # vote(20) => no function match error
   #
   expat def vote(adult_age(n: x)) do
     {:voted, x}
   end
end

You can even use expat only once at the module level, then all it’s def, case, fn, … will be able to use named patterns.

 use Expat
 import MyPatterns, only: [adult_age: 1]

 expat defmodule Ellections do

    def vote(adult_age(n: x)) do
      {:ok, x}
    end

    def vote(_), do: :error
 end