Meander is a Clojure/ClojureScript library that empowers you to write transparent data transformation code that allows you to plainly see the input and output of these transformations.
The latest version of the library can be found at the following link.
(require '[meander.epsilon :as m])
(defn favorite-food-info [foods-by-name user]
(m/match {:user user
:foods-by-name foods-by-name}
{:user
{:name ?name
:favorite-food {:name ?food}}
:foods-by-name {?food {:popularity ?popularity
:calories ?calories}}}
{:name ?name
:favorite {:food ?food
:popularity ?popularity
:calories ?calories}}))
Meander's match
macro allows us to pattern match on a data structure and return the answer that matches our pattern.
We use logic variables (symbols that start with ?
) to extract values from our input and return them in our output.
Logic variables also let us join across values.
In this example, we do that using the ?food
variable to lookup our users favorite foods in the foods-by-name
collection.
Here is an example of running this match with some test data foods-by-name
.
(def foods-by-name
{:nachos {:popularity :high
:calories :lots}
:smoothie {:popularity :high
:calories :less}})
(favorite-food-info foods-by-name
{:name :alice
:favorite-food {:name :nachos}})
;; =>
{:name :alice
:favorite {:food :nachos
:popularity :high
:calories :lots}}
What if instead of a user having one favorite food, they had many?
And suppose we wanted to return the information for all of them.
The search
macro performs exactly this job.
(defn favorite-foods-info [foods-by-name user]
(m/search {:user user
:foods-by-name foods-by-name}
{:user
{:name ?name
:favorite-foods (m/scan {:name ?food})}
:foods-by-name {?food {:popularity ?popularity
:calories ?calories}}}
{:name ?name
:favorite {:food ?food
:popularity ?popularity
:calories ?calories}}))
There is actually very little that is different here.
Some names have been pluralized, line 2 changed to use search
instead of match
, and on line 6 added we're using the scan
pattern matching operator.
That is all we need to find all of a users favorite foods and look up the information about them.
Lets try it with our test data from the previous example.
(favorite-foods-info foods-by-name
{:name :alice
:favorite-foods [{:name :nachos}
{:name :smoothie}]})
;; =>
({:name :alice
:favorite {:food :nachos
:popularity :high
:calories :lots}}
{:name :alice
:favorite {:food :smoothie
:popularity :high
:calories :less}})
Let's shift gears. What if a user has all sorts of different foods scattered through out their information and we want to collect them all? Here we can use memory variables.
(defn grab-all-foods [user]
(m/find user
{:favorite-foods [{:name !foods} ...]
:special-food !foods
:recipes [{:title !foods} ...]
:meal-plan {:breakfast [{:food !foods} ...]
:lunch [{:food !foods} ...]
:dinner [{:food !foods} ...]}}
!foods))
This code example is a little contrived, but it does immediately show you how you can grab values from all sorts of places in your data structure.
This combination of a !memory-variable
and the zero or more postfix operator ...
is a fairly common one.
Using them together allows you to gather up many values.
If you want to ensure that a certain number of elements exist you can also use the n or more postfix operator i.e. ..2
.
Stepping away from food examples, we can see a few of Meanders more traditional pattern matching abilities.
Imagine that we have coordinates that can either include be [x y]
or [x y z]
and we want a pattern to that extracts y
.
(def point [1 2])
(m/find point
[?x ?y] ?y
[?x ?y ?z] ?y)
;; => 2
Here we used find
to check against multiple patterns.
Meander checks this in a top to bottom ordering.
One thing to note is that since we didn't use ?x
or ?z
we could have replaced them with a wildcard match, a simple symbol which starts with _
.
The above pattern accomplishes the task, but imagine that for some reason people keep passing things that aren't numbers into our match, so we want to restrict our matches to only numbers.
(m/find point
[(m/pred number?) (m/pred number? ?y)]
?y
[(m/pred number?) (m/pred number? ?y) (m/pred number?)]
?y)
This ensures things that aren't number fail to match, but is a little verbose. Honestly, that isn't a problem. Length is not the measure of good code, clarity is. But just to see how Meander allows us to build our own extensions let's look at how we can shorten things up.
(m/defsyntax number
([] `(number _#))
([pattern]
(if (m/match-syntax? &env)
`(m/pred number? ~pattern)
&form)))
(m/find point
[(number) (number ?y)]
?y
[(number) (number ?y) (number)]
?y)
Here we use defsyntax
to essentially build our own macros for Meander.
To learn more check out the Syntax Extensions article.
Sometimes we have a multistep process where we want to transform nested data in place. This is an area that Meander continues to explore, but one powerful way of solving this problem is using Meander's strategies.
(require '[meander.strategy.epsilon :as m*])
(def eliminate-zeros
(m*/rewrite
(+ ?x 0) ?x
(+ 0 ?x) ?x))
(def eliminate-all-zeros
(m*/bottom-up
(m*/attempt eliminate-zeros)))
(eliminate-all-zeros '(+ (+ 0 (+ 0 (+ 3 (+ 2 0)))) 0))
;; => (+ 3 2)
Using our strategies we can make rewrite rules and then say how they ought to be applied. Here we use the attempt strategy, which just says if the match fails, then return whatever was passed in. And the bottom up strategies which applies our match to the most deep value, and replaces values that match all the way up the tree. To learn more checkout how to apply meander strategies
If you have any questions don't hesitate to ask them in Clojurians Slack #meander or file an issue for this project. We are happy to help.
Meander is a young, active, and ambitious project. Unless there is a reason to surrender, the project will continue to be regularly improved.
Prompt releases can be expected when bugs are fixed or there are significant performance enchancements.
This project uses an unorthodox method of versioning in that any change that could break compatibility with the current meander/artifactID
must occur at a new meander/artifactID
.
For instance, a change in syntax could cause matches to now fail.
In this case we do not want a new version of the software i.e. we do not wish to go from 0.0.N
to 1.N.N
etc.
Instead we create a new meander/artifactID
update the namespaces accordingly and continue to progress from there.
This might sound strange at first but this method allows the project to progress in a way that is much more free than a traditional approach with semantic versioning with the following advantages:
meander/artifactID
will always be safe to upgrade.meander/artifactID
is created, you can depend on both the new and the old libraries without conflict when or if you decide to transition.Like anything, there are drawbacks to this approach, however, as the project matures it should stabilize and be more like a "regular" project.
Can you improve this documentation? These fine people already did:
Joel Holdbrooks, Jimmy Miller, Timothy Pratley, Dominic Monroe, Orlando Osorio & xificurCEdit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close