MasonProtter / PatternDispatch.jl

Extensible multiple-dispatch style pattern matching in julia

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Build Status

PatternDispatch.jl

To install, simply type

] add https://github.com/MasonProtter/PatternDispatch.jl.git

at the julia REPL.


PatternDispatch.jl offers pattern matching through Rematch.jl but with extensible, multiple-dispatch like semantics.

using PatternDispatch
@pattern fib(x) = fib(x-1) + fib(x-2)
@pattern fib(1) = 1
@pattern fib(0) = 0
julia> fib(10)
55

Now suppose I later decide I don't want a stack overflow every time I accidentally call fib(-1), then I can just define

@pattern fib(x where x < 0) = error("Fib only takes positive inputs.")
julia> fib(-1)
ERROR: Fib only takes positive inputs.

Any valid Rematch.jl pattern can be used in a @pattern function signature, so you can write powerful destructuring code like

@pattern foo(x) = x
@pattern foo([x]) = x
@pattern foo((x,) where x < 1) = 1
@pattern foo((x,) where (x isa String || x > 1)) = x*x
@pattern foo(Expr(:call, [:+, a, b])) = a * b
julia> foo(1)
1

julia> foo([2])
2

julia> foo((0.5,))
1

julia> foo((1.5,))
2.25

julia> foo(("hi",))
"hihi"

julia> foo(:(3 + 2))
6

Pattern 'methods' are dispatched on in order of their specificity, so completely unconstrained patterns like @pattern f(x) have the lowest precedence whereas exact value patterns like @pattern f(1) have highest precedence. Constrained patterns like @pattern f(x, y where y > x) have intermediate precedence. For instance, the above function foo has a function body like

foo(args...) = foo(Pattern, args...)

foo(::Type{Pattern}, args...) = @match args begin
    (Expr(:call, [:+, a, b]),)          => a * b
    ((x,) where x isa String || x > 1,) => x * x
    ((x,) where x isa String || x > 1,) => -1
    ((x,) where x < 1,)                 => 1
    ([x],)                              => x
    (x,)                                => x
end

where @match is from the Rematch.jl package.

Adding pattern methods to functions you don't own

Suppose we want to add pattern matching methods to functions from other modules, e.g. we are annoyed that sin(2π) != 0.0 and want to change it. If we naively write

julia> @pattern Base.sin(x where x == 2π) = 0.0
sin (generic function with 13 methods)

julia> sin(2π)
-2.4492935982947064e-16

we don't get the result we might have expected. This is because overwriting the regular sin(x) method would be type piracy. To avoid type piracy, PatternDispatch.jl created a method sin(::Type{Pattern}, args...) = @match ... but not an accompanying method sin(args...) = sin(Pattern, args...), so to access our new method we need to do

julia> sin(Pattern, 2π)
0.0

directly.

Known Gotcha's

  • Due to an unfortunate implementation detail, pattern functions are all @generated functions, meaning that they cannot return closures (there are workarounds to this, see the 'closures' testset in tests/runteses.jl).
  • If you define a @pattern function in a local scope, you may get errors if you reference variables defined in that scope, even the pattern function itself ref.

About

Extensible multiple-dispatch style pattern matching in julia


Languages

Language:Julia 100.0%