Coder Social home page Coder Social logo

vic / expat Goto Github PK

View Code? Open in Web Editor NEW
177.0 6.0 5.0 72 KB

Reusable, composable patterns across Elixir libraries

Home Page: https://hex.pm/packages/expat

License: Apache License 2.0

Elixir 100.00%
elixir pattern-matching composable-patterns guards named-patterns algebraic-data-types union-types patterns macros

expat's Introduction

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:

          use Expat
          import Voting.Patterns

          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
          
          # Un-import since its pattern macros
          # were used only during compilation.
          import Voting.Patterns, only: []

Guarded data constructors

As mentioned previously, if you expand a pattern and bind all of it's inner variables (provided the pattern was not defined with any _ var), then you are effectively just building data from it.

However, for patterns that include guards (or those expanding inner patterns including guards), an special bang function can be used to build data and make sure the guards are satisfied.

Bang constructors are positional, that means variables are bound in the order they appear on your named pattern.

For example, for our previous adult pattern:

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

The adult!(age) constructor will be generated.

See HOW_IT_WORKS for more info on how guards are expanded within Expat.

Union Patterns

This is an Expat feature that lets you compose many named patterns into a single union pattern. They are explained best with code, see bellow.

Using unions, you can emulate things like Algebraic data types

For some examples, see:

Documentation

Your named pattern macros will be generated with documentation about what variables they take and what they will expand to. If you are in IEx, be sure to checkout their documentation using something like: h Voting.Patterns.adult

Also, be sure to read the documentation, and checkout some of the tests.

Happy Pattern Matching!

Installation

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

expat's People

Contributors

vic avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

expat's Issues

Generated macro functions.

Following @OvermindDL1's idea:

Currently when you define a named pattern

   def var(name, meta, context) when is_atom(name) and is_list(meta) and is_atom(context)

The following macros are generated:

defmacro var()
defmacro var(bindings)
defmacro var(name, bindings)
defmacro var(name, meta, bindings)
defmacro var(name, meta, context, bindings)

var() will underscore all pattern variables not used in guard.
bindings is a Keyword of names to expressions, used to bind variables inside the pattern.
All other functions are just there for providing positional arguments (vs named ones via the last keyword)

Now, I'm thiking about adding a couple more:

defmacro var?(expr)
defmacro var!(bindings)

The first one will just check that expr conforms - that is it matches the pattern and all its guards are satisfied
and the second will raise an error unless all variables inside the patter are bound by name from bindings, it will be used for as data constructor, even for those patterns with guards.

Raise on keys for non existing variables in matches

Hi! First of all: fantastic project!

Thanks you and sharing how the lib helps my usecase

I was writing a credo check. I needed to match on "pipe operator where the second param is function application". Those kinds of patterns are pretty big:

{:|>, a,
          [b, {{:., c, [{:__aliases__, meta, [module_name]}, function_name]}, d, params}]},

but using the lib makes them more manageable:

  defpat(pipe({:>, meta, [before_pipe, after_pipe]} = ast))

  defpat(
    function_call(
      {{:., dot_meta, [{:__aliases__, module_meta, [module_name]}, function_name]}, params_meta,
       params} = ast
    )
  )
  defp traverse(
         pipe(
           meta: a,
           before_pipe: b,
           after_pipe:
             function_call(
               dot_meta: c,
               module_meta: meta,
               module_name: module_name,
               function_name: function_name,
               params_meta: d,
               params: params
             )
         ),
         ...) do

Thank you very much for making composing pattern matches easier! I wanted to share with you how much easier it makes writing those checks :)

Actual issue

If I make a pattern like this:

  defpat ok({:ok, val})

but then misuse it like this:

  def my_fun(ok(value: value)) do
    value
  end

Notice that the binding is called val but used :value as the key in the keyword list. The error I get is:

warning: variable "value" does not exist and is being expanded to "value()", please use parentheses to remove the ambiguity or change the variable name

If I understand correctly, it means that the key value is ignored when there is no binding called this way. It might be a little bit confusing in case of typos to understand what is happening.

Would you consider it a good idea to raise in case usage of a given pattern tries to access non-existent binding?

If yes, I could try digging into it over the weekend and checking if I understand the lib enough to make that change.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.