Match.jl –- Advanced Pattern Matching for Julia
This package provides both simple and advanced pattern matching capabilities for Julia. Features include:
- Matching against almost any data type with a first-match policy
- Deep matching within data types, tuples, and vectors
- Variable binding within matches
- Efficient code generation via a decision automaton.
Installation
Use the Julia package manager. Within Julia, do:
Pkg.add("Match")
Usage
Simple-pattern @ismatch
macro
The @ismatch
macro tests if a value patches a given pattern, returning either true
if it matches, or false
if it does not. When the pattern matches, the variables named in the pattern are bound and can be used.
julia> using Match
julia> @ismatch (1, 2) (x, y)
true
julia> x
1
julia> y
2
Multi-case @match
macro
The @match
macro acts as a pattern-matching switch statement, in which each case has a pattern and a result for when that pattern matches. The first case that matches is the one that computes the result for the @match
.
using Match
@match item begin
pattern1 => result1
pattern2 where cond => result2
pattern3 || pattern4 => result3
_ => default_result
end
Patterns can be values, regular expressions, type checks or constructors, tuples, or arrays. It is possible to supply variables inside a pattern, which will be bound to corresponding values. This and other features are best seen with examples.
Match Values
The easiest kind of matching to use is simply to match against values:
@match item begin
1 => "one"
2 => "two"
_ => "Something else..."
end
Values can be computed expressions by using interpolation. That is how to use @match
with @enum
s:
@enum Color Red Blue Greed
@match item begin
$Red => "Red"
$Blue => "Blue"
$Greed => "Greed is the color of money"
_ => "Something else..."
end
Match Types
Julia already does a great job of this with functions and multiple dispatch, and it is generally be better to use those mechanisms when possible. But it can be done here:
julia> matchtype(item) = @match item begin
::Int => println("Integers are awesome!")
::String => println("Strings are the best")
::Dict{Int, String} => println("Ints for Strings?")
::Dict => println("A Dict! Looking up a word?")
_ => println("Something unexpected")
end
julia> matchtype(66)
Integers are awesome!
julia> matchtype("abc")
Strings are the best
julia> matchtype(Dict{Int, String}(1=>"a",2=>"b"))
Ints for Strings?
julia> matchtype(Dict())
A Dict! Looking up a word?
julia> matchtype(2.0)
Something unexpected
Deep Matching of Composite Types
One nice feature is the ability to match embedded types, as well as bind variables to components of those types:
struct Address
street::String
city::String
zip::String
end
struct Person
firstname::String
lastname::String
address::Address
end
personinfo(person) = @match person begin
Person("Julia", lname, _) => "Found Julia $lname"
Person(fname, "Julia", _) => "$fname Julia was here!"
Person(fname, lname,
Address(_, "Cambridge", zip)) => "$fname $lname lives in zip $zip"
Person(_...) => "Unknown person!"
end
julia> personinfo(Person("Julia", "Robinson",
Address("450 Serra Mall", "Stanford", "94305")))
"Found Julia Robinson"
julia> personinfo(Person("Gaston", "Julia",
Address("1 rue Victor Cousin", "Paris", "75005")))
"Gaston Julia was here!"
julia> personinfo(Person("Edwin", "Aldrin",
Address("350 Memorial Dr", "Cambridge", "02139")))
"Edwin Aldrin lives in zip 02139"
julia> personinfo(Person("Linus", "Pauling",
Address("1200 E California Blvd", "Pasadena", "91125")))
"Unknown person!"
Alternatives and Guards
Alternatives allow a match against multiple patterns.
Guards allow a conditional match. They are not a standard part of Julia yet, so to get the parser to accept them requires that they are preceded by a comma and end with "end":
function parse_arg(arg::String, value::Any=nothing)
@match (arg, value) begin
("-l", lang) => println("Language set to $lang")
("-o" || "--optim", n::Int),
if 0 < n <= 5 end => println("Optimization level set to $n")
("-o" || "--optim", n::Int) => println("Illegal optimization level $(n)!")
("-h" || "--help", nothing) => println("Help!")
bad => println("Unknown argument: $bad")
end
end
julia> parse_arg("-l", "eng")
Language set to eng
julia> parse_arg("-l")
Unknown argument: ("-l",nothing)
julia> parse_arg("-o", 4)
Optimization level set to 4
julia> parse_arg("--optim", 5)
Optimization level set to 5
julia> parse_arg("-o", 0)
Illegal optimization level 0!
julia> parse_arg("-o", 1.0)
Unknown argument: ("-o",1.0)
julia> parse_arg("-h")
Help!
julia> parse_arg("--help")
Help!
The alternative guard syntax pattern where expression
can sometimes be easier to use.
function parse_arg(arg::String, value::Any=nothing)
@match (arg, value) begin
("-l", lang) => println("Language set to $lang")
("-o" || "--optim", n::Int) where 0 < n <= 5 =>
println("Optimization level set to $n")
("-o" || "--optim", n::Int) => println("Illegal optimization level $(n)!")
("-h" || "--help", nothing) => println("Help!")
bad => println("Unknown argument: $bad")
end
end
Match Ranges
Borrowing a nice idea from pattern matching in Rust, pattern matching against ranges is also supported:
julia> function num_match(n)
@match n begin
0 => "zero"
1 || 2 => "one or two"
3:10 => "three to ten"
_ => "something else"
end
end
num_match (generic function with 1 method)
julia> num_match(0)
"zero"
julia> num_match(2)
"one or two"
julia> num_match(12)
"something else"
julia> num_match('c')
"something else"
Note that a range can still match another range exactly:
julia> num_match(3:10)
"three to ten"
Regular Expressions
A regular expression can be used as a pattern, and will match any string that satisfies the pattern.
Match.jl used to have complex regular expression handling, permitting the capturing of matched subpatterns. We are considering adding that back again.
Deep Matching Against Arrays
Arrays are intrinsic components of Julia. Match allows deep matching against single-dimensional vectors.
Match previously supported multidimensional arrays. If there is sufficient demand, we'll add support for that again.
The following examples also demonstrate how Match can be used strictly for its extraction/binding capabilities, by only matching against one pattern.
Extract first element, rest of vector
julia> @ismatch 1:4 [a,b...]
true
julia> a
1
julia> b
2:4
Match values at the beginning of a vector
julia> @ismatch 1:5 [1,2,a...]
true
julia> a
3:5
Notes/Gotchas
There are a few useful things to be aware of when using Match.
if
guards need a comma and an `end`:
Bad
julia> _iseven(a) = @match a begin
n::Int if n%2 == 0 end => println("$n is even")
m::Int => println("$m is odd")
end
ERROR: syntax: extra token "if" after end of expression
julia> _iseven(a) = @match a begin
n::Int, if n%2 == 0 => println("$n is even")
m::Int => println("$m is odd")
end
ERROR: syntax: invalid identifier name =>
Good
julia> _iseven(a) = @match a begin
n::Int, if n%2 == 0 end => println("$n is even")
m::Int => println("$m is odd")
end
# methods for generic function _iseven
_iseven(a) at none:1
It is sometimes easier to use the where
syntax for guards:
julia> _iseven(a) = @match a begin
n::Int where n%2 == 0 => println("$n is even")
m::Int => println("$m is odd")
end
# methods for generic function _iseven
_iseven(a) at none:1
@match_return
macro
@match_return value
Within the result value (to the right of the =>
) part of a @match
case, you can use the @match_return
macro to return a result early, before the end of the block. This is useful if you have a shortcut for computing the result in some cases. You can think of it as a return
statement for the @match
macro.
Use of this macro anywhere else will result in an error.
@match_fail
macros
@match_fail
Inside the result part of a @match
case, you can cause the case to fail as if the corresponding pattern did not match. The @match
statement will resume attempting to match the following cases. This is useful if you want to write some complex code that would be awkward to express as a guard.
Use of this macro anywhere else will result in an error.
single-case @match
macro
@match pattern = value
Returns the value if it matches the pattern, and binds any pattern variables. Otherwise, throws MatchFailure
.
ismatch
macro
@ismatch value pattern
Returns true
if value
matches pattern
, false
otherwise. When returning true
, binds the pattern variables in the enclosing scope.
Examples
Here are a couple of additional examples.
Mathematica-Inspired Sparse Array Constructor
I've realized that
Match.jl
is perfect for creating in Julia an equivalent of SparseArray which I find quite useful in Mathematica.My basic implementation is this:
macro sparsearray(size, rule) return quote _A = spzeros($size...) $(push!(rule.args, :(_ => 0))) for _itr in eachindex(_A) _A[_itr] = @match(_itr.I, $rule) end _A end end
Example:
julia> A = @sparsearray (5,5) begin (n,m), if n==m+1 end => m (n,m), if n==m-1 end => n+10 (1,5) => 1 end
which creates the matrix:
julia> full(A) 5x5 Array{Float64,2}: 0.0 11.0 0.0 0.0 1.0 1.0 0.0 12.0 0.0 0.0 0.0 2.0 0.0 13.0 0.0 0.0 0.0 3.0 0.0 14.0 0.0 0.0 0.0 4.0 0.0
Matching Exprs
The @match
macro can be used to match Julia expressions (Expr
objects). One issue is that the internal structure of Expr objects doesn't match their constructor exactly, so one has to put arguments in brackets, as well as capture the typ
field of macros.
The following function is a nice example of matching expressions. It is used in VideoIO.jl
to extract the names of expressions generated by Clang.jl
, for later filtering and rewriting.:
extract_name(x) = string(x)
function extract_name(e::Expr)
@match e begin
Expr(:type, [_, name, _]) => name
Expr(:typealias, [name, _]) => name
Expr(:call, [name, _...]) => name
Expr(:function, [sig, _...]) => extract_name(sig)
Expr(:const, [assn, _...]) => extract_name(assn)
Expr(:(=), [fn, body, _...]) => extract_name(fn)
Expr(expr_type, _...) => error("Can't extract name from ",
expr_type, " expression:\n",
" $e\n")
end
end
Inspiration
The following pages on pattern matching in scala provided inspiration for the library:
- http://thecodegeneral.wordpress.com/2012/03/25/switch-statements-on-steroids-scala-pattern-matching/
- http://java.dzone.com/articles/scala-pattern-matching-case
- http://kerflyn.wordpress.com/2011/02/14/playing-with-scalas-pattern-matching/
- http://docs.scala-lang.org/tutorials/tour/case-classes.html
The following paper on pattern-matching inspired the automaton approach to code generation:
API Documentation
Match.MatchFailure
Match.extract
Match.match_fieldnames
Match.@__match__
Match.@ismatch
Match.@match
Match.@match_fail
Match.@match_fail
Match.@match_return
Match.@match_return
Match.MatchFailure
— TypeMatchFailure(value)
Construct an exception to be thrown when a value fails to match a pattern in the @match
macro.
Match.extract
— Functionextract(T::Type, ::Val{n}, value)
Override matching for type T
, destructuring value
into its component fields.
Given a struct pattern T(p1,...,pn)
, if extract(T, ::Val{n}, v)
is implemented for type T
and arity n
, it is called and the result will be matched against the tuple pattern (p1,...,pn)
.
The function should return either a tuple of the correct arity, or nothing
if the match should fail.
Match.match_fieldnames
— Methodmatch_fieldnames(type::Type)
Return a tuple containing the ordered list of the names (as Symbols) of fields that can be matched either nominally or positionally. This list should exclude synthetic fields that are produced by packages such as Mutts and AutoHashEqualsCached. This function may be overridden by the client to hide fields that should not be matched.
Match.@__match__
— MacroUsage:
@__match__ value begin
pattern1 => result1
pattern2 => result2
...
end
Return result
for the first matching pattern
. If there are no matches, throw MatchFailure
. This uses a brute-force code gen strategy, essentially a series of if-else statements. It is used for testing purposes, as a reference for correct semantics. Because it is so simple, we have confidence about its correctness.
Match.@ismatch
— Macro@ismatch value pattern
Return true
if value
matches pattern
, false
otherwise. When returning true
, binds the pattern variables in the enclosing scope.
See also @match
for the syntax of patterns
Examples
julia> struct Point
x
y
end
julia> p = Point(0, 3)
Point(0, 3)
julia> if @ismatch p Point(0, y)
println("On the y axis at y = ", y)
end
On the y axis at y = 3
Guarded patterns ought not be used with @ismatch
, as you can just use &&
instead:
julia> if (@ismatch p Point(x, y)) && x < y
println("The point (", x, ", ", y, ") is in the upper left semiplane")
end
The point (0, 3) is in the upper left semiplane
Match.@match
— Macro@match pattern = value
@match value begin
pattern1 => result1
pattern2 => result2
...
end
Match a given value to a pattern or series of patterns.
This macro has two forms. In the first form
@match pattern = value
Return the value if it matches the pattern, and bind any pattern variables. Otherwise, throw MatchFailure
.
In the second form
@match value begin
pattern1 => result1
pattern2 => result2
...
end
Return result
for the first matching pattern
. If there are no matches, throw MatchFailure
.
To avoid a MatchFailure
exception, write the @match
to handle every possible input. One way to do that is to add a final case with the wildcard pattern _
.
See Also
See also
@match_fail
@match_return
@ismatch
Patterns:
The following syntactic forms can be used in patterns:
_
matches any valuex
(an identifier) matches any value and binds it to the variablex
T(x,y,z)
matches structs of typeT
with fields matching patternsx,y,z
T(y=p)
matches structs of typeT
whosey
field matches patternp
(;x,y,z)
matches values with fieldsx,y,z
binding to variablesx,y,z
(;x=p)
matches values with fieldx
matching patternp
; does not bindx
.(;x::T)
matches values with fieldx
matching pattern::T
; also binds the field tox
[x,y,z]
matchesAbstractArray
s with 3 entries matchingx,y,z
(x,y,z)
matchesTuple
s with 3 entries matchingx,y,z
[x,y...,z]
matchesAbstractArray
s with at least 2 entries, wherex
matches the first entry,z
matches the last entry andy
matches the remaining entries.(x,y...,z)
matchesTuple
s with at least 2 entries, wherex
matches the first entry,z
matches the last entry andy
matches the remaining entries.::T
matches any subtype (isa
) of typeT
x::T
matches any subtype (isa
) of T that also matches patternx
x || y
matches values which match either patternx
ory
(only variables which exist in both branches will be bound)x && y
matches values which match both patternsx
andy
x, if condition end
matches only ifcondition
is true (condition
may use any variables that occur earlier in the pattern eg(x, y, z where x + y > z)
)x where condition
An alternative form forx, if condition end
if condition end
A boolean computed pattern.x && if condition end
is another way of writingx where condition
.1
(a literal value) matches that value usingisequal
r"[a-z]*"
(a regular expression) matches strings that match the regular expression1:10
(a constant range) matches values in that range- Expressions can be interpolated in as constants via standard interpolation syntax
$(x)
. Interpolations may use previously bound variables.
Patterns can be nested arbitrarily.
Repeated variables only match if they are equal (isequal
). For example (x,x)
matches (1,1)
but not (1,2)
.
Examples
julia> value=(1, 2, 3, 4)
(1, 2, 3, 4)
julia> @match (x, y..., z) = value
(1, 2, 3, 4)
julia> x
1
julia> y
(2, 3)
julia> z
4
julia> struct Foo
x::Int64
y::String
end
julia> f(x) = @match x begin
_::String => :string
[a,a,a] => (:all_the_same, a)
[a,bs...,c] => (:at_least_2, a, bs, c)
Foo(x, "foo") where x > 1 => :foo
end
f (generic function with 1 method)
julia> f("foo")
:string
julia> f([1,1,1])
(:all_the_same, 1)
julia> f([1,1])
(:at_least_2, 1, Int64[], 1)
julia> f([1,2,3,4])
(:at_least_2, 1, [2, 3], 4)
julia> f([1])
ERROR: MatchFailure([1])
...
julia> f(Foo(2, "foo"))
:foo
julia> f(Foo(0, "foo"))
ERROR: MatchFailure(Foo(0, "foo"))
...
julia> f(Foo(2, "not a foo"))
ERROR: MatchFailure(Foo(2, "not a foo"))
...
Match.@match_fail
— Macro@match_fail
Inside the result part of a @match case, you can cause the pattern to fail (as if the pattern did not match).
Examples
julia> struct Vect
x
y
end
julia> function norm(v)
@match v begin
Vect(x, y) => begin
if x==0 && y==0
@match_fail
end
l = sqrt(x^2 + y^2)
Vect(x/l, y/l)
end
_ => v
end
end
norm (generic function with 1 method)
julia> norm(Vect(2, 3))
Vect(0.5547001962252291, 0.8320502943378437)
julia> norm(Vect(0, 0))
Vect(0, 0)
Match.@match_fail
— Macro@match_fail
This statement permits early-exit from the value of a @match case. The programmer may write the value as a begin ... end
and then, within the value, the programmer may write
@match_fail
to cause the case to terminate as if its pattern had failed. This permits cases to perform some computation before deciding if the rule "really" matched.
Match.@match_return
— Macro@match_return value
Inside the result part of a @match case, you can return a given value early.
Examples
julia> struct Vect
x
y
end
julia> function norm(v)
@match v begin
Vect(x, y) => begin
if x==0 && y==0
@match_return v
end
l = sqrt(x^2 + y^2)
Vect(x/l, y/l)
end
_ => v
end
end
norm (generic function with 1 method)
julia> norm(Vect(2, 3))
Vect(0.5547001962252291, 0.8320502943378437)
julia> norm(Vect(0, 0))
Vect(0, 0)
Match.@match_return
— Macro@match_return value
This statement permits early-exit from the value of a @match case. The programmer may write the value as a begin ... end
and then, within the value, the programmer may write
@match_return value
to terminate the value expression early with success, with the given value.