vic

vic

Asdf Core Team

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.

Showing Posts 32 to 23

marciol

marciol

I’m thinking that expat can be a simple way to improve readability and also it allows a way to do a sort of contract check at function boundaries. Is this the way to follow or the Elixir community are agreeing on other approaches?

josevalim

josevalim

Creator of Elixir

Follow-up question: how do you know that in the natural number case you need to wrap each subpattern inside {:nat, val} but you don’t have to in the maybe case? Given one says zero(0) and the other says nothing(nil), I was expecting zero() to generate 0 and not {:nat, 0}.

I also assume that when using unions you can no longer do something like def value(maybe())? I guess not doing that makes sense though, as the different subpatterns may have different shapes/number of arguments.

josevalim

josevalim

Creator of Elixir

Hi @vic,

I really enjoy the continuous work you have been doing in expat!

I wonder if we should support when clauses inside patterns and automatically move them out. So if you do:

def foo(x when is_integer(x)), do: x

It becomes:

def foo(x) when is_integer(x), do: x

while nobody would write the first in practice, it is very powerful for extending the language. It means you would be able to drop the expat prefix in many cases.

Similarly, regarding the union types, I wonder if instead of the expat prefix, you could use the prefix of the union, so this example becomes:

  test "nil cannot be pattern matched with just" do
    maybe case Keyword.get([], :foo) do
      just() -> raise "Should not happen"
      nothing() -> assert :ok
    end
  end

Or even:

  test "nil cannot be pattern matched with just" do
    maybe Keyword.get([], :foo) do
      just() -> raise "Should not happen"
      nothing() -> assert :ok
    end
  end

Although I am not sure how that would play with nesting. For example, if you have a maybe with a nat inside, how would you handle it? Do you need two separate cases? I guess that would be the case, as a nat with a nothing doesn’t make sense. I wonder if other languages (OCaml/Haskell/F#) allow case to be “folded” in such cases or do they require an explicit case per pattern? /cc @OvermindDL1

Thoughts?

OvermindDL1

OvermindDL1

I saw! I also left a comment/question on that commit. :slight_smile:

vic

vic OP

Asdf Core Team

Yep, added your additional Maybe example, thanks. Also added a
Result one.

OvermindDL1

OvermindDL1

Oh now those are really cool! Full matching and so forth without enforcing a structure, even the Maybe is just natural in elixir (still not big on the nil word, but eh, Elixir’isms). :slight_smile:

EDIT1: In the maybe test, you have this test:

  test "just can be pattern matched" do
    assert just() = :jordan
  end

What about having this too:

  test "just can be pattern matched and extract" do
    assert just(j) = :jordan
    assert j == :jordan  
  end

That shows you can extract as well. :slight_smile:

EDIT2: How would you implement a Result type that accepted an :ok, {:ok, value}, :error, and {:error, reason} as an example? That would show a variety of usages of using native types and no wrapping and so forth, preferably using ok/0/error/0/ok/1/error/1 and such constructors. :slight_smile:

vic

vic OP

Asdf Core Team

v1.0.4 features Union Patterns. Patterns composed into a single one, this lets you emulate things like:

OvermindDL1

OvermindDL1

What my defguard did was you could define something like:

defguard is_exception(%{__struct__: struct_name, __exception__: true}) when is_atom(struct_name)

This could then by used like:

  def blorp(_exc) when is_exception(_exc), do: "exceptioned"
  def blorp(val), do: "No-exception:  #{inspect val}"

And it does expand as expected. Specifically since _exc is passed into the is_exception ‘guard’ then the _exc in the argument list gets a matcher added to it, while it also added the guard in the is_exception(_exc) position. You could even match ‘out’ something by passing in more arguments in some custom defguard, but essentially the above example turned into:

  def blorp(_exc = %{__struct__: struct_name$1, __exception__: true}) when is_atom(struct_name$1), do: "exceptioned"
  def blorp(val), do: "No-exception:  #{inspect val}"

It basically just allows treating a guard as something that can both match and guard at the same time. This is more powerful than just what expat does now because currently expat can only add a single matcher and N guards, where with my defguard pattern above it allows for N matchers and N guards, like in this convoluted example:

  defguard can_vote?(%Human{birthdate: utc_bd}, now) when (utc_bd + 60*60*24*365*18) >= now

...

def vote(human, now) when can_vote?(human, now), do: ...
def vote(_, _), do: ...

Or whatever, the thing is that you can pass in multiple things, and notice that you can pass ‘out’ as well:

defguard bloop({a, b}, b) when is_atom(a)

...

def blorp(c) when bloop(c, d), do: d

So here you can see that it passes ‘out’ something as well, and you just put a binding there. Notice that you can put a specific value there too like:

def blorp(c) when bloop(c, :ok), do: ...

So now that will only match {a, :ok} where a is an atom. The thing is that instead of just being 1-matcher/N-guards, it is N-matchers/N-guards, and since it is in the guard position then you can use in_guard?/1 as usual to do the right action if in a guard or make it an expression, which means you could use it in statement positions properly like:

a = {:ok, :blah}
b = 42
if foo(a, b, c) do ... else ... end ...

And notice that you can match on yet ‘more’ things as well. I could easily imagine adding the capability of having it support static KWLists so you could do things like this too if you want:

def blorp(c) when bloop(c, out: d), do: d
# or
if foo(the_tuple: a, an_arg: b, out: c) do ... else ... end ...

Or whatever as well. :slight_smile:

Yep, same with the defguard style too.

Instead of just supporting single matchers, what about supporting multiple?

I’m not saying replace the existing functionality with this, but rather ‘add’ the defguard stuff to expat as well, then it would have both the ability for N-Matchers/N-Guards via a ‘guard’ and for easy-inline 1-Matcher/N-Guards via the matcher style too. :slight_smile:

Expansion is not limited to being used as part of when, it can be used as an expression with defguard as well as it essentially just appears as any normal function call. :slight_smile:

vic

vic OP

Asdf Core Team

Oh btw, generated macros now get documented.

vic

vic OP

Asdf Core Team

I think I don’t understand what you mean with multiple matchers, could you provide an example of what syntax you use in yours and what would you want it to expand into?

Let me ellaborate a bit on how expat expands maybe could be useful for other people following this thread:

When you define defpat foo(1), the ast inside foo (here 1) is the code that will be placed at call-site. eg. foo() = 1 expands to 1 = 1.

That means it’s actually possible to place any elixir code in there, and the foo macro will just expand it when called.

Now, since expat’s purpose in life is to help with pattern matching,
the ast inside foo is treated specially in the following cases:

  • if it contains a variable like defpat foo(x) then x is boundable by the caller of foo. The caller can bind it by name, like:
    foo(x: 1) = 1 => 1 = 1
    If x is not bound by the caller, like foo(), x will be replaced with an _ , so foo() = 1 is _ = 1

  • if it contains a guard like defpat bar(y) when y > 10 then, the code of the guard will also be expanded, for example:
    bar(y: 2) will expand to y = 2 when y > 10.

    Note however that since we have a guard to check, and y is being used in it, the variable y is preserved in expansion, however this y is higenic (elixir’s counter distingishes it from others) and will not bind any other y in your own scope.

    To bind in your scope you do something like
    bar(y: u) expands to u = y when y > 10 and u is a variable you provided from your scope.

    So, you could bind bar’s y with any expression, even other pattern expansions (just regular function calls)

    bar(y: z = foo(x: 20)) will expand to y = z = 20 when y > 20
    this will also work: bar(z = foo(20)) since expat now supports positional arguments (variables get bound in the order they appear on the pattern)

  • If it contains a nested pattern expansion. For example, if you had
    defpat t2({x, y}) when x > y and later did
    defpat teens(t2(bar(a), bar(b))) when a < 20 and b < 20

    Then teens has two bindable names, :a and :b and it will get expanded into a pattern like:
    {a, b} when a < 20 and b < 20 and a > b and a > 10 and b > 10
    That means inner guards get propagated into the calling expansion.

Now since defpat just captures the code inside the pattern for expanding it later, defpat named(%{"name" => name}) allows you to expand named anywhere you can place a pattern in elixir, like on the left hand side of =

named(x) = %{"name" => "vic"} will expand to
%{"name" => x} = %{"name" => "vic"}, that’s why you can use it on a function definition like:

def index(conn, params = named(name)), do: ...

However, for those containing guards,

def lalala(teens(m, n)) would by itself expand into:
def lalala({m, n} when m < 20 and n < 20 and m > n and m > 10 and n > 10)

Of course having a when in that context fails.
as it would do if you try:

iex(14)> {m, n} when m > n and m > 10 and n > 10 = {30, 20}
** (CompileError) iex:14: undefined function when/2

So, having guards was what introduced the expat def syntax:

expat def lalala(teens(m, n)) expands correctly into:
def lalala({m, n}) when m < 20 and n < 20 and m > n and m > 10 and n > 10.

Finally, that’s why expat is different from defguard both elixir’s and @OvermindDL1’s because for expat, expansion is not limited to be used as part of a when. Named expat patterns can be used anywhere it’s valid to expand it’s containing expression.

So, hope that helps anyone.

If someone is interested, you can read more examples in the tests, and the docs, or use the source.

Where Next? Top

Trending in Announcing Top

woylie
Flop is an Elixir library that applies filtering, ordering and pagination parameters to your Ecto queries. offset-based pagination with...
New
MRdotB
I needed to reuse React components from my Chrome extension in my Phoenix/LiveView backend. I noticed that for Svelte/Vue, there are live...
New
woylie
I released Doggo, a collection of unstyled Phoenix components. https://github.com/woylie/doggo Features Unstyled Phoenix components....
New
GenericJam
Edit: 2026 May 15 - This post is archived. Mob is alive!! Main docs: mob v0.7.11 — Documentation A bit of explanation for the slightly c...
New
JesseHerrick
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
marciok
Hi there! We created Gust: A task orchestrator inspired by Airflow. For those who have never heard about Aiflow, it’s a Python-based wor...
New
anuaralfetahe
Hello Published a new library - ProcessHub! ProcessHub is a library designed to manage process distribution within the Elixir cluster. ...
New

Other Trending Topics Top

mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
mhanberg
Hi everyone! The first release candidate for the Expert language server project is now available! We’ve published a press release detai...
New
webofbits
With AI doing more of the implementation work, I’ve been wondering how much coding I should deliberately keep doing myself. My main conc...
#ai
New
AstonJ
This showed up on my feed.. anyone heard of it? Just hype? Ox Alpha is a reasoning model designed for coding, sustained ag...
New
bartblast
Hey folks, I just published a post about Hologram’s funding and where the project goes next - the short version: Curiosum as Main Spons...
New
CodeSync
:microphone: ElixirConf 2026 - Call for Talks is open! We’re heading to Chicago :united_states: :round_pushpin: In person + virtual :d...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews