• Stars
    star
    175
  • Rank 218,059 (Top 5 %)
  • Language
    Elixir
  • License
    Apache License 2.0
  • Created almost 8 years ago
  • Updated over 1 year ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Reusable, composable patterns across Elixir libraries

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

More Repositories

1

params

Easy parameters validation/casting with Ecto.Schema, akin to Rails' strong parameters.
Elixir
356
star
2

ok_jose

Pipe elixir functions that match ok/error tuples or custom patterns.
Elixir
96
star
3

apollo-phoenix-websocket

An Apollo networkInterface for executing GraphQL queries via Phoenix Channels
JavaScript
91
star
4

typhon

Snakes on rbx-head. A Python implementation for the Rubinius VM
Ruby
83
star
5

color-theme-buffer-local

Set emacs color themes by buffer.
Emacs Lisp
78
star
6

spec

Data specification conformance and generation for Elixir
Elixir
77
star
7

mk-darwin-system

Small Nix utility to create an M1 aarch64-darwin (nixFlakes + nix-darwin + home-manager) system.
Nix
69
star
8

happy

the alchemist's happy path with elixir
Elixir
44
star
9

comeonin_ecto_password

Ecto type for saving encrypted passwords using Comeonin
Elixir
35
star
10

pipe_here

An Elixir macro for easily piping arguments at any position.
Elixir
33
star
11

happy_with

Avoid commas on Elixir's with special form.
Elixir
30
star
12

pit

Elixir macro for extracting or transforming values inside a pipe flow.
Elixir
27
star
13

pond

State aware Elixir functions without spawning processes
Elixir
27
star
14

asdf-elm

elm version manager plugin for asdf.
Shell
26
star
15

laminar_cycle

A cycle.js style user-computer model in Laminar
Scala
24
star
16

prefix-css

Prefix all rules from a css-file with a namespace selector.
CoffeeScript
23
star
17

deco

Minimalist Function Decorators for Elixir
Elixir
21
star
18

heroku-buildpack-nim

Deploy nim applications to heroku.
Shell
21
star
19

NoR

There's No Return. A javascript reactive programming engine.
JavaScript
20
star
20

indifferent

Elixir Indifferent access on maps/lists/tuples with custom key transforms.
Elixir
20
star
21

mix_under

Execute mix tasks under Elixir umbrella applications
Elixir
19
star
22

silabas.js

Spanish syllable separator in javascript
JavaScript
17
star
23

sube

Web-based Subtitle Editor.
JavaScript
17
star
24

mill-docker

Build minimalist distroless docker images for your java applications using Mill
Scala
15
star
25

phoenix_now

Example Phoenix deployment to Zeist Now.
Elixir
14
star
26

asdf-ocaml

OCaml plugin for ASDF version manager
Shell
13
star
27

ido-better-flex

A better fuzzy matching algorithm for emacs ido-mode
Emacs Lisp
12
star
28

cyclone

Cyclic Airstream-based stateful components for functional-reactive interfaces on Laminar and Scala.js
Scala
12
star
29

vix

Vic's *Nix config.
Emacs Lisp
12
star
30

gooby

An experimental rubinius bytecode interpreter in Go.
Go
12
star
31

gleam-nix

Build Gleam with Nix.
Nix
10
star
32

rouge

Ruby + Clojure = Rouge
Ruby
10
star
33

elm-facebook

Use Facebook Javascript API from inside ELM
Elm
9
star
34

swagger-elixir

Generate client and (plug) server code in elixir from a swagger api spec.
Elixir
9
star
35

verily

Demo application for APW subscriptions via websockets
Elixir
9
star
36

lispy

Lispy is sexpy Ruby
Ruby
9
star
37

mill-buildr

An small Mill module that lets you define big project structures as a regular scala project itself.
Scala
8
star
38

akin

An Akin programming language optimized for fun.
Ruby
8
star
39

having

Haskell like `where` sugar for Elixir. A pipe-able `with` special form.
Elixir
7
star
40

color-theme-select

Web based version of emacs' (color-theme-select)
JavaScript
7
star
41

tintan

Titanium development with style.
CoffeeScript
6
star
42

pinocchio

Git based server provisioning.
Shell
6
star
43

macpato

Simple pattern matching on Elixir quoted expressions.
Elixir
6
star
44

elmo

A cyclic elm architectured framework
JavaScript
6
star
45

discordex

Discord API for Elixir
Elixir
5
star
46

.hammerspoon

absolute power
Lua
5
star
47

test_async

Make tests inside your ExUnit case to run async.
Elixir
5
star
48

rex

Concatenative Elixir macro language.
Elixir
5
star
49

rojo

Rubinius bytecode interpreter in JavaScript.
5
star
50

fastparse_ext

FastParse extensions
Scala
5
star
51

vee

Vic's Emacs Environment.
Emacs Lisp
4
star
52

vinerb

Full featured Vine API client for ruby.
Ruby
4
star
53

mill-dotenv

A Mill module for twelve-factor apps loading environment variables from a local file.
Scala
4
star
54

docker-neo4j-spatial

Neo4J with spatial plugin on small alpine linux.
4
star
55

nim-heroku-example

Example nim app using heroku buildpack.
Nim
4
star
56

elaxtic

ElasticSearch client for Elixir and Ecto driver.
Elixir
4
star
57

redux-observable-adapter-xstream

Use xstream with redux-observable
JavaScript
4
star
58

redleaf

A Ruby parser producing s-exps using TreeTop.
Ruby
3
star
59

jakesmine

Add Coffee, Jake and Jasmine to your Titanium mobile development.
JavaScript
3
star
60

silabas4j

Spanish syllable separator for java
Java
3
star
61

...

start fresh.
Shell
3
star
62

knockout-jqueryBindingProvider

A binding provider for knockoutjs bind data without using data-bind on views.
JavaScript
3
star
63

clap-nix

Command line argument parser in pure Nix. Supports sub-commands, typed positional arguments, value coercion and resolution via Nix Modules.
Nix
3
star
64

jwm-tt-js-ninja

JWM TechTalk. Becomming a JavaScript Ninja.
JavaScript
3
star
65

silabas.rb

Spanish syllabe separator in ruby
Ruby
3
star
66

infancy

Programming In Fancy. An OpenSource Book to teach the Fancy Programming Language.
3
star
67

mill-scalaxb

Generate Scala from wsdl and xsd files on mill builds
Scala
3
star
68

mill-test-junit-report

Generate JUnit xml reports from Mill's test output.
Scala
3
star
69

mrT

Inverse of shell command, find file first and act accordingly on it. A fast file finder with curses interface.
Ruby
3
star
70

poops

Social real time tamagotchi game.
JavaScript
3
star
71

tito

Quickly create Titanium Mobile UI prototypes on an HTML5 canvas and export them to javascript and jss
2
star
72

elixir_idris

Idris compiler on Elixir
Elixir
2
star
73

ioke-outdated

ioke is a new language for the JVM, based on Io and other languages.
Java
2
star
74

wat.rb

Wat VM implemented in Ruby.
Ruby
2
star
75

SPC

Send keyboard macros to Spacemacs or DOOM Emacs via emacsclient.
Shell
2
star
76

blue

Blue Velvet - Block Lisp Underneath Elixir.
Elixir
2
star
77

promised

Convert back and forth from javascript Promises to Callbacks
2
star
78

jwmscript

Manipulate browser DOM and interact with Javascript libraries using your favourite JVM language
Java
2
star
79

setup_tag

Use tags to mix and match your exunit test context
Elixir
2
star
80

patuit

Twitter for ducks using polymer, neo4j and couchdb
Ruby
2
star
81

fap-fap-fap

Javascript *fap fap fap* Concatenative *fap fap fap* Combinators *fap fap fap*
2
star
82

elmx-webpack-boilerplate

Get up to speed with webpack elm + sass + elmx
JavaScript
2
star
83

knockout-routes

Routes for client-side apps powered by Knockout and History.js
CoffeeScript
2
star
84

im2fansi

Scala library to convert any image into true-color ansi text for display at terminal.
2
star
85

elmx-webpack-preloader

Compile elmx to elm files before using elm-webpack-loader
JavaScript
2
star
86

vico

vic personal homepage
JavaScript
2
star
87

redy

If javascript mews as ruby, it's a ruby.
JavaScript
2
star
88

bc-timer

A basecamp time tracking tool intended to be embedded in gmail, firefox sidebar, and kde's web plasma.
JavaScript
2
star
89

teclado-malvado

an evil keyboard for android
Java
1
star
90

exfmt

Elixir code formatting tool
1
star
91

typeset

An Scala type-indexed set, checked at compile time.
Scala
1
star
92

require.js

A simple node like synchronous require made just for the browser.
JavaScript
1
star
93

schmalz

Automatically exported from code.google.com/p/schmalz
Erlang
1
star
94

pegi

A minimalist parser expression grammar as ruby dsl
Ruby
1
star
95

eniv.co

vine's evil tween
Shell
1
star
96

ikka

Akka experiments in ioke
1
star
97

ice

ICE - IBE Control Examples
JavaScript
1
star
98

cabezon

A prototype using RaphaelJS
JavaScript
1
star
99

viento

No camines que despierta el viento
1
star
100

vic

vic readme
1
star