vic

vic

Asdf Core Team

Expat - composable, reusable pattern matching

Expat is a tiny experiment I did for extracting patterns and being able to reuse them (compose and share patterns between elixir libraries). Look for zombies in the README.

It was born as some of my first attempts at creating composable things like the just released data specification library Spec.

Hope anyone finds it interesting at least.

Most Liked

josevalim

josevalim

Creator of Elixir

This is a really nice project. Since you are exploring things on the area, here are some challenges/questions you can consider:

  1. Is it possible to extend expat to be a generalization of records? defpat contains an exact subset of the records functionality, which is the pattern matching one. It is also easy to see how defpat could be used for updates, all you need to do is to match on the value and then build another new value of exactly the same shape with the same variables in place except the ones you are replacing, etc.

  2. Have you considered using another operator instead of = for mixing patterns? I believe your mixing of patterns is actually an intersection. If you assume that %{"id" => id} will match all maps with the "id" field, including %{"id" => "foo", "name" => "baz"}, and the pattern %{"name" => name} all maps with a "name" field, including the map mentioned above, when you specify id() = name() you are actually saying it should have BOTH patterns, id() AND name(). If you think of patterns as sets representing the structures they can match on, it is an intersection. Another reason to choose another operator is that the precedence will work in a way it won’t require parentheses, for example: defpat subject id() &&& name(). Here is a list of operators.

There is one feature we could add to Elixir that would allow the library to become more powerful which is to allow guards inside patterns:

def is_foo_or_bar(atom when atom in [:foo, :bar])

Of course nobody would write such in practice but supporting it would allow you to express patterns with guards:

def is_foo_or_bar(foo_or_bar())

Which the Elixir compiler would then rewrite internally as:

def is_foo_or_bar(atom) when atom in [:foo, :bar]

Anyway, this is very exciting and it is close to topics I am currently researching. :slight_smile: I could expand more on both points above if you are interested. For example, if you are able to generalize defpat to records, you should also be able to generalize it for map updates, and if you define a pattern such as:

defpat foo_bar_baz %{"foo" => {bar, baz}}

And then allow someone to update the nested tuple like this:

map = %{"foo" => {1, 2}, "hello" => "world"}
foo_bar_baz(map, bar: 3)
#=> %{"foo" => {3, 2}, "hello" => "world"}

Optimizations could allow you to compile foo_bar_baz to %{map | "foo" => Map.fetch!(map, "foo") |> put_elem(0, 3)}.

vic

vic

Asdf Core Team

Hey, good news, I’ve just released v1.0, I did a major rewrite as I really wanted (and needed) it to support guards. Wrote much a much better README guide (I guess) and also documented it more.

The only thing I removed from v0 is the ... syntax as it introduced all variables in scope and it was mostly a pain since elixir 1.5. So now you have to be explicit on what you bind.

Hope the guide explains a bit better where this library fits and how it could be used.

<3

Edit. Forgot to mention, for @josevalim, in the example using guards from the readme, expat def just expands the inner patterns and collects (anding) any guards produced by them, and finally just ands those with any from the function definition. here’s the code

ibgib

ibgib

Yeah, this is definitely my favorite new library. The primary gain from it for me is making things DRYer. Where before I would have map structures repeated in multiple function clauses, e.g.

def handle_cmd(%{"dest_ib" => dest_ib,
                 "context_ib_gib" => context_ib_gib,
                 "src_ib_gib" => src_ib_gib} = data, ..) when guard1 do
def handle_cmd(%{"dest_ib" => dest_ib,
                 "context_ib_gib" => context_ib_gib,
                 "src_ib_gib" => src_ib_gib} = data, ..) when guard2 do

which has redundancy in the "var_name" => var_name, as well in the function clause level. And dest_ib and src_ib_gib are used across multiple commands in many places. I’m now able to create a single file for the reused patterns:

defmodule WebGib.Patterns do
  @moduledoc """
  Reusable patterns using expat
  """
  
  import Expat
  
  defpat dest_ib_        %{"dest_ib" => dest_ib}
  defpat src_ib_gib_     %{"src_ib_gib" => src_ib_gib}
  defpat context_ib_gib_ %{"context_ib_gib" => context_ib_gib}
  # ..
end

(NB: I am tacitly going with a _ suffix to indicate a pattern vs the var name, but I’m not sure what other non-word characters are legal in elixir. I would rather prefix the pattern with a single character and would love any suggestions.)

And then I compose them above the function and consume them:

defpat fork_data_(
  dest_ib_() =
  context_ib_gib_() =
  src_ib_gib_()
)

def handle_cmd(fork_data_(...) = data, ..) when guard1 do
def handle_cmd(fork_data_(...) = data, ..) when guard1 do

EDIT: The ... inside the pattern is literal syntax, which helps enormously with DRY. The other .. just means other args.

This is ridiculously more DRY and readable. Definitely a powerful lib you made here! :smile:

Thank you! :thumbsup:

vic

vic

Asdf Core Team

oh btw, there’s defpatp for private patterns (wont be visible from other modules it’s defmacropd)

ibgib

ibgib

Ah, so in foo(baz: qux), you’re actually accessing the variable assigned to the value of the key in the original map? In looking at your readme, this confusion arose I think because all of your examples have the same name for both the key/value, e.g. "iq" => iq and "email" => email. I had errantly latched onto the keys being converted. But now I see that you are pulling the variables (which are atoms) out of the pattern and using those for composition. That’s awesome! :+1:

Yes, this makes it much clearer to me. Thanks! :smile:

Where Next?

Popular in Libraries Top

mhanberg
I just released the first version of Temple: an HTML DSL for Elixir and Phoenix! You can read this blog post or the docs for more info...
New
Qqwy
While not as prevalent as in imperative languages, arrays (collections with efficient random element access) are still very useful in Eli...
New
mcrumm
If you would like to migrate away from node/npm/webpack while still using sass, the dart_sass package provides a installer and runner for...
New
deadtrickster
I’ve just released stable versions of my Prometheus Elixir libs: Elixir client [docs]; Ecto collector [docs]; Plugs instrumenter/Export...
New
oltarasenko
Dear Elixir community, After a year of development, bug fixes, and improvements, we are proudly ready to share the release of Crawly 0.1...
New
sasajuric
I’d like to announce a small library called boundaries. This is an experimental project which explores the idea of enforcing boundaries ...
New
entone
ExARI An Elixir library for interfacing with Asterisk using the ARI protocol. Be sure to check out the example application https://git...
New
KronicDeth
Elixir plugin for JetBrain’s IntelliJ Platform (including Rubymine) This is a plugin that adds support for Elixir to JetBrains IntelliJ...
289 35421 110
New
woylie
Flop is an Elixir library that applies filtering, ordering and pagination parameters to your Ecto queries. offset-based pagination with...
New
Antrater
Hi everyone! I’m thrilled to announce a huge thing. We have been developing Elixir Moon Design System for quite a while. We are finally ...
New

Other popular topics Top

lastday4you
I wanted to check elixir version in phoenix because i found that my elixir is 1.5 but when i use Enum.chunk_by it said the function is un...
New
JDanielMartinez
Hi! May someone helps me, please! I have two apps into an umbrella project: the first one is Database, which manages queries, and the se...
New
JakeBecker
TL;DR: I’ve just released an implementation of Microsoft’s IDE-independent Language Server Protocol for Elixir. It adds language support ...
1140 51847 244
New
Brian
What is the proper way to load a module from a file in to IEX? In the python world, doing something like this pretty standard: from ....
New
sergio
In Ruby, I can go: User.find_by(email: "foobar@email.com").update(email: "hello@email.com") How can I do something similar in Elixir? ...
New
srinivasu
How to handle excepions in elixir? Suppose i have A, B, C ,D, E modules. and each module has get() function. A.get() method will call th...
New
KronicDeth
Elixir plugin for JetBrain’s IntelliJ Platform (including Rubymine) This is a plugin that adds support for Elixir to JetBrains IntelliJ...
289 35421 110
New
quazar
How to set Jason to encode all fields in ecto schema, I don’t care about security and implementing only is taking long list of attributes...
New
myronmarston
The Elixir Typespec docs show the following syntax for keyword lists in typespecs: # ... | [key: type] # keyword lis...
New
siddhant3030
Hi, I have to write a raw query for one of my project. But till now I have used ecto queries and don’t have much experience writing raw ...
New

Sub Categories:

We're in Beta

About us Mission Statement