Skip to content

Commit

Permalink
allow _ as the fallback 'wildcard' case (#56)
Browse files Browse the repository at this point in the history
* allow `_` as the fallback 'wildcard' case

* bump version

* make coverage happy

* test on 1.10.0-rc1
  • Loading branch information
MasonProtter authored Nov 20, 2023
1 parent 003c9d4 commit 8cf1b49
Show file tree
Hide file tree
Showing 5 changed files with 73 additions and 14 deletions.
1 change: 1 addition & 0 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ jobs:
- '1.6'
- '1.8'
- '1.9'
- '1.10.0-rc1'
- 'nightly'
os:
- ubuntu-latest
Expand Down
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "SumTypes"
uuid = "8e1ec7a9-0e02-4297-b0fe-6433085c89f2"
authors = ["MasonProtter <mason.protter@icloud.com>"]
version = "0.5.0"
version = "0.5.1"

[deps]
MacroTools = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09"
Expand Down
14 changes: 12 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,8 +165,8 @@ The `@cases` macro still falls far short of a full on pattern matching system, l

### Defining many repetitive cases simultaneously

`@cases` does not allow for fallback branches, and it also does not allow one to write inexhaustive cases. To avoid making some code overly verbose and repetitive, we instead provide syntax for defining many cases in one line:

Generally, it's good to explicitly handle all cases of a sum type, but sometimes you just want one set of behaviour for
a large set of cases. One option, is 'collections' of cases like so:
``` julia
@sum_type Re begin
Empty
Expand Down Expand Up @@ -209,6 +209,16 @@ count_classes(r::Re, c=0) = @cases r begin
end;
```

SumTypes also lets you use `_` as a case predicate that accepts anything, but this only works in the final position, and
does not allow destructuring:

``` julia
isEmpty(x::Re) = @cases x begin
Empty => true
_ => false
end
```

<!-- </details> -->


Expand Down
33 changes: 27 additions & 6 deletions src/cases.jl
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,24 @@
end

macro cases(to_match, block)
@assert block.head == :block
esc(_cases(to_match, block))
end

function _cases(to_match, block)
block.head == :block || error("The second argument to @cases must be a code block")
lnns = filter(block.args) do arg
arg isa LineNumberNode
end
# Base.remove_linenums!(block)

stmts = []
foreach(block.args) do arg
wildcard_stmt = Ref{Any}()

foreach(enumerate(block.args)) do (i, arg)
if arg isa LineNumberNode
return nothing
end
arg.head == :call && arg.args[1] == :(=>) || throw(error("Malformed case $arg"))
arg.head == :call && arg.args[1] == :(=>) || error("Malformed case $arg")
lhs = arg.args[2]
rhs = arg.args[3]
if isexpr(lhs, :call) # arg.args[2] isa Expr && arg.args[2].head == :call
Expand All @@ -43,7 +49,15 @@ macro cases(to_match, block)
iscall = false
end
if variant isa Symbol
push!(stmts, (;variant=variant, rhs=rhs, fieldnames=fieldnames, iscall=iscall))
if variant === :_
if i == length(block.args)
wildcard_stmt[] = rhs
else
error("The wildcard variant _ can only be used as the last option to @cases")
end
else
push!(stmts, (;variant=variant, rhs=rhs, fieldnames=fieldnames, iscall=iscall))
end
elseif isexpr(variant, :vect)
for subvariant variant.args
if !(subvariant isa Symbol)
Expand Down Expand Up @@ -90,13 +104,20 @@ macro cases(to_match, block)
end
# push!(to_push, :($matching_error()))
deparameterize(x) = x isa Symbol ? x : x isa Expr && x.head == :curly ? x.args[1] : throw("Invalid variant name $x")
if isdefined(wildcard_stmt, :x)
push!(to_push, wildcard_stmt[])
exhaustive_stmt = nothing
else
exhaustive_stmt = :($assert_exhaustive(Val{$tags($Typ)},
Val{$(Expr(:tuple, QuoteNode.(deparameterize.(variants))...))}))
end
quote
let $data = $to_match
$Typ = $typeof($data)
$check_sum_type($Typ)
$assert_exhaustive(Val{$tags($Typ)}, Val{$(Expr(:tuple, QuoteNode.(deparameterize.(variants))...))})
$exhaustive_stmt
$unwrapped = $unwrap($data)
$ex
end
end |> esc
end
end
37 changes: 32 additions & 5 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,24 @@ Base.getproperty(f::Result, s::Symbol) = error("Don't do that!")
@test_throws Exception SumTypes._sum_type(
:(Blah{T}), :(begin
foo{U}(::U)
end ))
end ))
@test_throws Exception SumTypes._cases(:(Fruit), :(1))
@test_throws Exception SumTypes._cases(:(Fruit), :([1, 2, 3]))
@test_throws Exception SumTypes._cases(:(Fruit), :(begin
true
_ => false
banana => false
end))
@test_throws Exception SumTypes._cases(:(Fruit), :(begin
apple => true
_ => false
banana => false
end))
@test_throws Exception SumTypes._cases(:(Fruit), :(begin
apple => true
[banana, orange()] => false
_ => false
end))

let x = Left([1]), y = Left([1.0]), z = Right([1])
@test x == y
Expand Down Expand Up @@ -320,10 +337,10 @@ end
Empty
Class(::UInt8)
Rep(::Re)
Alt(::Re, ::Re)
Cat(::Re, ::Re)
Alt(::Re, ::Re)
Cat(::Re, ::Re)
Diff(::Re, ::Re)
And(::Re, ::Re)
And(::Re, ::Re)
end;

count_classes(r::Re, c=0) = @cases r begin
Expand All @@ -332,14 +349,24 @@ count_classes(r::Re, c=0) = @cases r begin
Rep(x) => c + count_classes(x)
[Alt, Cat, Diff, And](x, y) => c + count_classes(x) + count_classes(y)
end;


is_empty(r::Re) = @cases r begin
Empty => true
_ => false
end

@testset "Collection of variants" begin
@test foo(A(1, 1)) == 2
@test foo(B(1, 1.5)) == 2.5
@test foo(C("3")) == 3
@test foo(D(:a => 4)) == 4

@test count_classes(And(Alt(Rep(Class(0x1)), And(Class(0x1), Empty)), Class(0x0))) == 3

@test is_empty(Empty)
for r (Class(1), Rep(Class(1)), Alt(Empty, Empty), Cat(Empty, Empty), Diff(Empty, Empty), And(Empty, Empty))
@test !is_empty(r)
end
end

end

4 comments on commit 8cf1b49

@MasonProtter
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JuliaRegistrator
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Registration pull request created: JuliaRegistries/General/95659

Tip: Release Notes

Did you know you can add release notes too? Just add markdown formatted text underneath the comment after the text
"Release notes:" and it will be added to the registry PR, and if TagBot is installed it will also be added to the
release that TagBot creates. i.e.

@JuliaRegistrator register

Release notes:

## Breaking changes

- blah

To add them here just re-invoke and the PR will be updated.

Tagging

After the above pull request is merged, it is recommended that a tag is created on this repository for the registered package version.

This will be done automatically if the Julia TagBot GitHub Action is installed, or can be done manually through the github interface, or via:

git tag -a v0.5.1 -m "<description of version>" 8cf1b494d60dfe355bbd490529dc26482ea906d3
git push origin v0.5.1

@MasonProtter
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JuliaRegistrator register

Release notes:

  • Added _ as an option @cases to catch all variants.

@JuliaRegistrator
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Registration pull request updated: JuliaRegistries/General/95659

Tagging

After the above pull request is merged, it is recommended that a tag is created on this repository for the registered package version.

This will be done automatically if the Julia TagBot GitHub Action is installed, or can be done manually through the github interface, or via:

git tag -a v0.5.1 -m "<description of version>" 8cf1b494d60dfe355bbd490529dc26482ea906d3
git push origin v0.5.1

Please sign in to comment.