Expat - Elixir reusable, composable patterns <a href="https://travis-ci.org/vic/expat"><img src="https://travis-ci.org/vic/expat.svg"></a>
Extracting patterns into reusable bits
Pattern matching on function heads is the alchemist way for dispatching to the correct function on Elixir. However if a patterns get very large (I've seen people who could split their code better having large patterns on their phoenix controllers due to nested patterns like maps inside maps, or matching on many parameters) then code could turn a bit ugly (IMHO) forcing the reader's eyes to parse the whole pattern and then discover where the actual function logic starts.
# Like tinder, but for brains
# (actually a project from a friend who asked to codereview with her and thus expat was born)
defmodule Brainder do
# brain match two subjects if their iq difference is less than ten
# requires both of them to have email and location in their structure
def brain_match(subject_a = %{
"iq" => iq_a,
"email" => _,
"location" => %{
"lat" => _, "lng" => _
}
},
subject_b = %{
"iq" => iq_b
"email" => _,
"location" => %{
"lat" => _, "lng" => _
}
}) when abs(iq_a - ia_b) < 10
do
# finally, actual logic here
end
endUsage
Expat provides a defpat (define pattern) macro for moving away those patterns into resusable bits (expatriating them from the function head)
defmodule Brainder do
# provides `defpat` and `defpatp` for defining public and private patterns.
include Expat
# defpath takes a name and a pattern it will expand to:
defpat iq(%{"iq" => iq})
defpat email %{"email" => email}
# patterns can be reused inside others
defpat latlng %{"lat" => lat, "lng" => lng}
defpat location %{"location" => latlng()}
# *mixing* patterns is done by just using the `=` match operator
# thus subject is something that has iq, email and a location.
defpat subject(iq() = email() = location())
# the function head is more terse now, while still having access to the inner
# iq on each subject, and ensuring both of them have the same email, location fields
def brain_match(subject_a = subject(iq: iq_a),
subject_b = subject(iq: iq_b))
when abs(iq_a - ia_b) < 10 do
# logic here, distance from function head to this line is shorter
# while still explicit on what variables we can use here
end
end
Notice how subject(iq: iq_a) tells expat we only need to extract the value of iq from
the subject, while still matching all of its structure, thus expanding to:
%{
"iq" => iq_a,
"email" => _,
"location" => %{
"lat" => _, "lng" => _
}
}
Similarly, you can just validate the pattern structure without extracting values with subject(_) which expands to:
%{
"iq" => _,
"email" => _,
"location" => %{
"lat" => _, "lng" => _
}
}
One nice thing about expat patterns is that because they are generated as macros, they can be used anywhere a
pattern can be used in Elixir with, case, as the left side of a = match, like in tests
test "dude is smart", %{dude: dude} do
assert subject(iq: 200) = dude
end
test "subject(...) binds all variables inside it", %{dude: subject(...)} do
assert iq > 200
assert email == "terry.tao@example.com"
end
`````
For example, you could export the `Briander.subject` pattern in a library and have nice people to use it for matching on things with that pattern (maybe before passing them to your api).
def ZombieCoder do # use Brainder to search for brains, not love require Brainder
# find and eat juicy brains def braaaaains() do World.population |> Stream.filter(fn Brainder.subject(iq: iq, lat: lat, lng: lng) where iq > 200 -> {lat, lng} end) |> Stream.map(&yuuuumi_eaaaat/1) end end
## Installation
[Available in Hex](https://hex.pm/packages/expat), the package can be installed
by adding `expat` to your list of dependencies in `mix.exs`:
def deps do [{:expat, "~> 0.1"}] end