From 6c89eabd9717338f10847cea2c491ac515a3cfa5 Mon Sep 17 00:00:00 2001 From: Nate Nystrom Date: Fri, 7 Feb 2020 12:43:51 +0100 Subject: [PATCH 01/20] Added support for extractor functions. --- README.md | 52 +++++++++++++++++- src/Rematch.jl | 138 ++++++++++++++++++++++++++++++++++++------------ test/rematch.jl | 63 ++++++++++++++++++++++ 3 files changed, 217 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 9c78c3f..3ee77a3 100644 --- a/README.md +++ b/README.md @@ -95,8 +95,9 @@ 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)` matches structs of type `Foo` with fields matching `x,y,z` -* `Foo(y=1)` matches structs of type `Foo` whose `y` field equals `1` +* `foo(x,y,z)` calls the extractor function `foo(value)` which returns a tuple matching `(x,y,z)`; the function name must be lowercase +* `Foo(x,y,z)` matches structs of type `Foo` with fields matching `x,y,z`; struct names must be uppercase +* `Foo(y=1)` matches structs of type `Foo` whose `y` field equals `1`; struct names must be uppercase * `[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. @@ -112,6 +113,52 @@ 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 have a lowercase name to distinguish it from a struct name. + +An extractor function must take one argument--the value to be matched against--and should return either +one value (for nullary and unary patterns), or a tuple of values (for 2+-ary patterns). +Returning `nothing` indicates the extractor does not match. +For example to destruct an array into its head and tail: + +```julia +function cons(xs) + if isempty(xs) + nothing + else + ([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 that extracts the polar coordinates of a cartesian point: + +```julia +function polar(p) + @match p begin + (x, y) => + begin + r = sqrt(x^2+y^2) + theta = atan(y, x) + (r, theta) + end + _ => nothing + end +end + +@match (1,1) begin + polar(r, theta) => @assert r == sqrt(2) && theta == pi/4 +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 +169,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..bb839d2 100644 --- a/src/Rematch.jl +++ b/src/Rematch.jl @@ -128,42 +128,74 @@ function handle_destruct(value::Symbol, pattern, bound::Set{Symbol}, asserts::Ve end end elseif @capture(pattern, T_(subpatterns__)) - # struct - len = length(subpatterns) - 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." - 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"))) + # struct or extractor call + # structs are uppercase, extractor calls are lowercase + if length(string(T)) > 0 && islowercase(first(string(T))) + result = gensym("unapply") + len = length(subpatterns) + # Extractor call. + # The function should take one argument and return either ``nothing`` if the argument does not match + # or a tuple if it does match. + # If there are no subpatterns, the function should return a boolean. + if len == 0 + quote + !isnothing($(esc(T))($value)) 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"))) + elseif len == 1 + quote + begin + $result = $(esc(T))($value) + !isnothing($result) && $(handle_destruct(result, subpatterns[1], bound, asserts)) + end 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))) - $actual_fieldcount = $(esc(len)) - if $expected_fieldcount != $actual_fieldcount - error("Pattern field count is $($actual_fieldcount) expected $($expected_fieldcount)") + else + quote + begin + $result = $(esc(T))($value) + !isnothing($result) && + ($result isa Tuple) && + $(handle_destruct_fields(result, pattern, subpatterns, :(length($result)), :getindex, bound, asserts; allow_splat=false)) + end end - - 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". - end - 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)) + # Struct. + len = length(subpatterns) + 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." + 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"))) + 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"))) + 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))) + $actual_fieldcount = $(esc(len)) + if $expected_fieldcount != $actual_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". + end + 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)) + end end elseif @capture(pattern, (subpatterns__,)) # tuple @@ -273,8 +305,9 @@ Patterns: * `_` matches anything * `foo` matches anything, binds value to `foo` - * `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` + * `foo(x,y,z)` calls the extractor function `foo(value)` which returns a tuple matching `(x,y,z)`; the function name must be lowercase + * `Foo(x,y,z)` matches structs of type `Foo` with fields matching `x,y,z`; struct names must be uppercase + * `Foo(y=1)` matches structs of type `Foo` whose `y` field equals `1`; struct names must be uppercase * `[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. @@ -289,6 +322,43 @@ Patterns: 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: + +An extractor function must take one argument--the value to be matched against--and should return either +one value (for nullary and unary patterns), or a tuple of values (for 2+-ary patterns). +Returning `nothing` indicates the extractor does not match. +For example to destruct an array into its head and tail: + + function cons(xs) + if isempty(xs) + nothing + else + ([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 polar(p) + @match p begin + (x, y) => + begin + r = sqrt(x^2+y^2) + theta = atan(y, x) + (r, theta) + end + _ => nothing + end + end + + @match (1,1) begin + polar(r, theta) => @assert r == sqrt(2) && theta == pi/4 + end """ :(@match) diff --git a/test/rematch.jl b/test/rematch.jl index dba87e7..cd2face 100644 --- a/test/rematch.jl +++ b/test/rematch.jl @@ -47,6 +47,69 @@ assertion_error = (VERSION >= v"0.7.0-DEV") ? LoadError : AssertionError end end +@testset "Match using extractors" begin + function sub1(x) x+1 end + + # sub1(4) == 3 + let x = nothing + @test (@match 3 begin + sub1(x) => x + end) == 4 + + @test x == nothing + end + + function cons(xs) + if isempty(xs) + nothing + else + (xs[1], xs[2:end]) + end + end + + # cons(1, cons(2, (cons(3, []))) == [1,2,3] + let a = nothing + b = nothing + c = nothing + + @test (@match [1,4,9] begin + cons(a, cons(b, cons(c, []))) => a + b + c + end) == 14 + + @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 polar(p) + @match p begin + (x, y) => + begin + r = sqrt(x^2+y^2) + theta = atan(y, x) + (r, theta) + end + _ => nothing + end + end + + @match (1,1) begin + polar(r, theta) => + begin + @test r == sqrt(2) + @test theta == pi/4 + end + end +end + @testset "Match Struct by field names" begin # match one struct field by name let x = nothing From 453de9d2129b22acfbcf56a5ffb1d274e64d3764 Mon Sep 17 00:00:00 2001 From: Nate Nystrom Date: Fri, 7 Feb 2020 21:14:21 +0100 Subject: [PATCH 02/20] changed call to length > 0 to !isempty --- src/Rematch.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Rematch.jl b/src/Rematch.jl index bb839d2..aedf611 100644 --- a/src/Rematch.jl +++ b/src/Rematch.jl @@ -130,7 +130,7 @@ function handle_destruct(value::Symbol, pattern, bound::Set{Symbol}, asserts::Ve elseif @capture(pattern, T_(subpatterns__)) # struct or extractor call # structs are uppercase, extractor calls are lowercase - if length(string(T)) > 0 && islowercase(first(string(T))) + if !isempty(string(T)) && islowercase(first(string(T))) result = gensym("unapply") len = length(subpatterns) # Extractor call. From 8d22d8a27f4cf3dabff6fdc98a5a4e1e671cc6c5 Mon Sep 17 00:00:00 2001 From: Nate Nystrom Date: Sat, 8 Feb 2020 17:37:18 +0100 Subject: [PATCH 03/20] updated comments; allow splats in extractor matching --- src/Rematch.jl | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Rematch.jl b/src/Rematch.jl index aedf611..82b7cf4 100644 --- a/src/Rematch.jl +++ b/src/Rematch.jl @@ -134,14 +134,15 @@ function handle_destruct(value::Symbol, pattern, bound::Set{Symbol}, asserts::Ve result = gensym("unapply") len = length(subpatterns) # Extractor call. - # The function should take one argument and return either ``nothing`` if the argument does not match + # The function should take one argument and return either `nothing`, if the argument does not match, # or a tuple if it does match. - # If there are no subpatterns, the function should return a boolean. if len == 0 + # If there are no subpatterns, the result value is just checked for nothingness. quote !isnothing($(esc(T))($value)) end elseif len == 1 + # If there is one subpattern, the result value is just matched against it. quote begin $result = $(esc(T))($value) @@ -149,12 +150,13 @@ function handle_destruct(value::Symbol, pattern, bound::Set{Symbol}, asserts::Ve end end else + # If there is more than one subpattern, the result value is matched against a tuple pattern. quote begin $result = $(esc(T))($value) !isnothing($result) && ($result isa Tuple) && - $(handle_destruct_fields(result, pattern, subpatterns, :(length($result)), :getindex, bound, asserts; allow_splat=false)) + $(handle_destruct_fields(result, pattern, subpatterns, :(length($result)), :getindex, bound, asserts; allow_splat=true)) end end end From f482fe0ef9c9388c1ec555da5503f2d8873d4365 Mon Sep 17 00:00:00 2001 From: Nate Nystrom Date: Sat, 8 Feb 2020 17:37:53 +0100 Subject: [PATCH 04/20] whitespace. red-black trees test --- test/rematch.jl | 173 ++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 147 insertions(+), 26 deletions(-) diff --git a/test/rematch.jl b/test/rematch.jl index cd2face..3349f28 100644 --- a/test/rematch.jl +++ b/test/rematch.jl @@ -53,7 +53,7 @@ end # sub1(4) == 3 let x = nothing @test (@match 3 begin - sub1(x) => x + sub1(x) => x end) == 4 @test x == nothing @@ -67,47 +67,168 @@ end end end - # cons(1, cons(2, (cons(3, []))) == [1,2,3] + # 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) == 14 + 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) => + @match [1,2,3] begin + cons(x, xs) => begin - @test x == 1 + @test x == 1 @test xs == [2,3] end - end - - function polar(p) - @match p begin - (x, y) => - begin - r = sqrt(x^2+y^2) - theta = atan(y, x) - (r, theta) - end - _ => nothing - end - end - - @match (1,1) begin - polar(r, theta) => + end + + function snoc(xs) + if isempty(xs) + nothing + else + (xs[1:end-1], xs[end]) + 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 r == sqrt(2) - @test theta == pi/4 + @test x == 3 + @test xs == [1,2] + end + end + + function polar(p) + @match p begin + (x, y) => + begin + r = sqrt(x^2+y^2) + theta = atan(y, x) + (r, theta) + end + _ => nothing + end + end + + @match (1,1) begin + polar(r, theta) => + begin + @test r == sqrt(2) + @test theta == pi/4 + end + end +end + +@testset "Red-black trees" begin + # Okasaki-style red-black trees + @enum Color R B + abstract type Tree end + struct E <: Tree end + struct T <: Tree + color::Color + a::Tree + x + b::Tree + end + + # Extractor for red nodes + function 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 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 + 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 = 1 + + 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 @testset "Match Struct by field names" begin From 9a6094b2f15bc28a10305479400416ce21bcd96c Mon Sep 17 00:00:00 2001 From: Nate Nystrom Date: Sat, 8 Feb 2020 17:40:10 +0100 Subject: [PATCH 05/20] fixed red-black tree test size --- test/rematch.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/rematch.jl b/test/rematch.jl index 3349f28..d62b3f2 100644 --- a/test/rematch.jl +++ b/test/rematch.jl @@ -216,7 +216,7 @@ end @test member(3, insert(3, E())) - n = 1 + n = 100 t = E() for x in 1:n From edb44cc37ed4b6a750f4ffe28085f4f19ddba793 Mon Sep 17 00:00:00 2001 From: Nate Nystrom Date: Thu, 12 Mar 2020 11:37:52 +0100 Subject: [PATCH 06/20] comments --- src/Rematch.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Rematch.jl b/src/Rematch.jl index 82b7cf4..7b4e30f 100644 --- a/src/Rematch.jl +++ b/src/Rematch.jl @@ -129,8 +129,8 @@ function handle_destruct(value::Symbol, pattern, bound::Set{Symbol}, asserts::Ve end elseif @capture(pattern, T_(subpatterns__)) # struct or extractor call - # structs are uppercase, extractor calls are lowercase if !isempty(string(T)) && islowercase(first(string(T))) + # Extractor call. result = gensym("unapply") len = length(subpatterns) # Extractor call. @@ -142,7 +142,7 @@ function handle_destruct(value::Symbol, pattern, bound::Set{Symbol}, asserts::Ve !isnothing($(esc(T))($value)) end elseif len == 1 - # If there is one subpattern, the result value is just matched against it. + # If there is just one subpattern, the result value is matched against it. quote begin $result = $(esc(T))($value) From 6e37f0cdb391e3be3f789b4126ff0b1e6132c4c2 Mon Sep 17 00:00:00 2001 From: Nate Nystrom Date: Thu, 12 Mar 2020 11:38:13 +0100 Subject: [PATCH 07/20] add .julia to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) 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 From 6e9c9b8949f7aea1033c2064d67639c4a624981b Mon Sep 17 00:00:00 2001 From: Nate Nystrom Date: Thu, 12 Mar 2020 11:43:57 +0100 Subject: [PATCH 08/20] expand tabs in README --- README.md | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 3ee77a3..6ebf401 100644 --- a/README.md +++ b/README.md @@ -127,15 +127,15 @@ For example to destruct an array into its head and tail: ```julia function cons(xs) - if isempty(xs) - nothing - else - ([xs[1], xs[2:end]]) - end + if isempty(xs) + nothing + else + ([xs[1], xs[2:end]]) + end end @match [1,2,3] begin - cons(x, xs) => @assert x == 1 && xs == [2,3] + cons(x, xs) => @assert x == 1 && xs == [2,3] end ``` @@ -143,19 +143,19 @@ Or, here's one that extracts the polar coordinates of a cartesian point: ```julia function polar(p) - @match p begin - (x, y) => - begin - r = sqrt(x^2+y^2) - theta = atan(y, x) - (r, theta) - end - _ => nothing - end + @match p begin + (x, y) => + begin + r = sqrt(x^2+y^2) + theta = atan(y, x) + (r, theta) + end + _ => nothing + end end @match (1,1) begin - polar(r, theta) => @assert r == sqrt(2) && theta == pi/4 + polar(r, theta) => @assert r == sqrt(2) && theta == pi/4 end ``` From d5a1e2c8aa5b7adb47d4fe4d5443116f422912d7 Mon Sep 17 00:00:00 2001 From: Nate Nystrom Date: Thu, 12 Mar 2020 11:55:03 +0100 Subject: [PATCH 09/20] changed isnothing to === nothing --- src/Rematch.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Rematch.jl b/src/Rematch.jl index 7b4e30f..3b5a7ad 100644 --- a/src/Rematch.jl +++ b/src/Rematch.jl @@ -139,14 +139,14 @@ function handle_destruct(value::Symbol, pattern, bound::Set{Symbol}, asserts::Ve if len == 0 # If there are no subpatterns, the result value is just checked for nothingness. quote - !isnothing($(esc(T))($value)) + $(esc(T))($value) !== nothing end elseif len == 1 # If there is just one subpattern, the result value is matched against it. quote begin $result = $(esc(T))($value) - !isnothing($result) && $(handle_destruct(result, subpatterns[1], bound, asserts)) + $result !== nothing && $(handle_destruct(result, subpatterns[1], bound, asserts)) end end else @@ -154,7 +154,7 @@ function handle_destruct(value::Symbol, pattern, bound::Set{Symbol}, asserts::Ve quote begin $result = $(esc(T))($value) - !isnothing($result) && + $result !== nothing && ($result isa Tuple) && $(handle_destruct_fields(result, pattern, subpatterns, :(length($result)), :getindex, bound, asserts; allow_splat=true)) end From 24f9fde1b49aa070935ad7d30046285149d19f69 Mon Sep 17 00:00:00 2001 From: Nate Nystrom Date: Thu, 12 Mar 2020 11:59:45 +0100 Subject: [PATCH 10/20] @eval types in tests (0.7 compat issue) --- test/rematch.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/rematch.jl b/test/rematch.jl index d62b3f2..d425044 100644 --- a/test/rematch.jl +++ b/test/rematch.jl @@ -142,9 +142,9 @@ end @testset "Red-black trees" begin # Okasaki-style red-black trees @enum Color R B - abstract type Tree end - struct E <: Tree end - struct T <: Tree + @eval abstract type Tree end + @eval struct E <: Tree end + @eval struct T <: Tree color::Color a::Tree x From 19bb13c1d91a30cbd078f40ee57e6197cd6ce917 Mon Sep 17 00:00:00 2001 From: Nate Nystrom Date: Tue, 21 Apr 2020 10:23:08 +0200 Subject: [PATCH 11/20] style fixes, mostly line lengths --- src/Rematch.jl | 146 +++++++++++++++++++++++++++++++++++------------- test/rematch.jl | 16 ++---- 2 files changed, 112 insertions(+), 50 deletions(-) diff --git a/src/Rematch.jl b/src/Rematch.jl index 3b5a7ad..fdf52fb 100644 --- a/src/Rematch.jl +++ b/src/Rematch.jl @@ -28,7 +28,16 @@ fieldcount. fieldcount(T) end -function handle_destruct_fields(value::Symbol, pattern, subpatterns, len, get::Symbol, bound::Set{Symbol}, asserts::Vector{Expr}; allow_splat=true) +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 @@ -94,7 +103,8 @@ function handle_destruct(value::Symbol, pattern, bound::Set{Symbol}, asserts::Ve 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) @@ -105,7 +115,8 @@ function handle_destruct(value::Symbol, pattern, bound::Set{Symbol}, asserts::Ve 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) @@ -134,10 +145,11 @@ function handle_destruct(value::Symbol, pattern, bound::Set{Symbol}, asserts::Ve result = gensym("unapply") len = length(subpatterns) # Extractor call. - # The function should take one argument and return either `nothing`, if the argument does not match, - # or a tuple if it does match. + # The function should take one argument and return either `nothing`, if the + # argument does not match, or a tuple if it does match. if len == 0 - # If there are no subpatterns, the result value is just checked for nothingness. + # If there are no subpatterns, the result value is just checked for + # nothingness. quote $(esc(T))($value) !== nothing end @@ -146,70 +158,117 @@ function handle_destruct(value::Symbol, pattern, bound::Set{Symbol}, asserts::Ve quote begin $result = $(esc(T))($value) - $result !== nothing && $(handle_destruct(result, subpatterns[1], bound, asserts)) + $result !== nothing && + $(handle_destruct(result, subpatterns[1], bound, asserts)) end end else - # If there is more than one subpattern, the result value is matched against a tuple pattern. + # If there is more than one subpattern, the result value is matched + # against a tuple pattern. quote begin $result = $(esc(T))($value) $result !== nothing && ($result isa Tuple) && - $(handle_destruct_fields(result, pattern, subpatterns, :(length($result)), :getindex, bound, asserts; allow_splat=true)) + $(handle_destruct_fields( + result, + pattern, + subpatterns, + :(length($result)), + :getindex, + bound, + asserts; + allow_splat=true + )) end end end else # Struct. 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 + # 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 end elseif @capture(pattern, (subpatterns__,)) # tuple 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 ($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 @@ -283,7 +342,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) @@ -296,7 +356,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) @@ -307,29 +368,38 @@ Patterns: * `_` matches anything * `foo` matches anything, binds value to `foo` - * `foo(x,y,z)` calls the extractor function `foo(value)` which returns a tuple matching `(x,y,z)`; the function name must be lowercase - * `Foo(x,y,z)` matches structs of type `Foo` with fields matching `x,y,z`; struct names must be uppercase - * `Foo(y=1)` matches structs of type `Foo` whose `y` field equals `1`; struct names must be uppercase + * `foo(x,y,z)` calls the extractor function `foo(value)` which returns a tuple + matching `(x,y,z)`; the function name must be lowercase + * `Foo(x,y,z)` matches structs of type `Foo` with fields matching `x,y,z`; + struct names must be uppercase + * `Foo(y=1)` matches structs of type `Foo` whose `y` field equals `1`; + struct names must be uppercase * `[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 -one value (for nullary and unary patterns), or a tuple of values (for 2+-ary patterns). -Returning `nothing` indicates the extractor does not match. +An extractor function must take one argument--the value to be matched against--and should +return either one value (for nullary and unary patterns), or a tuple of values (for 2+-ary +patterns). Returning `nothing` indicates the extractor does not match. For example to destruct an array into its head and tail: function cons(xs) diff --git a/test/rematch.jl b/test/rematch.jl index d425044..2ce74fc 100644 --- a/test/rematch.jl +++ b/test/rematch.jl @@ -60,11 +60,7 @@ end end function cons(xs) - if isempty(xs) - nothing - else - (xs[1], xs[2:end]) - end + isempty(xs) ? nothing : (xs[1], xs[2:end]) end # cons(1, cons(4, (cons(9, []))) == [1,4,9] @@ -90,11 +86,7 @@ end end function snoc(xs) - if isempty(xs) - nothing - else - (xs[1:end-1], xs[end]) - end + isempty(xs) ? nothing : (xs[1:end-1], xs[end]) end let a = nothing @@ -124,9 +116,9 @@ end begin r = sqrt(x^2+y^2) theta = atan(y, x) - (r, theta) + return (r, theta) end - _ => nothing + _ => return nothing end end From e6969401f0a5d462eb7acfd0e62bfb1bed5fb376 Mon Sep 17 00:00:00 2001 From: Nate Nystrom Date: Tue, 21 Apr 2020 10:56:53 +0200 Subject: [PATCH 12/20] added docstrings and more style fixes --- src/Rematch.jl | 79 +++++++++++++++++++++++++++++++++++++------------- 1 file changed, 59 insertions(+), 20 deletions(-) diff --git a/src/Rematch.jl b/src/Rematch.jl index fdf52fb..cb14319 100644 --- a/src/Rematch.jl +++ b/src/Rematch.jl @@ -28,6 +28,17 @@ fieldcount. fieldcount(T) end +""" + 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, @@ -41,6 +52,7 @@ function handle_destruct_fields( # 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" @@ -56,7 +68,8 @@ function handle_destruct_fields( push!(fields, (i, subpattern)) end end - Expr(:&&, + + return Expr(:&&, if seen_splat :($len >= $(length(subpatterns)-1)) else @@ -68,37 +81,48 @@ function handle_destruct_fields( 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 @@ -106,29 +130,36 @@ function handle_destruct(value::Symbol, pattern, bound::Set{Symbol}, asserts::Ve 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 == :&) # 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 @@ -142,20 +173,21 @@ function handle_destruct(value::Symbol, pattern, bound::Set{Symbol}, asserts::Ve # struct or extractor call if !isempty(string(T)) && islowercase(first(string(T))) # Extractor call. + result = gensym("unapply") len = length(subpatterns) - # Extractor call. + # The function should take one argument and return either `nothing`, if the # argument does not match, or a tuple if it does match. if len == 0 # If there are no subpatterns, the result value is just checked for # nothingness. - quote + return quote $(esc(T))($value) !== nothing end elseif len == 1 # If there is just one subpattern, the result value is matched against it. - quote + return quote begin $result = $(esc(T))($value) $result !== nothing && @@ -165,7 +197,7 @@ function handle_destruct(value::Symbol, pattern, bound::Set{Symbol}, asserts::Ve else # If there is more than one subpattern, the result value is matched # against a tuple pattern. - quote + return quote begin $result = $(esc(T))($value) $result !== nothing && @@ -186,13 +218,16 @@ function handle_destruct(value::Symbol, pattern, bound::Set{Symbol}, asserts::Ve else # Struct. len = length(subpatterns) + 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." + if named_count == 0 # Pattern uses positional arguments to refer to fields e.g. Foo(0, true) expected_fieldcount = gensym("$(T)_expected_fieldcount") @@ -224,7 +259,8 @@ function handle_destruct(value::Symbol, pattern, bound::Set{Symbol}, asserts::Ve # 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 + + return quote # I would prefer typeof($value) == $(esc(T)) but this doesn't convey type # information in Julia 0.6 $value isa $(esc(T)) && @@ -242,7 +278,7 @@ function handle_destruct(value::Symbol, pattern, bound::Set{Symbol}, asserts::Ve end elseif @capture(pattern, (subpatterns__,)) # tuple - quote + return quote ($value isa Tuple) && $(handle_destruct_fields( value, @@ -257,7 +293,7 @@ function handle_destruct(value::Symbol, pattern, bound::Set{Symbol}, asserts::Ve end elseif @capture(pattern, [subpatterns__]) # array - quote + return quote ($value isa AbstractArray) && $(handle_destruct_fields( value, @@ -272,7 +308,7 @@ function handle_destruct(value::Symbol, pattern, bound::Set{Symbol}, asserts::Ve end elseif @capture(pattern, subpattern_::T_) # typeassert - quote + return quote ($value isa $(esc(T))) && $(handle_destruct(value, subpattern, bound, asserts)) end @@ -286,7 +322,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)) @@ -304,7 +340,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 @@ -326,10 +363,12 @@ function handle_match_cases(value, match) tail = :(throw(MatchFailure(value))) if @capture(match, begin cases__ end) asserts = Expr[] + for case in reverse(cases) tail = handle_match_case(:value, case, tail, asserts) end - quote + + return quote $(asserts...) value = $(esc(value)) $tail From d2ea77d1b8298c0f9954f276329db4cf5ef03486 Mon Sep 17 00:00:00 2001 From: Nate Nystrom Date: Mon, 11 May 2020 20:54:17 +0200 Subject: [PATCH 13/20] Changed syntax of extractor patterns to require a ~ in front of the pattern. This allows again struct names to be lowercase. Extractor functions can now take arguments. --- README.md | 23 ++-- src/Rematch.jl | 296 ++++++++++++++++++++++++++---------------------- test/rematch.jl | 124 +++++++++++++------- 3 files changed, 255 insertions(+), 188 deletions(-) diff --git a/README.md b/README.md index 6ebf401..f8c8a17 100644 --- a/README.md +++ b/README.md @@ -95,9 +95,9 @@ 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 `foo(value)` which returns a tuple matching `(x,y,z)`; the function name must be lowercase -* `Foo(x,y,z)` matches structs of type `Foo` with fields matching `x,y,z`; struct names must be uppercase -* `Foo(y=1)` matches structs of type `Foo` whose `y` field equals `1`; struct names must be uppercase +* `~Foo(x,y,z)` calls the extractor function `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. @@ -118,7 +118,6 @@ Repeated variables only match if they are equal (`==`). For example `(x,x)` matc 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 have a lowercase name to distinguish it from a struct name. An extractor function must take one argument--the value to be matched against--and should return either one value (for nullary and unary patterns), or a tuple of values (for 2+-ary patterns). @@ -126,36 +125,36 @@ Returning `nothing` indicates the extractor does not match. For example to destruct an array into its head and tail: ```julia -function cons(xs) +function Cons(xs) if isempty(xs) - nothing + return nothing else - ([xs[1], xs[2:end]]) + return ([xs[1], xs[2:end]]) end end @match [1,2,3] begin - cons(x, xs) => @assert x == 1 && xs == [2,3] + ~Cons(x, xs) => @assert x == 1 && xs == [2,3] end ``` Or, here's one that extracts the polar coordinates of a cartesian point: ```julia -function polar(p) +function Polar(p) @match p begin (x, y) => begin r = sqrt(x^2+y^2) theta = atan(y, x) - (r, theta) + return (r, theta) end - _ => nothing + _ => return nothing end end @match (1,1) begin - polar(r, theta) => @assert r == sqrt(2) && theta == pi/4 + ~Polar(r, theta) => @assert r == sqrt(2) && theta == pi/4 end ``` diff --git a/src/Rematch.jl b/src/Rematch.jl index cb14319..91d742f 100644 --- a/src/Rematch.jl +++ b/src/Rematch.jl @@ -29,7 +29,7 @@ fieldcount. end """ - handle_destruct_fields(value, pattern, subpatterns, len, get, bound, asserts; allow_splat=true) + handle_destruct_fields(__module__, 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. @@ -40,6 +40,7 @@ The value is indexed by `get` from 1 to `len` and element i is matched against ` If `allow_splat` is true, one `...` is allowed among the subpatterns. """ function handle_destruct_fields( + __module__, value::Symbol, pattern, subpatterns, @@ -77,12 +78,30 @@ function handle_destruct_fields( end, @splice (i, (field, subpattern)) in enumerate(fields) quote $(Symbol("$(value)_$i")) = $get($value, $field) - $(handle_destruct(Symbol("$(value)_$i"), subpattern, bound, asserts)) + $(handle_destruct(__module__, Symbol("$(value)_$i"), subpattern, bound, asserts)) end) end """ -handle_destruct(value, pattern, bound, asserts) + is_name_expr(e) + +Returns true if `e` is a symbol or a qualified name expression. +""" +function is_name_expr(e) + if e isa Symbol + return true + end + if e isa QuoteNode + return is_name_expr(e.value) + end + if e isa Expr && e.head == :(.) + return Base.all(is_name_expr, e.args) + end + return false +end + +""" +handle_destruct(__module__, value, pattern, bound, asserts) Destruct `value` with the given `pattern`. @@ -90,7 +109,7 @@ 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}) +function handle_destruct(__module__, value::Symbol, pattern, bound::Set{Symbol}, asserts::Vector{Expr}) if pattern == :(_) # wildcard return true @@ -135,8 +154,8 @@ function handle_destruct(value::Symbol, pattern, bound::Set{Symbol}, asserts::Ve bound1 = copy(bound) bound2 = copy(bound) - body1 = handle_destruct(value, subpattern1, bound1, asserts) - body2 = handle_destruct(value, subpattern2, bound2, asserts) + body1 = handle_destruct(__module__, value, subpattern1, bound1, asserts) + body2 = handle_destruct(__module__, value, subpattern2, bound2, asserts) union!(bound, intersect(bound1, bound2)) return quote @@ -146,8 +165,8 @@ function handle_destruct(value::Symbol, pattern, bound::Set{Symbol}, asserts::Ve (@capture(pattern, f_(subpattern1_, subpattern2_)) && f == :&) # conjunction - body1 = handle_destruct(value, subpattern1, bound, asserts) - body2 = handle_destruct(value, subpattern2, bound, asserts) + body1 = handle_destruct(__module__, value, subpattern1, bound, asserts) + body2 = handle_destruct(__module__, value, subpattern2, bound, asserts) return quote $body1 && $body2 @@ -160,7 +179,7 @@ function handle_destruct(value::Symbol, pattern, bound::Set{Symbol}, asserts::Ve guard = pattern.args[2] return quote - $(handle_destruct(value, subpattern, bound, asserts)) && + $(handle_destruct(__module__, value, subpattern, bound, asserts)) && let $(bound...) # bind variables locally so they can be used in the guard $(@splice variable in bound quote @@ -169,118 +188,128 @@ 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. + + # Structs and extractor calls have the same syntax. We distinguish them + # by evaluating the symbol and checking if it's a function. + # Note that if the extractor is a complex expression, it is evaluated now. + + result = gensym("unapply") + len = length(subpatterns) + + # The function should take one argument and return either `nothing`, if the + # argument does not match, or a tuple if it does match. + if len == 0 + # If there are no subpatterns, the result value is just checked for + # nothingness. + return quote + $(esc(f))($value) !== nothing + end + elseif len == 1 + # If there is just one subpattern, the result value is matched against it. + return quote + begin + $result = $(esc(f))($value) + $result !== nothing && + $(handle_destruct(__module__, result, subpatterns[1], bound, asserts)) + end + end + else + # If there is more than one subpattern, the result value is matched + # against a tuple pattern. + return quote + begin + $result = $(esc(f))($value) + $result !== nothing && + ($result isa Tuple) && + $(handle_destruct_fields( + __module__, + result, + pattern, + subpatterns, + :(length($result)), + :getindex, + bound, + asserts; + allow_splat=true + )) + end + end + end elseif @capture(pattern, T_(subpatterns__)) - # struct or extractor call - if !isempty(string(T)) && islowercase(first(string(T))) - # Extractor call. - - result = gensym("unapply") - len = length(subpatterns) - - # The function should take one argument and return either `nothing`, if the - # argument does not match, or a tuple if it does match. - if len == 0 - # If there are no subpatterns, the result value is just checked for - # nothingness. - return quote - $(esc(T))($value) !== nothing + # 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_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." + + 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 + t = $(esc(T)) + if typeof(t) <: Function + throw(LoadError("Attempted to match on a function", + @__LINE__, AssertionError("Incorrect match usage"))) end - elseif len == 1 - # If there is just one subpattern, the result value is matched against it. - return quote - begin - $result = $(esc(T))($value) - $result !== nothing && - $(handle_destruct(result, subpatterns[1], bound, asserts)) - end + 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 - else - # If there is more than one subpattern, the result value is matched - # against a tuple pattern. - return quote - begin - $result = $(esc(T))($value) - $result !== nothing && - ($result isa Tuple) && - $(handle_destruct_fields( - result, - pattern, - subpatterns, - :(length($result)), - :getindex, - bound, - asserts; - allow_splat=true - )) - 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(t) + $actual_fieldcount = $(esc(len)) + if $expected_fieldcount != $actual_fieldcount + error("Pattern field count is $($actual_fieldcount), ", + "expected $($expected_fieldcount)") end - end - else - # Struct. - len = length(subpatterns) - - 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." - - 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 - t = $(esc(T)) - if typeof(t) <: Function - throw(LoadError("Attempted to match on a function", - @__LINE__, AssertionError("Incorrect match usage"))) - end - 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(t) - $actual_fieldcount = $(esc(len)) - if $expected_fieldcount != $actual_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". - 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". + end - 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 - )) - end + 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( + __module__, + value, + pattern, + subpatterns, + length(subpatterns), + :getfield, + bound, + asserts; + allow_splat=false + )) end elseif @capture(pattern, (subpatterns__,)) # tuple return quote ($value isa Tuple) && $(handle_destruct_fields( + __module__, value, pattern, subpatterns, @@ -296,6 +325,7 @@ function handle_destruct(value::Symbol, pattern, bound::Set{Symbol}, asserts::Ve return quote ($value isa AbstractArray) && $(handle_destruct_fields( + __module__, value, pattern, subpatterns, @@ -310,18 +340,18 @@ function handle_destruct(value::Symbol, pattern, bound::Set{Symbol}, asserts::Ve # typeassert return quote ($value isa $(esc(T))) && - $(handle_destruct(value, subpattern, bound, asserts)) + $(handle_destruct(__module__, value, subpattern, bound, asserts)) end else error("Unrecognized pattern syntax: $pattern") end end -function handle_match_eq(expr) +function handle_match_eq(__module__, expr) if @capture(expr, pattern_ = value_) asserts = Expr[] bound = Set{Symbol}() - body = handle_destruct(:value, pattern, bound, asserts) + body = handle_destruct(__module__, :value, pattern, bound, asserts) return quote $(asserts...) value = $(esc(value)) @@ -336,10 +366,10 @@ function handle_match_eq(expr) end end -function handle_match_case(value, case, tail, asserts) +function handle_match_case(__module__, value, case, tail, asserts) if @capture(case, pattern_ => result_) bound = Set{Symbol}() - body = handle_destruct(:value, pattern, bound, asserts) + body = handle_destruct(__module__, :value, pattern, bound, asserts) return quote if $body @@ -359,13 +389,13 @@ function handle_match_case(value, case, tail, asserts) end end -function handle_match_cases(value, match) - tail = :(throw(MatchFailure(value))) +function handle_match_cases(__module__, value, match) 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) + tail = handle_match_case(__module__, :value, case, tail, asserts) end return quote @@ -385,7 +415,7 @@ If `value` matches `pattern`, bind variables and return `value`. Otherwise, throw `MatchFailure`. """ macro match(expr) - handle_match_eq(expr) + handle_match_eq(__module__, expr) end """ @@ -399,7 +429,7 @@ Return `result` for the first matching `pattern`. If there are no matches, throw `MatchFailure`. """ macro match(value, cases) - handle_match_cases(value, cases) + handle_match_cases(__module__, value, cases) end """ @@ -407,12 +437,10 @@ Patterns: * `_` matches anything * `foo` matches anything, binds value to `foo` - * `foo(x,y,z)` calls the extractor function `foo(value)` which returns a tuple - matching `(x,y,z)`; the function name must be lowercase - * `Foo(x,y,z)` matches structs of type `Foo` with fields matching `x,y,z`; - struct names must be uppercase - * `Foo(y=1)` matches structs of type `Foo` whose `y` field equals `1`; - struct names must be uppercase + * `~Foo(x,y,z)` calls the extractor function `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 @@ -441,38 +469,38 @@ return either one value (for nullary and unary patterns), or a tuple of values ( patterns). Returning `nothing` indicates the extractor does not match. For example to destruct an array into its head and tail: - function cons(xs) + function Cons(xs) if isempty(xs) - nothing + return nothing else - ([xs[1], xs[2:end]]) + return ([xs[1], xs[2:end]]) end end @match [1,2,3] begin - cons(x, xs) => @assert x == 1 && xs == [2,3] + ~Cons(x, xs) => @assert x == 1 && xs == [2,3] end Or here's one to extract the polar coordinates of a point: - function polar(p) + function Polar(p) @match p begin (x, y) => begin r = sqrt(x^2+y^2) theta = atan(y, x) - (r, theta) + return (r, theta) end - _ => nothing + _ => return nothing end end @match (1,1) begin - polar(r, theta) => @assert r == sqrt(2) && theta == pi/4 + ~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 2ce74fc..9867e65 100644 --- a/test/rematch.jl +++ b/test/rematch.jl @@ -18,32 +18,34 @@ 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 - end + @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 + # 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 + # 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 + # 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 - @test x == nothing end end @@ -53,7 +55,7 @@ end # sub1(4) == 3 let x = nothing @test (@match 3 begin - sub1(x) => x + ~sub1(x) => x end) == 4 @test x == nothing @@ -63,13 +65,13 @@ end isempty(xs) ? nothing : (xs[1], xs[2:end]) end - # cons(1, cons(4, (cons(9, []))) == [1,4,9] + # ~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) + ~cons(a, ~cons(b, ~cons(c, []))) => (a,b,c) end) == (1,4,9) @test a == nothing @@ -78,14 +80,14 @@ end end @match [1,2,3] begin - cons(x, xs) => + ~cons(x, xs) => begin @test x == 1 @test xs == [2,3] end end - function snoc(xs) + function Snoc(xs) isempty(xs) ? nothing : (xs[1:end-1], xs[end]) end @@ -94,7 +96,7 @@ end c = nothing @test (@match [1,4,9] begin - snoc(snoc(snoc([], c), b), a) => (a,b,c) + ~Snoc(~Snoc(~Snoc([], c), b), a) => (a,b,c) end) == (9,4,1) @test a == nothing @@ -103,14 +105,14 @@ end end @match [1,2,3] begin - snoc(xs, x) => + ~Snoc(xs, x) => begin @test x == 3 @test xs == [1,2] end end - function polar(p) + function Polar(p) @match p begin (x, y) => begin @@ -123,15 +125,53 @@ end end @match (1,1) begin - polar(r, theta) => + ~Polar(r, theta) => begin @test r == sqrt(2) @test theta == pi/4 end end + + # A more complex extractor. + function Re(r::Regex) + x -> begin + m = match(r, x) + if m == nothing + return nothing + else + if length(m.captures) == 0 + return true + elseif length(m.captures) == 1 + return m.captures[1] + else + return tuple(m.captures...) + end + 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" begin +@testset "Red-black trees with extractors" begin # Okasaki-style red-black trees @enum Color R B @eval abstract type Tree end @@ -144,7 +184,7 @@ end end # Extractor for red nodes - function red(t::Tree) + function 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) @@ -153,7 +193,7 @@ end end # Extractor for black nodes - function blk(t::Tree) + function 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) @@ -162,8 +202,8 @@ 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 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 @@ -177,7 +217,7 @@ end function insert(x, s::Tree) function ins(t) @match t begin - E() => red(E(), x, E()) + 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 @@ -186,15 +226,15 @@ end @match T(_, a, y, b) = ins(s) - blk(a, y, b) + 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)) + ( ~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 From f9916ec428f2ed69867e2db4f6c65fdc323a2f06 Mon Sep 17 00:00:00 2001 From: Nate Nystrom Date: Thu, 27 Aug 2020 12:42:36 +0200 Subject: [PATCH 14/20] some extractor cleanup: should always return tuples or nothing --- README.md | 39 ++++++++++++++++++++++++++++++++++----- src/Rematch.jl | 23 ++++++----------------- test/rematch.jl | 23 +++++++---------------- 3 files changed, 47 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index f8c8a17..e607bbe 100644 --- a/README.md +++ b/README.md @@ -119,10 +119,14 @@ 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 either -one value (for nullary and unary patterns), or a tuple of values (for 2+-ary patterns). -Returning `nothing` indicates the extractor does not match. -For example to destruct an array into its head and tail: +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 patterns are written `~fn(p1, p2, ...)`, where `fn` 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 Cons(xs) @@ -138,7 +142,7 @@ end end ``` -Or, here's one that extracts the polar coordinates of a cartesian point: +Here's an extractor that extracts the polar coordinates of a Cartesian point: ```julia function Polar(p) @@ -158,6 +162,31 @@ end end ``` +Extractors can even be higher-order. + +```julia +function 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: diff --git a/src/Rematch.jl b/src/Rematch.jl index 91d742f..3f10b58 100644 --- a/src/Rematch.jl +++ b/src/Rematch.jl @@ -190,34 +190,23 @@ function handle_destruct(__module__, value::Symbol, pattern, bound::Set{Symbol}, end elseif @capture(pattern, ~p_) && @capture(p, f_(subpatterns__)) # Extractor call. - - # Structs and extractor calls have the same syntax. We distinguish them - # by evaluating the symbol and checking if it's a function. - # Note that if the extractor is a complex expression, it is evaluated now. + # Extractor calls must begin with `~`. result = gensym("unapply") len = length(subpatterns) - # The function should take one argument and return either `nothing`, if the - # argument does not match, or a tuple if it does match. + # 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 - elseif len == 1 - # If there is just one subpattern, the result value is matched against it. - return quote - begin - $result = $(esc(f))($value) - $result !== nothing && - $(handle_destruct(__module__, result, subpatterns[1], bound, asserts)) - end - end else - # If there is more than one subpattern, the result value is matched - # against a tuple pattern. + # If there are subpatterns, the result value is matched against a tuple pattern. return quote begin $result = $(esc(f))($value) diff --git a/test/rematch.jl b/test/rematch.jl index 9867e65..2fdc69d 100644 --- a/test/rematch.jl +++ b/test/rematch.jl @@ -50,28 +50,25 @@ assertion_error = (VERSION >= v"0.7.0-DEV") ? LoadError : AssertionError end @testset "Match using extractors" begin - function sub1(x) x+1 end + function sub1(x) tuple(x+1) end # sub1(4) == 3 let x = nothing - @test (@match 3 begin - ~sub1(x) => x - end) == 4 - + @test (@match 3 begin ~sub1(x) => x end) == 4 @test x == nothing end - function cons(xs) + function Cons(xs) isempty(xs) ? nothing : (xs[1], xs[2:end]) end - # ~cons(1, ~cons(4, (~cons(9, []))) == [1,4,9] + # ~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) + ~Cons(a, ~Cons(b, ~Cons(c, []))) => (a,b,c) end) == (1,4,9) @test a == nothing @@ -80,7 +77,7 @@ end end @match [1,2,3] begin - ~cons(x, xs) => + ~Cons(x, xs) => begin @test x == 1 @test xs == [2,3] @@ -139,13 +136,7 @@ end if m == nothing return nothing else - if length(m.captures) == 0 - return true - elseif length(m.captures) == 1 - return m.captures[1] - else - return tuple(m.captures...) - end + return tuple(m.captures...) end end end From cddbfdcd5cf018b1a636b6704b6859cc71b1a727 Mon Sep 17 00:00:00 2001 From: Nate Nystrom Date: Thu, 27 Aug 2020 12:58:24 +0200 Subject: [PATCH 15/20] Extractor functions must be named unapply_X. --- README.md | 9 +++++---- src/Rematch.jl | 20 ++++++++++++++++++++ test/rematch.jl | 14 +++++++------- 3 files changed, 32 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index e607bbe..a5c854b 100644 --- a/README.md +++ b/README.md @@ -122,14 +122,15 @@ or a tuple that decomposes the value. The tuple is then matched against other pa 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 `~fn(p1, p2, ...)`, where `fn` is the name of the extractor function +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 Cons(xs) +function unapply_Cons(xs) if isempty(xs) return nothing else @@ -145,7 +146,7 @@ end Here's an extractor that extracts the polar coordinates of a Cartesian point: ```julia -function Polar(p) +function unapply_Polar(p) @match p begin (x, y) => begin @@ -165,7 +166,7 @@ end Extractors can even be higher-order. ```julia -function Re(r::Regex) +function unapply_Re(r::Regex) x -> begin m = match(r, x) if m == nothing diff --git a/src/Rematch.jl b/src/Rematch.jl index 3f10b58..b3ebe50 100644 --- a/src/Rematch.jl +++ b/src/Rematch.jl @@ -195,6 +195,26 @@ function handle_destruct(__module__, value::Symbol, pattern, bound::Set{Symbol}, 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 diff --git a/test/rematch.jl b/test/rematch.jl index 2fdc69d..c5a6e83 100644 --- a/test/rematch.jl +++ b/test/rematch.jl @@ -50,7 +50,7 @@ assertion_error = (VERSION >= v"0.7.0-DEV") ? LoadError : AssertionError end @testset "Match using extractors" begin - function sub1(x) tuple(x+1) end + function unapply_sub1(x) tuple(x+1) end # sub1(4) == 3 let x = nothing @@ -58,7 +58,7 @@ end @test x == nothing end - function Cons(xs) + function unapply_Cons(xs) isempty(xs) ? nothing : (xs[1], xs[2:end]) end @@ -84,7 +84,7 @@ end end end - function Snoc(xs) + function unapply_Snoc(xs) isempty(xs) ? nothing : (xs[1:end-1], xs[end]) end @@ -109,7 +109,7 @@ end end end - function Polar(p) + function unapply_Polar(p) @match p begin (x, y) => begin @@ -130,7 +130,7 @@ end end # A more complex extractor. - function Re(r::Regex) + function unapply_Re(r::Regex) x -> begin m = match(r, x) if m == nothing @@ -175,7 +175,7 @@ end end # Extractor for red nodes - function Red(t::Tree) + 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) @@ -184,7 +184,7 @@ end end # Extractor for black nodes - function Blk(t::Tree) + 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) From a8c3cb849a2d0b3ff77089294b9929289a5e1129 Mon Sep 17 00:00:00 2001 From: Nate Nystrom Date: Thu, 27 Aug 2020 18:04:58 +0200 Subject: [PATCH 16/20] remove dead function --- src/Rematch.jl | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/src/Rematch.jl b/src/Rematch.jl index b3ebe50..5154d92 100644 --- a/src/Rematch.jl +++ b/src/Rematch.jl @@ -82,24 +82,6 @@ function handle_destruct_fields( end) end -""" - is_name_expr(e) - -Returns true if `e` is a symbol or a qualified name expression. -""" -function is_name_expr(e) - if e isa Symbol - return true - end - if e isa QuoteNode - return is_name_expr(e.value) - end - if e isa Expr && e.head == :(.) - return Base.all(is_name_expr, e.args) - end - return false -end - """ handle_destruct(__module__, value, pattern, bound, asserts) From 036dd6587691d71f829c44899afc958d3a1be42b Mon Sep 17 00:00:00 2001 From: Nate Nystrom Date: Thu, 27 Aug 2020 18:08:13 +0200 Subject: [PATCH 17/20] fixed documentation of extractors --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a5c854b..6c29bbc 100644 --- a/README.md +++ b/README.md @@ -95,7 +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 `Foo(value)` which returns a tuple matching `(x,y,z)` +* `~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` From 8b0e92458f7d3fa60f7862694610ab2a6c1c6f3a Mon Sep 17 00:00:00 2001 From: Nate Nystrom Date: Thu, 27 Aug 2020 18:08:35 +0200 Subject: [PATCH 18/20] removed unused __module__ param from functions --- src/Rematch.jl | 41 ++++++++++++++++++----------------------- 1 file changed, 18 insertions(+), 23 deletions(-) diff --git a/src/Rematch.jl b/src/Rematch.jl index 5154d92..7ce7ca8 100644 --- a/src/Rematch.jl +++ b/src/Rematch.jl @@ -29,7 +29,7 @@ fieldcount. end """ - handle_destruct_fields(__module__, value, pattern, subpatterns, len, get, bound, asserts; 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. @@ -40,7 +40,6 @@ The value is indexed by `get` from 1 to `len` and element i is matched against ` If `allow_splat` is true, one `...` is allowed among the subpatterns. """ function handle_destruct_fields( - __module__, value::Symbol, pattern, subpatterns, @@ -78,12 +77,12 @@ function handle_destruct_fields( end, @splice (i, (field, subpattern)) in enumerate(fields) quote $(Symbol("$(value)_$i")) = $get($value, $field) - $(handle_destruct(__module__, Symbol("$(value)_$i"), subpattern, bound, asserts)) + $(handle_destruct(Symbol("$(value)_$i"), subpattern, bound, asserts)) end) end """ -handle_destruct(__module__, value, pattern, bound, asserts) +handle_destruct(value, pattern, bound, asserts) Destruct `value` with the given `pattern`. @@ -91,7 +90,7 @@ 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(__module__, value::Symbol, pattern, bound::Set{Symbol}, asserts::Vector{Expr}) +function handle_destruct(value::Symbol, pattern, bound::Set{Symbol}, asserts::Vector{Expr}) if pattern == :(_) # wildcard return true @@ -136,8 +135,8 @@ function handle_destruct(__module__, value::Symbol, pattern, bound::Set{Symbol}, bound1 = copy(bound) bound2 = copy(bound) - body1 = handle_destruct(__module__, value, subpattern1, bound1, asserts) - body2 = handle_destruct(__module__, value, subpattern2, bound2, asserts) + body1 = handle_destruct(value, subpattern1, bound1, asserts) + body2 = handle_destruct(value, subpattern2, bound2, asserts) union!(bound, intersect(bound1, bound2)) return quote @@ -147,8 +146,8 @@ function handle_destruct(__module__, value::Symbol, pattern, bound::Set{Symbol}, (@capture(pattern, f_(subpattern1_, subpattern2_)) && f == :&) # conjunction - body1 = handle_destruct(__module__, value, subpattern1, bound, asserts) - body2 = handle_destruct(__module__, value, subpattern2, bound, asserts) + body1 = handle_destruct(value, subpattern1, bound, asserts) + body2 = handle_destruct(value, subpattern2, bound, asserts) return quote $body1 && $body2 @@ -161,7 +160,7 @@ function handle_destruct(__module__, value::Symbol, pattern, bound::Set{Symbol}, guard = pattern.args[2] return quote - $(handle_destruct(__module__, value, subpattern, bound, asserts)) && + $(handle_destruct(value, subpattern, bound, asserts)) && let $(bound...) # bind variables locally so they can be used in the guard $(@splice variable in bound quote @@ -215,7 +214,6 @@ function handle_destruct(__module__, value::Symbol, pattern, bound::Set{Symbol}, $result !== nothing && ($result isa Tuple) && $(handle_destruct_fields( - __module__, result, pattern, subpatterns, @@ -284,7 +282,6 @@ function handle_destruct(__module__, value::Symbol, pattern, bound::Set{Symbol}, # information in Julia 0.6 $value isa $(esc(T)) && $(handle_destruct_fields( - __module__, value, pattern, subpatterns, @@ -300,7 +297,6 @@ function handle_destruct(__module__, value::Symbol, pattern, bound::Set{Symbol}, return quote ($value isa Tuple) && $(handle_destruct_fields( - __module__, value, pattern, subpatterns, @@ -316,7 +312,6 @@ function handle_destruct(__module__, value::Symbol, pattern, bound::Set{Symbol}, return quote ($value isa AbstractArray) && $(handle_destruct_fields( - __module__, value, pattern, subpatterns, @@ -331,18 +326,18 @@ function handle_destruct(__module__, value::Symbol, pattern, bound::Set{Symbol}, # typeassert return quote ($value isa $(esc(T))) && - $(handle_destruct(__module__, value, subpattern, bound, asserts)) + $(handle_destruct(value, subpattern, bound, asserts)) end else error("Unrecognized pattern syntax: $pattern") end end -function handle_match_eq(__module__, expr) +function handle_match_eq(expr) if @capture(expr, pattern_ = value_) asserts = Expr[] bound = Set{Symbol}() - body = handle_destruct(__module__, :value, pattern, bound, asserts) + body = handle_destruct(:value, pattern, bound, asserts) return quote $(asserts...) value = $(esc(value)) @@ -357,10 +352,10 @@ function handle_match_eq(__module__, expr) end end -function handle_match_case(__module__, value, case, tail, asserts) +function handle_match_case(value, case, tail, asserts) if @capture(case, pattern_ => result_) bound = Set{Symbol}() - body = handle_destruct(__module__, :value, pattern, bound, asserts) + body = handle_destruct(:value, pattern, bound, asserts) return quote if $body @@ -380,13 +375,13 @@ function handle_match_case(__module__, value, case, tail, asserts) end end -function handle_match_cases(__module__, value, match) +function handle_match_cases(value, match) if @capture(match, begin cases__ end) tail = :(throw(MatchFailure(value))) asserts = Expr[] for case in reverse(cases) - tail = handle_match_case(__module__, :value, case, tail, asserts) + tail = handle_match_case(:value, case, tail, asserts) end return quote @@ -406,7 +401,7 @@ If `value` matches `pattern`, bind variables and return `value`. Otherwise, throw `MatchFailure`. """ macro match(expr) - handle_match_eq(__module__, expr) + handle_match_eq(expr) end """ @@ -420,7 +415,7 @@ Return `result` for the first matching `pattern`. If there are no matches, throw `MatchFailure`. """ macro match(value, cases) - handle_match_cases(__module__, value, cases) + handle_match_cases(value, cases) end """ From e1e5256cfe5653d9335476b2396ae27b27a02b1d Mon Sep 17 00:00:00 2001 From: Nate Nystrom Date: Thu, 27 Aug 2020 18:11:33 +0200 Subject: [PATCH 19/20] documentation cleanup --- src/Rematch.jl | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/Rematch.jl b/src/Rematch.jl index 7ce7ca8..a5ec8f2 100644 --- a/src/Rematch.jl +++ b/src/Rematch.jl @@ -250,7 +250,7 @@ function handle_destruct(value::Symbol, pattern, bound::Set{Symbol}, asserts::Ve expected_fieldcount = gensym("$(T)_expected_fieldcount") actual_fieldcount = gensym("$(T)_actual_fieldcount") push!(asserts, quote - t = $(esc(T)) + t = $(esc(T)) if typeof(t) <: Function throw(LoadError("Attempted to match on a function", @__LINE__, AssertionError("Incorrect match usage"))) @@ -423,7 +423,7 @@ Patterns: * `_` matches anything * `foo` matches anything, binds value to `foo` - * `~Foo(x,y,z)` calls the extractor function `Foo(value)` which returns a tuple + * `~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` @@ -451,11 +451,10 @@ 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 one value (for nullary and unary patterns), or a tuple of values (for 2+-ary -patterns). Returning `nothing` indicates the extractor does not match. -For example to destruct an array into its head and tail: +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 Cons(xs) + function unapply_Cons(xs) if isempty(xs) return nothing else @@ -469,7 +468,7 @@ For example to destruct an array into its head and tail: Or here's one to extract the polar coordinates of a point: - function Polar(p) + function unapply_Polar(p) @match p begin (x, y) => begin From e6d52bb6d1909bcd10a18a367d2ce948631cabef Mon Sep 17 00:00:00 2001 From: Nate Nystrom Date: Fri, 3 Sep 2021 12:33:27 +0200 Subject: [PATCH 20/20] Fix missing close quote --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6c29bbc..47b5d89 100644 --- a/README.md +++ b/README.md @@ -186,7 +186,7 @@ end end _ => @assert false end - +``` ## Differences from [Match.jl](https://github.com/kmsquire/Match.jl)