diff --git a/.gitignore b/.gitignore index 45b0b95..60c62a9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ .DS_Store /Manifest.toml /dev/ +.julia *.jl.cov *.jl.*.cov diff --git a/README.md b/README.md index 9c78c3f..47b5d89 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,7 @@ Note that unlike the _assignment syntax_, this does not create any variable bind * `_` matches anything * `foo` matches anything, binds value to `foo` +* `~Foo(x,y,z)` calls the extractor function `unapply_Foo(value)` which returns a tuple matching `(x,y,z)` * `Foo(x,y,z)` matches structs of type `Foo` with fields matching `x,y,z` * `Foo(y=1)` matches structs of type `Foo` whose `y` field equals `1` * `[x,y,z]` matches `AbstractArray`s with 3 entries matching `x,y,z` @@ -112,6 +113,81 @@ Patterns can be nested arbitrarily. Repeated variables only match if they are equal (`==`). For example `(x,x)` matches `(1,1)` but not `(1,2)`. +### Extractors + +Patterns can use _extractor functions_ (also known as _active patterns_). +These are just any function that takes a value to match and returns either `nothing` (indicating match failure) +or a tuple that decomposes the value. The tuple is then matched against other patterns. + +An extractor function must take one argument--the value to be matched against--and should return +a `Union{Tuple{T,...}, Nothing}`. Returning `nothing` indicates the extractor does not match. +If a tuple is returned, the components are matched against the subpatterns of the pattern. +Extractor functions should have names of the form `unapply_X`, for some name `X`. + +Extractor patterns are written `~X(p1, p2, ...)`, where `unapply_X` is the name of the extractor function +and `p1`, `p2`, etc., are subpatterns to match against the returned tuple. + +For example, to destruct an array into its head and tail, one could write the following function: + +```julia +function unapply_Cons(xs) + if isempty(xs) + return nothing + else + return ([xs[1], xs[2:end]]) + end +end + +@match [1,2,3] begin + ~Cons(x, xs) => @assert x == 1 && xs == [2,3] +end +``` + +Here's an extractor that extracts the polar coordinates of a Cartesian point: + +```julia +function unapply_Polar(p) + @match p begin + (x, y) => + begin + r = sqrt(x^2+y^2) + theta = atan(y, x) + return (r, theta) + end + _ => return nothing + end +end + +@match (1,1) begin + ~Polar(r, theta) => @assert r == sqrt(2) && theta == pi/4 +end +``` + +Extractors can even be higher-order. + +```julia +function unapply_Re(r::Regex) + x -> begin + m = match(r, x) + if m == nothing + return nothing + else + return tuple(m.captures...) + end + end +end + +@match "abc123def" begin + ~Re(r"(\w+?)(\d+)(\w+)")(a,x,d) => + begin + @assert a == "abc" + @assert x == "123" + @assert d == "def" + end + _ => @assert false +end +``` + ## Differences from [Match.jl](https://github.com/kmsquire/Match.jl) This package was branched from the original [Match.jl](https://github.com/kmsquire/Match.jl). It now differs in several ways: @@ -122,5 +198,6 @@ This package was branched from the original [Match.jl](https://github.com/kmsqui * The syntax for guards is `x where x > 1` instead of `x, if x > 1 end` and can occur anywhere in a pattern. * Structs can be matched by field-names, allowing partial matches: `@match Foo(1,2) begin Foo(y=2) => :ok end` returns `:ok`. * Patterns support interpolation, ie `let x=1; @match ($x,$(x+1)) = (1,2); end` is a match. +* Extractor functions can be used in patterns. * No support (yet) for matching `Regex` or `UnitRange`. * No support (yet) for matching against multidimensional arrays - all array patterns use linear indexing. diff --git a/src/Rematch.jl b/src/Rematch.jl index 7931472..a5ec8f2 100644 --- a/src/Rematch.jl +++ b/src/Rematch.jl @@ -28,10 +28,31 @@ fieldcount. fieldcount(T) end -function handle_destruct_fields(value::Symbol, pattern, subpatterns, len, get::Symbol, bound::Set{Symbol}, asserts::Vector{Expr}; allow_splat=true) +""" + handle_destruct_fields(value, pattern, subpatterns, len, get, bound, asserts; allow_splat=true) + +Destruct a tuple, vector, or struct `value` with the given `pattern`. +Returns a boolean expression that evaluates to true if the pattern matches. +Variables bound in the pattern are added to the `bound` set. +Assertions are added to the `asserts` vector. + +The value is indexed by `get` from 1 to `len` and element i is matched against `subpattern` i. +If `allow_splat` is true, one `...` is allowed among the subpatterns. +""" +function handle_destruct_fields( + value::Symbol, + pattern, + subpatterns, + len, + get::Symbol, + bound::Set{Symbol}, + asserts::Vector{Expr}; + allow_splat=true +) # NOTE we assume `len` is cheap fields = [] seen_splat = false + for (i,subpattern) in enumerate(subpatterns) if (subpattern isa Expr) && (subpattern.head == :(...)) @assert allow_splat && !seen_splat "Too many ... in pattern $pattern" @@ -47,7 +68,8 @@ function handle_destruct_fields(value::Symbol, pattern, subpatterns, len, get::S push!(fields, (i, subpattern)) end end - Expr(:&&, + + return Expr(:&&, if seen_splat :($len >= $(length(subpatterns)-1)) else @@ -59,65 +81,85 @@ function handle_destruct_fields(value::Symbol, pattern, subpatterns, len, get::S end) end +""" +handle_destruct(value, pattern, bound, asserts) + +Destruct `value` with the given `pattern`. + +The pattern is compiled to a boolean expression which evaluates to `true` if the +pattern matches. Variables bound in the pattern are added to the `bound` set. +Assertions are added to the `asserts` vector. +""" function handle_destruct(value::Symbol, pattern, bound::Set{Symbol}, asserts::Vector{Expr}) if pattern == :(_) # wildcard - true + return true elseif !(pattern isa Expr || pattern isa Symbol) || pattern == :nothing || @capture(pattern, _quote_macrocall) || @capture(pattern, Symbol(_)) # constant - quote + return quote $value == $pattern end elseif (pattern isa Expr && pattern.head == :$) # interpolated value - quote + return quote $value == $(esc(pattern.args[1])) end elseif @capture(pattern, subpattern_Symbol) # variable + # if the pattern doesn't match, we don't want to set the variable # so for now just set a temp variable our_sym = Symbol("variable_$pattern") + if pattern in bound # already bound, check that this value matches - quote + return quote $our_sym == $value end else # bind push!(bound, pattern) - quote + return quote $our_sym = $value; true end end - elseif @capture(pattern, subpattern1_ || subpattern2_) || (@capture(pattern, f_(subpattern1_, subpattern2_)) && f == :|) + elseif @capture(pattern, subpattern1_ || subpattern2_) || + (@capture(pattern, f_(subpattern1_, subpattern2_)) && f == :|) # disjunction + # need to only bind variables which exist in both branches bound1 = copy(bound) bound2 = copy(bound) + body1 = handle_destruct(value, subpattern1, bound1, asserts) body2 = handle_destruct(value, subpattern2, bound2, asserts) union!(bound, intersect(bound1, bound2)) - quote + + return quote $body1 || $body2 end - elseif @capture(pattern, subpattern1_ && subpattern2_) || (@capture(pattern, f_(subpattern1_, subpattern2_)) && f == :&) + elseif @capture(pattern, subpattern1_ && subpattern2_) || + (@capture(pattern, f_(subpattern1_, subpattern2_)) && f == :&) # conjunction + body1 = handle_destruct(value, subpattern1, bound, asserts) body2 = handle_destruct(value, subpattern2, bound, asserts) - quote + + return quote $body1 && $body2 end elseif @capture(pattern, _where) # guard @assert length(pattern.args) == 2 + subpattern = pattern.args[1] guard = pattern.args[2] - quote + + return quote $(handle_destruct(value, subpattern, bound, asserts)) && let $(bound...) # bind variables locally so they can be used in the guard @@ -127,59 +169,162 @@ function handle_destruct(value::Symbol, pattern, bound::Set{Symbol}, asserts::Ve $(esc(guard)) end end + elseif @capture(pattern, ~p_) && @capture(p, f_(subpatterns__)) + # Extractor call. + # Extractor calls must begin with `~`. + + result = gensym("unapply") + len = length(subpatterns) + + function replace_call(f) + if f isa Symbol + return Symbol("unapply_$f") + elseif @capture(f, g_(args__)) + h = replace_call(g) + if h == nothing + return nothing + end + return Expr(:call, replace_call(g), args...) + else + return nothing + end + end + + f = replace_call(f) + + if f === nothing + error("Extractor pattern could not be parsed: should be a call to a named function.") + end + + # The function should take one argument and return a `Union{Tuple{T,...}, Nothing}`. + # If the argument does not match, nothing should be returned. + # If the argument does match, a tuple should be returned, which is then matched against + # the subpatterns. + if len == 0 + # If there are no subpatterns, the result value is just checked for + # nothingness. + return quote + $(esc(f))($value) !== nothing + end + else + # If there are subpatterns, the result value is matched against a tuple pattern. + return quote + begin + $result = $(esc(f))($value) + $result !== nothing && + ($result isa Tuple) && + $(handle_destruct_fields( + result, + pattern, + subpatterns, + :(length($result)), + :getindex, + bound, + asserts; + allow_splat=true + )) + end + end + end elseif @capture(pattern, T_(subpatterns__)) - # struct + # Struct. + + # Since extractors and struct patterns have similar syntax, we require structs to + # be either symbols or qualified names. We then eval the struct name to check if + # it is a DataType. Extractors should be Functions instead, and can have arbitrary + # syntax. + len = length(subpatterns) - named_fields = [pat.args[1] for pat in subpatterns if (pat isa Expr) && pat.head == :kw] + + named_fields = [pat.args[1] for pat in subpatterns + if (pat isa Expr) && pat.head == :kw] named_count = length(named_fields) - @assert named_count == length(unique(named_fields)) "Pattern $pattern has duplicate named arguments ($(named_fields))." - @assert named_count == 0 || named_count == len "Pattern $pattern mixes named and positional arguments." + + @assert named_count == length(unique(named_fields)) + "Pattern $pattern has duplicate named arguments ($(named_fields))." + @assert named_count == 0 || named_count == len + "Pattern $pattern mixes named and positional arguments." + if named_count == 0 # Pattern uses positional arguments to refer to fields e.g. Foo(0, true) expected_fieldcount = gensym("$(T)_expected_fieldcount") actual_fieldcount = gensym("$(T)_actual_fieldcount") push!(asserts, quote - if typeof($(esc(T))) <: Function - throw(LoadError("Attempted to match on a function", @__LINE__, AssertionError("Incorrect match usage"))) + t = $(esc(T)) + if typeof(t) <: Function + throw(LoadError("Attempted to match on a function", + @__LINE__, AssertionError("Incorrect match usage"))) end - if !(isstructtype(typeof($(esc(T)))) || issabstracttype(typeof($(esc(T))))) - throw(LoadError("Attempted to match on a pattern that is not a struct", @__LINE__, AssertionError("Incorrect match usage"))) + if !(isstructtype(typeof(t)) || issabstracttype(typeof(t))) + throw(LoadError("Attempted to match on a type that is not a struct", + @__LINE__, AssertionError("Incorrect match usage"))) end # This assertion is necessary: # If $expected_fieldcount < $actual_fieldcount, to catch missing fields. # If $expected_fieldcount > $actual_fieldcount, to avoid a BoundsError. - $expected_fieldcount = evaluated_fieldcount($(esc(T))) + $expected_fieldcount = evaluated_fieldcount(t) $actual_fieldcount = $(esc(len)) if $expected_fieldcount != $actual_fieldcount - error("Pattern field count is $($actual_fieldcount) expected $($expected_fieldcount)") + error("Pattern field count is $($actual_fieldcount), ", + "expected $($expected_fieldcount)") end - + end) else # Pattern uses named arguments to refer to fields e.g. Foo(x=0, y=true) - # Could assert that the expected field names are a subset of the actual field names. - # Can omit the assertion because if the field doesn't exist getfield() will fail with "type $T has no field $field". + # Could assert that the expected field names are a subset of the actual + # field names. Can omit the assertion because if the field doesn't exist + # getfield() will fail with "type $T has no field $field". end - quote - # I would prefer typeof($value) == $(esc(T)) but this doesn't convey type information in Julia 0.6 + + return quote + # I would prefer typeof($value) == $(esc(T)) but this doesn't convey type + # information in Julia 0.6 $value isa $(esc(T)) && - $(handle_destruct_fields(value, pattern, subpatterns, length(subpatterns), :getfield, bound, asserts; allow_splat=false)) + $(handle_destruct_fields( + value, + pattern, + subpatterns, + length(subpatterns), + :getfield, + bound, + asserts; + allow_splat=false + )) end elseif @capture(pattern, (subpatterns__,)) # tuple - quote + return quote ($value isa Tuple) && - $(handle_destruct_fields(value, pattern, subpatterns, :(length($value)), :getindex, bound, asserts; allow_splat=true)) + $(handle_destruct_fields( + value, + pattern, + subpatterns, + :(length($value)), + :getindex, + bound, + asserts; + allow_splat=true + )) end elseif @capture(pattern, [subpatterns__]) # array - quote + return quote ($value isa AbstractArray) && - $(handle_destruct_fields(value, pattern, subpatterns, :(length($value)), :getindex, bound, asserts; allow_splat=true)) + $(handle_destruct_fields( + value, + pattern, + subpatterns, + :(length($value)), + :getindex, + bound, + asserts; + allow_splat=true + )) end elseif @capture(pattern, subpattern_::T_) # typeassert - quote + return quote ($value isa $(esc(T))) && $(handle_destruct(value, subpattern, bound, asserts)) end @@ -193,7 +338,7 @@ function handle_match_eq(expr) asserts = Expr[] bound = Set{Symbol}() body = handle_destruct(:value, pattern, bound, asserts) - quote + return quote $(asserts...) value = $(esc(value)) $body || throw(MatchFailure(value)) @@ -211,7 +356,8 @@ function handle_match_case(value, case, tail, asserts) if @capture(case, pattern_ => result_) bound = Set{Symbol}() body = handle_destruct(:value, pattern, bound, asserts) - quote + + return quote if $body let $(bound...) # export bindings @@ -230,13 +376,15 @@ function handle_match_case(value, case, tail, asserts) end function handle_match_cases(value, match) - tail = :(throw(MatchFailure(value))) if @capture(match, begin cases__ end) + tail = :(throw(MatchFailure(value))) asserts = Expr[] + for case in reverse(cases) tail = handle_match_case(:value, case, tail, asserts) end - quote + + return quote $(asserts...) value = $(esc(value)) $tail @@ -249,7 +397,8 @@ end """ @match pattern = value -If `value` matches `pattern`, bind variables and return `value`. Otherwise, throw `MatchFailure`. +If `value` matches `pattern`, bind variables and return `value`. +Otherwise, throw `MatchFailure`. """ macro match(expr) handle_match_eq(expr) @@ -262,7 +411,8 @@ end ... end -Return `result` for the first matching `pattern`. If there are no matches, throw `MatchFailure`. +Return `result` for the first matching `pattern`. +If there are no matches, throw `MatchFailure`. """ macro match(value, cases) handle_match_cases(value, cases) @@ -273,25 +423,69 @@ Patterns: * `_` matches anything * `foo` matches anything, binds value to `foo` + * `~Foo(x,y,z)` calls the extractor function `unapply_Foo(value)` which returns a tuple + matching `(x,y,z)` * `Foo(x,y,z)` matches structs of type `Foo` with fields matching `x,y,z` * `Foo(y=1)` matches structs of type `Foo` whose `y` field equals `1` * `[x,y,z]` matches `AbstractArray`s with 3 entries matching `x,y,z` * `(x,y,z)` matches `Tuple`s with 3 entries matching `x,y,z` - * `[x,y...,z]` matches `AbstractArray`s with at least 2 entries, where `x` matches the first entry, `z` matches the last entry and `y` matches the remaining entries. - * `(x,y...,z)` matches `Tuple`s with at least 2 entries, where `x` matches the first entry, `z` matches the last entry and `y` matches the remaining entries. + * `[x,y...,z]` matches `AbstractArray`s with at least 2 entries, where `x` matches + the first entry, `z` matches the last entry and `y` matches the remaining entries. + * `(x,y...,z)` matches `Tuple`s with at least 2 entries, where `x` matches the first + entry, `z` matches the last entry and `y` matches the remaining entries. * `_::T` matches any subtype (`isa`) of T - * `x || y` matches values which match either `x` or `y` (only variables which exist in both branches will be bound) + * `x || y` matches values which match either `x` or `y` (only variables which exist in + both branches will be bound) * `x && y` matches values which match both `x` and `y` - * `x where condition` matches only if `condition` is true (`condition` may use any variables that occur earlier in the pattern eg `(x, y, z where x + y > z)`) + * `x where condition` matches only if `condition` is true (`condition` may use any + variables that occur earlier in the pattern eg `(x, y, z where x + y > z)`) * Anything else is treated as a constant and tested for equality - * Expressions can be interpolated in as constants via standard interpolation syntax `\$(x)` + * Expressions can be interpolated in as constants via standard interpolation + syntax `\$(x)` Patterns can be nested arbitrarily. -Repeated variables only match if they are equal (`==`). For example `(x,x)` matches `(1,1)` but not `(1,2)`. +Repeated variables only match if they are equal (`==`). +For example `(x,x)` matches `(1,1)` but not `(1,2)`. + +Extractors: + +An extractor function must take one argument--the value to be matched against--and should +return either a tuple of values or `nothing`. Returning `nothing` indicates the extractor +does not match. For example to destruct an array into its head and tail: + + function unapply_Cons(xs) + if isempty(xs) + return nothing + else + return ([xs[1], xs[2:end]]) + end + end + + @match [1,2,3] begin + ~Cons(x, xs) => @assert x == 1 && xs == [2,3] + end + +Or here's one to extract the polar coordinates of a point: + + function unapply_Polar(p) + @match p begin + (x, y) => + begin + r = sqrt(x^2+y^2) + theta = atan(y, x) + return (r, theta) + end + _ => return nothing + end + end + + @match (1,1) begin + ~Polar(r, theta) => @assert r == sqrt(2) && theta == pi/4 + end """ :(@match) -export @match +export @match, @pattern end diff --git a/test/rematch.jl b/test/rematch.jl index dba87e7..c5a6e83 100644 --- a/test/rematch.jl +++ b/test/rematch.jl @@ -18,32 +18,239 @@ assertion_error = (VERSION >= v"0.7.0-DEV") ? LoadError : AssertionError y end - # test basic match - let x = nothing - @test (@match Foo(x,2) = Foo(1,2)) == Foo(1,2) - @test x == 1 + @eval begin + # test basic match + let x = nothing + @test (@match Foo(x,2) = Foo(1,2)) == Foo(1,2) + @test x == 1 + end + + # variables not bound if match fails + let x = nothing + @test_throws MatchFailure @match Foo(x, 3) = Foo(1,2) + @test x == nothing + end + + # doesn't overwrite variables in outer scope + let x = nothing + @test (@match Foo(1,2) begin + Foo(x,2) => x + end) == 1 + @test x == nothing + end + + # variables not bound if guard fails + let x = nothing + @test_throws MatchFailure @match Foo(1,2) begin + Foo(x, 2) where x != 1 => :ok + end + @test x == nothing + end end +end - # variables not bound if match fails +@testset "Match using extractors" begin + function unapply_sub1(x) tuple(x+1) end + + # sub1(4) == 3 let x = nothing - @test_throws MatchFailure @match Foo(x, 3) = Foo(1,2) + @test (@match 3 begin ~sub1(x) => x end) == 4 @test x == nothing end - # doesn't overwrite variables in outer scope - let x = nothing - @test (@match Foo(1,2) begin - Foo(x,2) => x - end) == 1 - @test x == nothing + function unapply_Cons(xs) + isempty(xs) ? nothing : (xs[1], xs[2:end]) end - # variables not bound if guard fails - let x = nothing - @test_throws MatchFailure @match Foo(1,2) begin - Foo(x, 2) where x != 1 => :ok + # ~Cons(1, ~Cons(4, (~Cons(9, []))) == [1,4,9] + let a = nothing + b = nothing + c = nothing + + @test (@match [1,4,9] begin + ~Cons(a, ~Cons(b, ~Cons(c, []))) => (a,b,c) + end) == (1,4,9) + + @test a == nothing + @test b == nothing + @test c == nothing + end + + @match [1,2,3] begin + ~Cons(x, xs) => + begin + @test x == 1 + @test xs == [2,3] + end + end + + function unapply_Snoc(xs) + isempty(xs) ? nothing : (xs[1:end-1], xs[end]) + end + + let a = nothing + b = nothing + c = nothing + + @test (@match [1,4,9] begin + ~Snoc(~Snoc(~Snoc([], c), b), a) => (a,b,c) + end) == (9,4,1) + + @test a == nothing + @test b == nothing + @test c == nothing + end + + @match [1,2,3] begin + ~Snoc(xs, x) => + begin + @test x == 3 + @test xs == [1,2] + end + end + + function unapply_Polar(p) + @match p begin + (x, y) => + begin + r = sqrt(x^2+y^2) + theta = atan(y, x) + return (r, theta) + end + _ => return nothing end - @test x == nothing + end + + @match (1,1) begin + ~Polar(r, theta) => + begin + @test r == sqrt(2) + @test theta == pi/4 + end + end + + # A more complex extractor. + function unapply_Re(r::Regex) + x -> begin + m = match(r, x) + if m == nothing + return nothing + else + return tuple(m.captures...) + end + end + end + + @match "abc123def" begin + ~Re(r"(\w+?)(\d+)(\w+)")(a,x,d) => + begin + @test a == "abc" + @test x == "123" + @test d == "def" + end + _ => @test false + end + + @match "abc123def" begin + ~Re(r"(\d+)")(x) => @test x == "123" + _ => @test false + end + + @match "abc123def" begin + ~Re(r"\d+")() => @test true + _ => @test false + end +end + +@testset "Red-black trees with extractors" begin + # Okasaki-style red-black trees + @enum Color R B + @eval abstract type Tree end + @eval struct E <: Tree end + @eval struct T <: Tree + color::Color + a::Tree + x + b::Tree + end + + # Extractor for red nodes + function unapply_Red(t::Tree) + @match t begin + # Use color == B since we can't match against an enum value R + T(color, a, x, b) where (color == R) => (a, x, b) + _ => nothing + end + end + + # Extractor for black nodes + function unapply_Blk(t::Tree) + @match t begin + # Use color == B since we can't match against an enum value B + T(color, a, x, b) where (color == B) => (a, x, b) + _ => nothing + end + end + + # Constructors + function Red(a, x, b) T(R, a, x, b) end + function Blk(a, x, b) T(B, a, x, b) end + + function member(x, t::Tree) + @match t begin + E() => false + T(_, a, y, b) where (x < y) => member(x, a) + T(_, a, y, b) where (x > y) => member(x, b) + _ => true + end + end + + function insert(x, s::Tree) + function ins(t) + @match t begin + E() => Red(E(), x, E()) + T(color, a, y, b) where (x < y) => balance(T(color, ins(a), y, b)) + T(color, a, y, b) where (x > y) => balance(T(color, a, y, ins(b))) + t => t + end + end + + @match T(_, a, y, b) = ins(s) + + Blk(a, y, b) + end + + function balance(t::T) + @match t begin + ( ~Blk(~Red(~Red(a, x, b), y, c), z, d) || + ~Blk(~Red(a, x, ~Red(b, y, c)), z, d) || + ~Blk(a, x, ~Red(~Red(b, y, c), z, d)) || + ~Blk(a, x, ~Red(b, y, ~Red(c, z, d))) ) => Red(Blk(a, x, b), y, Blk(c, z, d)) + t => t + end + end + + function height(t::Tree) + @match t begin + E() => 0 + T(_, a, x, b) => max(height(a), height(b)) + 1 + end + end + + @test member(3, insert(3, E())) + + n = 100 + + t = E() + for x in 1:n + t = insert(x, t) + end + + theoretical_max_height = 2 * floor(log(2, n+1)) + @test height(t) <= theoretical_max_height + + for x in 1:n + @test member(x, t) end end