Appendix C: Pattern-Matching Macros
Pattern-matching is a native Lux feature, and yet case
is a macro.
Why?, you may wonder. What does being a macro add to the mix?
Well, as it turns out, by making case
be a macro, Lux can perform some compile-time calculations which ultimately enable a set of really cool features to be implemented: custom pattern-matching.
Most languages with pattern-matching have a fixed set of rules and patterns for how everything works. Not so with Lux.
Lux provides a set of default mechanisms, but by using macros where patterns are located, case
can expand those macro calls to get the myriad benefits they offer.
But enough chit-chat. Let's see them in action.
PM-Macros in the Standard Library
(case (list 1 2 3)
(^ (list x y z))
(#;Some (+ x (* y z)))
_
#/None)
You may remember how annoying it was to pattern-match against lists in the Chapter 5 example.
Well, by using the ^
PM-macro, you can use any normal macros you want inside the pattern to profit from their code-construction capacities.
## Multi-level pattern matching.
## Useful in situations where the result of a branch depends on further refinements on the values being matched.
## For example:
(case (split (size static) uri)
(^=> (#;Some [chunk uri']) [(Text/= static chunk) true])
(match-uri endpoint? parts' uri')
_
(#;Left (format "Static part " (%t static) " doesn't match URI: " uri)))
## Short-cuts can be taken when using boolean tests.
## The example above can be rewritten as...
(case (split (size static) uri)
(^=> (#;Some [chunk uri']) (Text/= static chunk))
(match-uri endpoint? parts' uri')
_
(#;Left (format "Static part " (%t static) " doesn't match URI: " uri)))
I love ^=>
.
It's one of those features you don't need often, but when you do, it saves the day.
The possibilities are endless when it comes to the refinement you can do, and when you consider what you'd have to do to get the same results without it, it's easy to see how much code it saves you.
## Allows you to simultaneously bind and de-structure a value.
(def: (hash (^@ set [Hash<a> _]))
(List/fold (lambda [elem acc] (n.+ (:: Hash<a> hash elem) acc))
+0
(to-list set)))
^@
is for when you want to deal with a value both as a whole and in parts, but you want to save yourself the hassle of this 2-part operation.
## Same as the "open" macro, but meant to be used as a pattern-matching macro for generating local bindings.
## Can optionally take a "prefix" text for the generated local bindings.
(def: #export (range (^open) from to)
(All [a] (-> (Enum a) a a (List a)))
(range' <= succ from to))
^open
allows you to open structures using local variables during pattern-matching.
It's excellent when taking structures as function arguments, or when opening structures locally in let
expressions.
## Or-patterns.
(type: Weekday
#Monday
#Tuesday
#Wednesday
#Thursday
#Friday
#Saturday
#Sunday)
(def: (weekend? day)
(-> Weekday Bool)
(case day
(^or #Saturday #Sunday)
true
_
false))
^or
patterns allow you to have multiple patterns with the same branch.
It's a real time-saver.
## It's similar to do-template, but meant to be used during pattern-matching.
(def: (beta-reduce env type)
(-> (List Type) Type Type)
(case type
(#;HostT name params)
(#;HostT name (List/map (beta-reduce env) params))
(^template [<tag>]
(<tag> left right)
(<tag> (beta-reduce env left) (beta-reduce env right)))
([#;SumT] [#;ProdT])
(^template [<tag>]
(<tag> left right)
(<tag> (beta-reduce env left) (beta-reduce env right)))
([#;LambdaT]
[#;AppT])
(^template [<tag>]
(<tag> old-env def)
(case old-env
#;Nil
(<tag> env def)
_
type))
([#;UnivQ]
[#;ExQ])
(#;BoundT idx)
(default type (list;at idx env))
(#;NamedT name type)
(beta-reduce env type)
_
type
))
^template
is ^or
's big brother.
Whereas ^or
demands that you provide the exact patterns you want (and with a single branch body), ^template
lets you provide templates for both the patterns and bodies, and then fills the blanks with all the given parameters.
You can save yourself quite a lot of typing (and debugging) by reusing a lot of pattern-matching code with this macro.
It's a great asset!
## Allows you to extract record members as local variables with the same names.
## For example:
(let [(^slots [#foo #bar #baz]) quux]
(f foo bar baz))
^slots
is great for working with records, as it allows you to create local variables with the names of the tags you specify.
Now you can work with record member values with ease.
## Allows destructuring of streams in pattern-matching expressions.
## Caveat emptor: Only use it for destructuring, and not for testing values within the streams.
(let [(^stream& x y z _tail) (some-stream-func 1 2 3)]
(func x y z))
^stream&
hails from the lux/codata/struct/stream
module, and it's quite special, because it allows you to de-structure something you normally wouldn't be able to: functions.
You see, Lux streams (as defined in lux/codata/struct/stream
) are implemented using functions.
The reason is that they are infinite in scope, and having an infinite data-structure would be... well... impossible (unless you manage to steal a computer with infinite RAM from space aliens).
Well, no biggie!
^stream&
does some black magic to make sure you can de-structure your stream just like any other data-structure.
How to Make your Own
The technique is very simple.
Just create a normal macro that will take as a first argument a form containing all the "parameters" to your pattern-matching macro.
It's second argument will be the body associated with the pattern.
After that, you'll receive all the branches (pattern + body) following the call to PM-macro.
You can process all your inputs as you wish, but in the end you must produce an even number of outputs (even, because the outputs must take the form of pattern+body pairs).
These will be further macro-expanded until all macros have been dealt with and only primitive patterns remain, so case
can just go ahead and do normal pattern-matching.
You may wonder: why do I receive the body?
The answer is simple: depending on your macro, you may be trying to provide some advance functionality that requires altering (or replicating) the code of the body.
For example, ^or
copies the body for every alternative pattern you give it, and ^template
must both copy and customize the bodies (and patterns) according to the parameters you give it.
But receiving the next set of branches takes the cake for the weirdest input.
What gives?
It's simple: some macros are so advanced that they require altering not just their bodies, but anything that comes later.
A great example of that is the ^=>
macro (which is actually the reason those inputs are given to PM-macros in the first place).
^=>
performs some large-scale transformations on your code which require getting access to the rest of the code after a given usage of ^=>
.
However, most of the time, you'll just return the branches (and sometimes the body) unchanged.
To make things easier to understand, here is the implementation of the ^
macro, from the lux
module:
(macro: #export (^ tokens)
(case tokens
(#Cons [_ (#FormS (#Cons pattern #Nil))] (#Cons body branches))
(do Monad<Lux>
[pattern+ (macro-expand-all pattern)]
(case pattern+
(#Cons pattern' #Nil)
(wrap (list& pattern' body branches))
_
(fail "^ can only expand to 1 pattern.")))
_
(fail "Wrong syntax for ^ macro")))
The ^ prefix given to PM-macros was chosen simply to make them stand-out when you see them in code.
There is nothing special attached to that particular character.