Giter VIP home page Giter VIP logo

sumtypes.jl's Introduction

SumTypes.jl

Basics

Sum types, sometimes called 'tagged unions' are the type system equivalent of the disjoint union operation (which is not a union in the traditional sense). In the Rust programming language, these are called "Enums", and they're more general than what Julia calls an enum.

At the end of the day, a sum type is really just a fancy word for a container that can store data of a few different, pre-declared types and is labelled by how it was instantiated.

Users of statically typed programming languages often prefer Sum types to unions because it makes type checking easier. In a dynamic language like julia, the benefit of these objects is less obvious, but there are cases where they're helpful, like performance sensitive branching on heterogeneous types, and enforcing the handling of cases.

The simplest version of a sum type is just a list of constant variants (i.e. basically a julia enum):

using SumTypes

@sum_type Fruit begin
    apple
    banana
    orange
end
julia> apple
apple::Fruit

julia> banana
banana::Fruit

julia> orange
brange::Fruit

julia> typeof(apple) == typeof(banana) == typeof(orange) == Fruit
true

But this isn't particularly interesting. More interesting are sum types which can enclose data. Let's explore a very fundamental sum type (fundamental in the sense that all other sum types may be derived from it):

@sum_type Either{A, B} begin
    Left{A}(::A)
    Right{B}(::B)
end

This says that we have a sum type Either{A, B}, and it can hold a value that is either of type A or of type B. Either has two 'constructors' which we have called Left{A} and Right{B}. These exist essentially as a way to have instances of Either carry a record of how they were constructed by being wrapped in dummy structs named Left or Right.

Here is how these constructors behave:

julia> Left(1)
Left(1)::Either{Int64, Uninit}

julia> Right(1.0)
Right(1.0)::Either{Uninit, Float64}

Notice that because both Left{A} and Right{B} each carry one fewer type parameter than Either{A,B}, then simply writing Left(1) is not enough to fully specify the type of the full Either, so the unspecified field is SumTypes.Uninit by default.

In cases like this, you can rely on implicit conversion to get the fully initialized type. E.g.

julia> let x::Either{Int, Float64} = Left(1)
           x
       end
Left(1)::Either{Int64, Float64}

Typically, you'll do this by enforcing a return type on a function:

function foo() :: Either{Int, Float64}
    # Randomly return either a Left(1) or a Right(2.0)
    rand(Bool) ? Left(1) : Right(2.0)
end
julia> foo()
Left(1)::Either{Int64, Float64}

julia> foo()
Right(2.0)::Either{Int64, Float64}

This is particularly useful because in this case foo is type stable!

julia> Core.Compiler.return_type(foo, Tuple{})
Either{Int64, Float64}

julia> isconcretetype(ans)
true

Note that unlike Union{A, B}, A <: Either{A,B} is false, and Either{A, A} is distinct from A.

Destructuring Sum types

Okay, but how do I actually access the data enclosed in a Fruit or an Either? The answer is destructuring. SumTypes.jl exposes a @cases macro for efficiently unwrapping and branching on the contents of a sum type:

julia> myfruit = orange
orange::Fruit

julia> @cases myfruit begin
           apple => "Got an apple!"
           orange => "Got an orange!"
           banana => error("I'm allergic to bananas!")
       end
"Got an orange!"

julia> @cases banana begin
           apple => "Got an apple!"
           orange => "Got an orange!"
           banana => error("I'm allergic to bananas!")
       end
ERROR: I'm allergic to bananas!
[...]

@cases can automatically detect if you didn't give an exhaustive set of cases (with no runtime penalty) and throw an error.

julia> @cases myfruit begin
           apple => "Got an apple!"
           orange => "Got an orange!"
       end
ERROR: Inexhaustive @cases specification. Got cases (:apple, :orange), expected (:apple, :banana, :orange)
[...]

Furthermore, @cases can destructure sum types which hold data:

julia> let x::Either{Int, Float64} = rand(Bool) ? Left(1) : Right(2.0)
           @cases x begin
               Left(l) => l + 1.0
               Right(r) => r - 1
           end
       end
2.0

i.e. in this example, @cases took in an Either{Int,Float64} and if it contained a Left, it took the wrapped data (an Int) bound it do the variable l and added 1.0 to l, whereas if it was a Right, it took the Float64 and bound it to a variable r and subtracted 1 from r.

The @cases macro still falls far short of a full on pattern matching system, lacking many features. For anything advanced, I'd recommend using @match from MLStyle.jl.

Defining many repetitive cases simultaneously

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:

@sum_type Re begin
    Empty
    Class(::UInt8)
    Rep(::Re)
    Alt(::Re, ::Re)
    Cat(::Re, ::Re)
    Diff(::Re, ::Re)
    And(::Re, ::Re)
end;

isEmpty(x::Re) = @cases x begin
    Empty => true
    [Class, Rep, Alt, Cat, Diff, And] => false
end

This is the same as if one had manually written out

isEmpty(r::Re) = @cases r begin
    Empty => true
    Class => false
    Rep => false
    Alt => false
    Cat => false
    Diff => false
    And => false
end

You can also destructure repeated cases with the [] syntax:

count_classes(r::Re, c=0) = @cases r begin
    Empty => c
    Class => c + 1
    Rep(x) => c + count_classes(x)
   [Alt, Cat, Diff, And](x, y)  => c + count_classes(x) + count_classes(y)
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:

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

Avoiding namespace clutter

Click to expand

A common complaint about Enums and Sum Types is that sometimes they can contribute to clutter in the namespace. If you want to avoid having all the variants being available as top-level constant variables, then you can use the :hidden option:

julia> @sum_type Foo{T} :hidden begin
           A
           B{T}(::T)
       end

julia> A
ERROR: UndefVarError: A not defined

julia> B
ERROR: UndefVarError: B not defined

These 'hidden' variants can be accessed by applying the ' operator to the type Foo, which returns a named tuple of the variants:

julia> Foo'
(A = A::Foo{Uninit}, B = var"#Foo#B")

And then you can access this named tuple as normal:

julia> Foo'.A
A::Foo{Uninit}

julia> Foo'.B(1)
B(1)::Foo{Int64}

You can even do fancy things like

julia> let (; B) = Foo'
           B(1)
       end
B(1)::Foo{Int64}

Note that property-destructuring syntax is only available on julia version 1.7 and higher JuliaLang/julia#39285

Custom printing

Click to expand

SumTypes.jl automatically overloads Base.show(::IO, ::YourType) and Base.show(::IO, ::MIME"text/plain", ::YourType) for your type when you create a sum type, but it forwards that call to an internal function SumTypes.show_sumtype. If you wish to customize the printing of a sum type, then you should overload SumTypes.show_sumtype:

julia> @sum_type Fruit2 begin
           apple
           orange
           banana
       end;

julia> apple
apple::Fruit2

julia> SumTypes.show_sumtype(io::IO, x::Fruit2) = @cases x begin
           apple => print(io, "apple")
           orange => print(io, "orange")
           banana => print(io, "banana")
       end

julia> apple
apple

julia> SumTypes.show_sumtype(io::IO, ::MIME"text/plain", x::Fruit2) = @cases x begin
           apple => print(io, "apple!")
           orange => print(io, "orange!")
           banana => print(io, "banana!")
       end

julia> apple
apple!

If you overload Base.show directly inside a package, you might get annoying method deletion warnings during pre-compilation.

Alternative syntax

See also DynamicSumTypes.jl for an alternative syntax for defining sum types, and some other niceties.

Performance

SumTypes.jl can provide some speedups compared to union-splitting when destructuring and branching on abstractly typed data.

SumTypes.jl

Benchmark code
module SumTypeTest

using SumTypes,  BenchmarkTools
@sum_type AT begin
    A(common_field::Int, a::Bool, b::Int)
    B(common_field::Int, a::Int, b::Float64, d::Complex)
    C(common_field::Int, b::Float64, d::Bool, e::Float64, k::Complex{Real})
    D(common_field::Int, b::Any)
end

foo!(xs) = for i in eachindex(xs)
    xs[i] = @cases xs[i] begin
        A(cf, a, b)       => B(cf+1, a, b, b)
        B(cf, a, b, d)    => C(cf-1, b, isodd(a), b, d)
        C(cf, b, d, e, k) => D(cf+1, isodd(cf) ? "hi" : "bye")
        D(cf, b)          => A(cf-1, b=="hi", cf)
    end
end

xs = rand((A(1, true, 10), 
           B(1, 1, 1.0, 1+1im), 
           C(1, 2.0, false, 3.0, Complex{Real}(1 + 2im)), 
           D(1, "hi")), 
	      10000);

display(@benchmark foo!($xs);)

end
BenchmarkTools.Trial: 10000 samples with 1 evaluation.
 Range (min … max):  300.541 μs …   2.585 ms  ┊ GC (min … max): 0.00% … 86.91%
 Time  (median):     313.611 μs               ┊ GC (median):    0.00%
 Time  (mean ± σ):   342.285 μs ± 242.158 μs  ┊ GC (mean ± σ):  8.29% ± 10.04%

  █                                                             ▁
  █▇▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█ █
  301 μs        Histogram: log(frequency) by time       2.37 ms <

 Memory estimate: 620.88 KiB, allocs estimate: 19900.

Branching on abstractly typed data:

Benchmark code
module AbstractTypeTest

using BenchmarkTools

abstract type AT end
Base.@kwdef struct A <: AT
    common_field::Int
    a::Bool 
    b::Int
end
Base.@kwdef struct B <: AT
    common_field::Int
    a::Int
    b::Float64 
    d::Complex  # not isbits
end
Base.@kwdef struct C <: AT
    common_field::Int
    b::Float64 
    d::Bool 
    e::Float64
    k::Complex{Real}  # not isbits
end
Base.@kwdef struct D <: AT
    common_field::Int
    b::Any  # not isbits
end

foo!(xs) = for i in eachindex(xs)
    @inbounds x = xs[i]
    @inbounds xs[i] = x isa A ? B(x.common_field+1, x.a, x.b, x.b) :
        x isa B ? C(x.common_field-1, x.b, isodd(x.a), x.b, x.d) :
        x isa C ? D(x.common_field+1, isodd(x.common_field) ? "hi" : "bye") :
        x isa D ? A(x.common_field-1, x.b=="hi", x.common_field) : error()
end


xs = rand((A(1, true, 10), 
           B(1, 1, 1.0, 1+1im), 
           C(1, 2.0, false, 3.0, Complex{Real}(1 + 2im)), 
           D(1, "hi")), 
	      10000);
display(@benchmark foo!($xs);)

end
BenchmarkTools.Trial: 10000 samples with 1 evaluation.
 Range (min … max):  366.510 μs …   4.504 ms  ┊ GC (min … max):  0.00% … 90.65%
 Time  (median):     386.470 μs               ┊ GC (median):     0.00%
 Time  (mean ± σ):   478.369 μs ± 571.525 μs  ┊ GC (mean ± σ):  18.62% ± 13.77%

  █                                                           ▂ ▁
  █▇▄▅▅▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█ █
  367 μs        Histogram: log(frequency) by time        4.1 ms <

 Memory estimate: 1.06 MiB, allocs estimate: 31958.

Unityper.jl

Unityper.jl is a somewhat similar package, with some overlapping goals to SumTypes.jl. However, In this test, Unityper.jl ends up doing much worse than abstract containers or SumTypes.jl:

Benchmark code
module UnityperTest

using Unityper, BenchmarkTools

@compactify begin
    @abstract struct AT
        common_field::Int = 0
    end
    struct A <: AT
        a::Bool = true
        b::Int = 10
    end
    struct B <: AT
        a::Int = 1
        b::Float64 = 1.0
        d::Complex = 1 + 1.0im # not isbits
    end
    struct C <: AT
        b::Float64 = 2.0
        d::Bool = false
        e::Float64 = 3.0
        k::Complex{Real} = 1 + 2im # not isbits
    end
    struct D <: AT
        b::Any = "hi" # not isbits
    end
end

foo!(xs) = for i in eachindex(xs)
    @inbounds x = xs[i]
    @inbounds xs[i] = @compactified x::AT begin
        A => B(;common_field=x.common_field+1, a=x.a, b=x.b, d=x.b)
        B => C(;common_field=x.common_field-1, b=x.b, d=isodd(x.a), e=x.b, k=x.d)
        C => D(;common_field=x.common_field+1, b=isodd(x.common_field) ? "hi" : "bye")
        D => A(;common_field=x.common_field-1, a=x.b=="hi", b=x.common_field)
    end
end

xs = rand((A(), B(), C(), D()), 10000);
display(@benchmark foo!($xs);)

end
BenchmarkTools.Trial: 2539 samples with 1 evaluation.
 Range (min … max):  1.847 ms …   5.341 ms  ┊ GC (min … max): 0.00% … 64.05%
 Time  (median):     1.890 ms               ┊ GC (median):    0.00%
 Time  (mean ± σ):   1.968 ms ± 478.604 μs  ┊ GC (mean ± σ):  3.93% ±  9.68%

  █▆
  ██▇▆▁▃▁▁▃▁▃▁▁▁▁▁▃▁▁▃▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▅▆▆▆▇ █
  1.85 ms      Histogram: log(frequency) by time       4.9 ms <

 Memory estimate: 1.14 MiB, allocs estimate: 27272.

SumTypes.jl has some other advantages relative to Unityper.jl such as:

  • SumTypes.jl allows parametric types for much greater container flexibility.
  • SumTypes.jl does not require default values for every field of the struct.
  • SumTypes.jl's @cases macro is more powerful and flexible than Unityper's @compactified.
  • SumTypes.jl allows you to hide its variants from the namespace (opt in).

One advantage of Unityper.jl is:

  • If you're not modifying the data and just re-using old heap allocated data, there are cases where Unityper.jl can avoid an allocation that SumTypes.jl would have incurred.

sumtypes.jl's People

Contributors

danielvandh avatar jakobnissen avatar jonathanbieler avatar lmiq avatar masonprotter avatar shalokshalom avatar tlienart avatar tortar avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

sumtypes.jl's Issues

Error when creating SumType in an untyped vector

Let's say you have

using SumTypes

@sum_type A{X,Y} begin
    B{X}(x::X)
    C{Y}(y::Y)
end

it seems to work fine

julia> B(1)
B(1)::A{Int64, Uninit}

julia> [B(i) for i in 1:2]
2-element Vector{A{Int64, Uninit}}:
 B(1)::A{Int64, Uninit}
 B(2)::A{Int64, Uninit}

but instead this errors

julia> [B(1)]
ERROR: MethodError: convert(::Type{A{Int64, Uninit}}, ::A{Int64, Uninit}) is ambiguous.

Candidates:
  convert(::Type{var"##_T#226"}, x::var"##_T#226") where {X, Y, var"##_T#226"<:A{X, Y}}
    @ Main ~/.julia/dev/SumTypes/src/sum_type.jl:212
  convert(::Type{<:A{X, Y}}, x::A{X, Uninit}) where {X, Y}
    @ Main ~/.julia/dev/SumTypes/src/sum_type.jl:203
  convert(::Type{A{X, Y}}, x::A{X, Uninit}) where {X, Y}
    @ Main ~/.julia/dev/SumTypes/src/sum_type.jl:202

Possible fix, define
  convert(::Type{A{X, Uninit}}, ::A{X, Uninit}) where X

Stacktrace:
 [1] __inbounds_setindex!(A::Vector{A{Int64, Uninit}}, x::A{Int64, Uninit}, i1::Int64)
   @ Base ./array.jl:1026
 [2] vect(X::A{Int64, Uninit})
   @ Base ./array.jl:165
 [3] top-level scope
   @ REPL[17]:1

Instead if you type the vector it works:

julia> A[B(1)]
1-element Vector{A}:
 B(1)::A{Int64, Uninit}

julia> A{Int}[B(1)]
1-element Vector{A{Int64}}:
 B(1)::A{Int64, Uninit}

Dealing with extra parameters in recursive sumtypes

Hey @MasonProtter, really nice package.

Is there a way I can make a recursive SumType which uses the correct parametrized field type?

For example, say I want to make a linked list, with a separate type for the end of the list:

@sum_type LinkedList begin
    BranchNode(val::Float64, child::LinkedList)
    EndNode(val::Float64)
end

However, this LinkedList type referenced in BranchNode is actually generic and will incur type inference issues, as the sum type is parametrized:

julia> typeof(BranchNode(0.1, EndNode(0.1)))
LinkedList{8, 1, UInt8}

Is there a way I can get this same parametrization within the @sum_type macro, so that I may use it to build recursive SumTypes?

Cheers,
Miles

Too verbose display of SumTypes

Okay so that was my PR that messed it up, but try doing Left{Vector{Float64}, Nothing}(rand(1000))... the issue is that show is unbounded (i.e. no ellipsis). And I can't figure out how to make it display as if it's inside a struct.

Can constructors of a sum type be alias of variants?

e.g. consider the sum type in the readme:

@sum_type AT begin
    A(common_field::Int, a::Bool, b::Int)
    B(common_field::Int, a::Int, b::Float64, d::Complex)
    C(common_field::Int, b::Float64, d::Bool, e::Float64, k::Complex{Real})
    D(common_field::Int, b::Any)
end

here A, B, C and D are just "fake types" used for making constructors for sum types. But I wonder why couldn't they be alias of the variants?

const A = SumTypes.Variant{:A, (:common_field, :a, :b), Tuple{Int64, Bool, Int64}}

this way the dispatch system will become available even for sum types, which is something it would be useful for things I plan to do in MixedStructTypes, but I think it would be good in general...this shouldn't be breaking right?

Constructor overloads

Let's say I have

@sum_type Either begin
    A
    B(x::Int, y::Int)
end

And normally I would have had

struct B
    x::Int
    y::Int
end
B() = B(0, 0)
B(x) = B(x, x)

I cannot add such constructors to the sum type B currently, can I?

Allow recursive definitions?

e.g. @jakobnissen was discussing on #helpdesk how to implement types for regex, and wrote the Rust version would look like

enum Re{
    Empty,
    Class(u8),
    Rep(Box<Re>),
    Alt(Box<Re>, Box<Re>),
    Cat(Box<Re>, Box<Re>),
    Diff(Box<Re>, Box<Re>),
    And(Box<Re>, Box<Re>),
}

I wanted to try with SumTypes, but ran into the fact that you can't have recursive definitions:

julia> using SumTypes

julia> @sum_type Re begin
           Empty()
           Class(::UInt8)
           Rep(::Re)
           Alt(::Re, ::Re)
           Cat(::Re, ::Re)
           Diff(::Re, ::Re)
           And(::Re, ::Re)
       end
ERROR: UndefVarError: Re not defined
Stacktrace:
 [1] top-level scope
   @ ~/.julia/packages/SumTypes/OTmhF/src/SumTypes.jl:92
 [2] top-level scope
   @ REPL[3]:1

StackOverflow in show_sumtype usage

julia> module Ex

       using SumTypes

       struct Foo end

       @sum_type FooWrapper{T} begin
           FooWrapper1{T}(::T)
           FooWrapper2{T}(::T)
           FooWrapper3{T}(::T)
       end

       function SumTypes.show_sumtype(io::IO, x::FooWrapper{Foo})
           foo = SumTypes.unwrap(x).data[1]
           print(io, x)
       end

       end
Main.Ex

julia> Ex.FooWrapper2(Ex.Foo())
Error showing value of type Main.Ex.FooWrapper{Main.Ex.Foo, 0, 0, UInt8}:
ERROR: StackOverflowError:

caused by: StackOverflowError:
Stacktrace:
     [1] print(io::IOContext{Base.TTY}, x::Main.Ex.FooWrapper{Main.Ex.Foo, 0, 0, UInt8})
       @ Base ./strings/io.jl:37
     [2] show_sumtype(io::IOContext{Base.TTY}, x::Main.Ex.FooWrapper{Main.Ex.Foo, 0, 0, UInt8})
       @ Main.Ex ./REPL[4]:15
     [3] show(io::IOContext{Base.TTY}, x::Main.Ex.FooWrapper{Main.Ex.Foo, 0, 0, UInt8})
       @ Main.Ex ~/.data/julia/packages/SumTypes/Hn80O/src/sum_type.jl:293
     [4] print(io::IOContext{Base.TTY}, x::Main.Ex.FooWrapper{Main.Ex.Foo, 0, 0, UInt8})
       @ Base ./strings/io.jl:35
--- the last 3 lines are repeated 9277 more times ---
 [27836] show_sumtype(io::IOContext{Base.TTY}, x::Main.Ex.FooWrapper{Main.Ex.Foo, 0, 0, UInt8})
       @ Main.Ex ./REPL[4]:15
 [27837] show_sumtype(io::IOContext{Base.TTY}, m::MIME{Symbol("text/plain")}, x::Main.Ex.FooWrapper{Main.Ex.Foo, 0, 0, UInt8})
       @ SumTypes ~/.data/julia/packages/SumTypes/Hn80O/src/SumTypes.jl:61
 [27838] show(io::IOContext{Base.TTY}, m::MIME{Symbol("text/plain")}, x::Main.Ex.FooWrapper{Main.Ex.Foo, 0, 0, UInt8})
       @ Main.Ex ~/.data/julia/packages/SumTypes/Hn80O/src/sum_type.jl:294
 [27839] display(d::REPL.REPLDisplay, x::Any)
       @ REPL /usr/share/julia/stdlib/v1.9/REPL/src/REPL.jl:281

caused by: StackOverflowError:
Stacktrace:
     [1] print(io::IOContext{Base.TTY}, x::Main.Ex.FooWrapper{Main.Ex.Foo, 0, 0, UInt8})
       @ Base ./strings/io.jl:37
     [2] show_sumtype(io::IOContext{Base.TTY}, x::Main.Ex.FooWrapper{Main.Ex.Foo, 0, 0, UInt8})
       @ Main.Ex ./REPL[4]:15
     [3] show(io::IOContext{Base.TTY}, x::Main.Ex.FooWrapper{Main.Ex.Foo, 0, 0, UInt8})
       @ Main.Ex ~/.data/julia/packages/SumTypes/Hn80O/src/sum_type.jl:293
     [4] print(io::IOContext{Base.TTY}, x::Main.Ex.FooWrapper{Main.Ex.Foo, 0, 0, UInt8})
       @ Base ./strings/io.jl:35
--- the last 3 lines are repeated 9278 more times ---
 [27839] show_sumtype(io::IOContext{Base.TTY}, x::Main.Ex.FooWrapper{Main.Ex.Foo, 0, 0, UInt8})
       @ Main.Ex ./REPL[4]:15
 [27840] show_sumtype(io::IOContext{Base.TTY}, m::MIME{Symbol("text/plain")}, x::Main.Ex.FooWrapper{Main.Ex.Foo, 0, 0, UInt8})
       @ SumTypes ~/.data/julia/packages/SumTypes/Hn80O/src/SumTypes.jl:61
 [27841] show(io::IOContext{Base.TTY}, m::MIME{Symbol("text/plain")}, x::Main.Ex.FooWrapper{Main.Ex.Foo, 0, 0, UInt8})
       @ Main.Ex ~/.data/julia/packages/SumTypes/Hn80O/src/sum_type.jl:294
 [27842] display(d::REPL.REPLDisplay, x::Any)
       @ REPL /usr/share/julia/stdlib/v1.9/REPL/src/REPL.jl:281

caused by: StackOverflowError:
Stacktrace:
     [1] print(io::IOContext{Base.TTY}, x::Main.Ex.FooWrapper{Main.Ex.Foo, 0, 0, UInt8})
       @ Base ./strings/io.jl:32
     [2] show_sumtype(io::IOContext{Base.TTY}, x::Main.Ex.FooWrapper{Main.Ex.Foo, 0, 0, UInt8})
       @ Main.Ex ./REPL[4]:15
     [3] show(io::IOContext{Base.TTY}, x::Main.Ex.FooWrapper{Main.Ex.Foo, 0, 0, UInt8})
       @ Main.Ex ~/.data/julia/packages/SumTypes/Hn80O/src/sum_type.jl:293
     [4] print(io::IOContext{Base.TTY}, x::Main.Ex.FooWrapper{Main.Ex.Foo, 0, 0, UInt8})
       @ Base ./strings/io.jl:35
--- the last 3 lines are repeated 9279 more times ---
 [27842] show_sumtype(io::IOContext{Base.TTY}, x::Main.Ex.FooWrapper{Main.Ex.Foo, 0, 0, UInt8})
       @ Main.Ex ./REPL[4]:15
 [27843] show_sumtype(io::IOContext{Base.TTY}, m::MIME{Symbol("text/plain")}, x::Main.Ex.FooWrapper{Main.Ex.Foo, 0, 0, UInt8})
       @ SumTypes ~/.data/julia/packages/SumTypes/Hn80O/src/SumTypes.jl:61
 [27844] show(io::IOContext{Base.TTY}, m::MIME{Symbol("text/plain")}, x::Main.Ex.FooWrapper{Main.Ex.Foo, 0, 0, UInt8})
       @ Main.Ex ~/.data/julia/packages/SumTypes/Hn80O/src/sum_type.jl:294
 [27845] display(d::REPL.REPLDisplay, x::Any)
       @ REPL /usr/share/julia/stdlib/v1.9/REPL/src/REPL.jl:281

Support for kwdef constructors

e.g. I imagine a syntax like

@sum_type @kwdef AT begin
    A(common_field::Int = 3, a::Bool = true, b::Int)
    B(common_field::Int = 1, a::Int, b::Float64 = 4.0, d::Complex)
end

and so it will work creating new all keywords constructors for each variant just like the kwdef macro works

julia> A(; b = 4)
A(3, true, 4)::AT

I will try to work out the details if this feature is welcomed

Improve error message when missing parenthesis

Rust's enums look like this:

enum Foo {
    A,
    B,
}

when duplicating the code in Julia:

julia> @sum_type Foo begin
           A
           B
       end
ERROR: MethodError: no method matching (::SumTypes.var"#3#12"{Symbol, Vector{Any}})(::Symbol)
[ very long stacktrace ]

Since this particular typo (namely, typing A instead of A()), is probably going to happen a lot, it may be worth having a good error message for this.

TagBot trigger issue

This issue is used to trigger TagBot; feel free to unsubscribe.

If you haven't already, you should update your TagBot.yml to include issue comment triggers.
Please see this post on Discourse for instructions and more details.

If you'd like for me to do this for you, comment TagBot fix on this issue.
I'll open a PR within a few hours, please be patient!

Noob question: performance tradeoffs vs. the MLStyle approach to sum types

Hi there! I'm really impressed with the technical depth of this package, and I'm keen on trying to deepen my understanding of how it's designed and why.

My question

Assuming my understanding of the SumTypes and MLStyle approaches to implementing sum types is correct (see below), my general question is "what are the performance tradeoffs between these approaches?"

Sub questions:

  • Are the structs defined by SumTypes fixed size no matter which conceptual type they represent, or does their generic typing afford flexibility there?
    • If so, does that trade off potentially higher memory usage?
    • If so, does it enable performance wins for things like an Array, since indexing is simpler?
    • If not, does that trade off performance for things like Arrays?
  • What kinds of use cases would one of the two approaches offer substantially better performance than the other, and in which kind of use cases would the performance difference be small?
  • In general (beyond the world of sum types), how bad is it to use an abstract type vs. a union on concrete types vs. a single concrete type for things like Arrays?

My (possibly faulty) understanding of how we can implement sum types in Julia

As far as I can tell, there are two main options to offering an algebraic datatype experience in Julia.

First there's SumTypes, which like Unityper defines a single struct which can serve to store and access one of several different "conceptual" types.

E.g.

# Let's look under the hood of SumTypes.@sum_type.
macroexpand(
    @__MODULE__,
    :(
        SumTypes.@sum_type Example begin
            Stringy(value::String)
            Symbolic(name::Symbol)
        end
    )
)

# We get this.
($(Expr(:toplevel, quote
    #= /home/luke/.julia/packages/SumTypes/qpNRC/src/sum_type.jl:260 =#
    struct Example{var"#N#", var"#M#", var"#FT#"}
        bits::(Tuple{Vararg{T, N}} where {N, T}){var"#N#", UInt8}
        ptrs::(Tuple{Vararg{T, N}} where {N, T}){var"#M#", Any}
        var"#tag#"::var"#FT#"
        1 + 1
    end
...  # <truncated>

The rest of the generated code is pretty involved, so I don't follow completely, but my understanding is that we're doing some pretty heavy lifting to somewhat reimpliment structs at a pretty low level (including unsafe memory viewing?). My guess is that we do this for performance reasons, but I don't really understand where the performance gains come from.

On the other hand, MLStyle seems to just be providing syntactic sugar around what I'm assuming will eventually be equivalent to isa checks on multiple different concrete implementations of an abstract type. In other words, still using Julia's type system to define an abstract type and several concrete subtypes (one for each of the possible types of the sum type).

# Let's look under the hood of MLStyle.@data.
macroexpand(
    @__MODULE__,
    :(
        MLStyle.@data Example begin 
            Stringy(value::String)
            Symbolic(name::Symbol)
        end
    )
)

quote
    abstract type Example end
    struct Stringy{} <: Example
        #= REPL[7]:5 =#
        value::String
    end
    nothing
    begin
        #= /home/luke/.julia/packages/MLStyle/SLOsr/src/Record.jl:95 =#
        #= REPL[7]:5 =#
        #= /home/luke/.julia/packages/MLStyle/SLOsr/src/Record.jl:96 =#
        function (MLStyle).pattern_uncall(t::Type{<:Stringy}, self::Function, type_params::Any, type_args::Any, args::Any)
            #= /home/luke/.julia/packages/MLStyle/SLOsr/src/Record.jl:96 =#
            #= /home/luke/.julia/packages/MLStyle/SLOsr/src/Record.jl:103 =#
            #= REPL[7]:5 =#
            #= /home/luke/.julia/packages/MLStyle/SLOsr/src/Record.jl:104 =#
            (MLStyle.Record._compile_record_pattern)(t, self, type_params, type_args, args)
        end
    end
    struct Symbolic{} <: Example
        #= REPL[7]:6 =#
        name::Symbol
    end
    nothing
    begin
        #= /home/luke/.julia/packages/MLStyle/SLOsr/src/Record.jl:95 =#
        #= REPL[7]:6 =#
        #= /home/luke/.julia/packages/MLStyle/SLOsr/src/Record.jl:96 =#
        function (MLStyle).pattern_uncall(t::Type{<:Symbolic}, self::Function, type_params::Any, type_args::Any, args::Any)
            #= /home/luke/.julia/packages/MLStyle/SLOsr/src/Record.jl:96 =#
            #= /home/luke/.julia/packages/MLStyle/SLOsr/src/Record.jl:103 =#
            #= REPL[7]:6 =#
            #= /home/luke/.julia/packages/MLStyle/SLOsr/src/Record.jl:104 =#
            (MLStyle.Record._compile_record_pattern)(t, self, type_params, type_args, args)
        end
    end
end

This seems like it offers fewer "sharp edges" with respect to matching the standard mental model of types in Julia, but I'm guessing it is trading off performance somewhere, and I'm hoping to better understand where!

Allow specifying a supertype

I had forgot to file this after discussing it on Zulip, but at the time it was said that it'd be rather easy and not a bad idea to support deriving from an abstract type.

I don't have a need for this anymore, but wanted to file it for completion sake.

A `compactify_interface` feature that takes a list of types and a list of functions that have methods for these types and "compactifies" them

As seen in discussions on slack and discourse:

I want to be able to do:

for g in list_of_different_types_of_gates:
    apply!(state, g)
end

without dynamic dispatch or allocations.

apply! is a function that has a ton of non-allocating fast methods of the form apply!(::State, ::OneOfManyDifferentTypes)::State

I have the following approach:

I made a function that takes

  • a list of types
  • a list of functions that have methods defined for these types

and it creates

  • one single sumtype
  • for all the given functions, it defines a method for the new sumtype
    In pseudo code it looks like
make_sumtype_infrastructure(my_many_types, [:fun1, :fun2]) |> eval
which generates the following code
@sum_type MySumType
    OneOfMyOriginalTypes(...)
    ...
end
function fun1(arg::MySumType)
    @case arg begin
        OneOfMyOriginalTypes(...) => fun1(OneOfMyOriginalTypes(...))
        ...
    end
end

The implementation is at QuantumSavory/QuantumClifford.jl@17cc1ca

It is based on a discussion from https://discourse.julialang.org/t/ann-sumtypes-jl-v0-4/97038/4?u=krastanov

Having some reliable form of this officially supported by the library would be amazing (I suspect what I have written is very prone to breaking, as I am not experienced with meta-programing). Especially if it has some way for expanding the sumtype without a ton of invalidations and recompilation (maybe by "padding" the sumtype so that it can be extended) -- that way we can still keep some of the amazing interoperability of julia packages.

Using parametric struct and only defining the top level type makes it errors out

I'm trying to use SumTypes to replicate Rust Result type, and I expected the Result type to be able to carry all types. Here's the example code:

using SumTypes

#Rust like result type
@sum_type Result{A, B} begin
    Ok{A}(::A)
    Err{B}(::B)
end

#Some default errors
struct SequenceError{T}
    prev::T
    cur::T
end

struct Good 
    good::Int
end


function test(a) :: Result{Good, SequenceError}
    if a
        return Ok(Good(1))
    else
        return Err(SequenceError(1,1))
    end
end

Somehow if my test function tries to return SequenceError it errors out with cannot convert error:

julia> test(true)
Ok(Good(1))::Result{Good, SequenceError}

julia> test(false)
ERROR: MethodError: Cannot `convert` an object of type 
  Result{Uninit,SequenceError{Int64}} to an object of type 
  Result{Good,SequenceError}

Closest candidates are:
  convert(::Type{<:Result{A, B}}, ::Result{Uninit, B}) where {A, B}
   @ Main ~/.julia/packages/SumTypes/Mrn3L/src/sum_type.jl:197
  convert(::Type{T}, ::T) where T
   @ Base Base.jl:84
  convert(::Type{Result{A, B}}, ::Result{Uninit, B}) where {A, B}
   @ Main ~/.julia/packages/SumTypes/Mrn3L/src/sum_type.jl:196
  ...

Stacktrace:
 [1] test(a::Bool)
   @ Main ~/workdir/nanoavionics/result.jl:24
 [2] top-level scope
   @ REPL[59]:1

But if I fully specify the type then it works:

julia> function test2(a) :: Result{Good, SequenceError{Int}}
           if a
               return Ok(Good(1))
           else
               return Err(SequenceError(1,1))
           end
       end
test2 (generic function with 1 method)

julia> test2(true)
Ok(Good(1))::Result{Good, SequenceError{Int64}}

julia> test2(false)
Err(SequenceError{Int64}(1, 1))::Result{Good, SequenceError{Int64}}

This is maybe #44, but I'm not that familiar with Julia type system yet

implement Base.getindex

From a Zulip discussion with Mason:

Can I suggest implementing getindex(::SumTypes.Variant, ::Int)? Seems odd how we can use tuple destructuring but not indexing when we only care about one in the middle which requires 2 lines.

yeah, I can do that. Variant is basically just NamedTuple structurally

sum types are not `isbits`

I was surprised to observe that sum types are not isbits. In my (flawed) mental model of this library, one of the benefits is that it provides for neat memory-contiguous storage of vectors of variants (or at least I thought memory-contiguous storage in julia vectors is possible only for isbits types).

It seems that was indeed the case in 0.4, but it was changed in 0.5 (see examples below).

The isbits property would be very valuable for me, because it would make it easy to dispatch over an array of variants in a loop on a GPU (because the GPU kernel macros do not seem to work for non-isbits arguments).

Hence I have two questions:

  • Is there an easy way to make my variants isbits?
  • Is there some other way to use instances of variants as arguments of a gpu kernel?

Examples of 0.4 vs 0.5 behavior:

# on 0.4
julia> @sum_type myST begin
           A
           B(::Int)
           C(::Int, ::Int)
       end

julia> A |> isbits
true

julia> A |> sizeof
24

julia> B(1) |> isbits
true

julia> B(1) |> sizeof
24
# on 0.5
julia> @sum_type myST begin
           A
           B(::Int)
           C(::Int, ::Int)
       end

julia> A |> isbits
false

julia> A |> sizeof
24

julia> B(1) |> isbits
false

julia> B(1) |> sizeof
24

julia> A.data |> isbits
true

StackOverflow in weird usage

The following occurs only with the (useless) <:Foo constraint. It was suggested this still be posted as a bug:

julia> module Ex

       using SumTypes

       struct Foo end

       @sum_type FooWrapper{T<:Foo} begin
           FooWrapper1{T}(::T)
           FooWrapper2{T}(::T)
           FooWrapper3{T}(::T)
       end

       end
WARNING: replacing module Ex.
Main.Ex

julia> Ex.FooWrapper2(Ex.Foo())
ERROR: StackOverflowError:
Stacktrace:
 [1] flagtype(#unused#::Type{Main.Ex.FooWrapper{Main.Ex.Foo, 0, 0, UInt8}}) (repeats 79984 times)
   @ Main.Ex ~/.data/julia/packages/SumTypes/qpNRC/src/sum_type.jl:263

Strange error when sum_type macro is used inside a begin block

For non-parametric sum types this works fine

julia> using SumTypes

julia> begin
           mutable struct A
               x::Int
           end
           @sum_type B begin
               C(a::A)
           end
       end

but when you try with a parametric one a strange error with no informative stacktrace appears:

julia> using SumTypes

julia> begin
           mutable struct A{X}
               x::X
           end
           @sum_type B{X} begin
               C{X}(a::A{X})
           end
       end
ERROR: syntax: SlotNumber objects should not occur in an AST
Stacktrace:
 [1] top-level scope
   @ REPL[2]:1
 [2] top-level scope
   @ REPL[2]:5

Default `@cases`?

I was wondering if it is possible to have a default branch in a @cases? For some methods it only makes sense to write logic for a few of the possible types, and throw an error if an unexpected type is passed. However, currently this requires one to write out all unexpected types, rather than a default.

default in `@cases` when unpacking is not necessary

I have a situation in which a sumtype instance will be passing through "filters", where they are either "consumed" if they are a specific particular variant or passed on to the next "filter".

Is an additional "default" option possible, as discussed in #31 ?

Opening a new issue, because I have the impression the conversation in 31 was focused on default with unpacking

Pattern matching taking more arguments

I have a sum type like this:

@sum_type Indel begin
    Insertion()
    Deletion()
end

struct Edit
    pos::UInt
    x::Indel
end

And now I'd like to write a function with Edit, for a simple example, let's say I want to do this:

@case Indel report(()::Insertion, pos::UInt) = print("Insertion at position $pos")
@case Indel report(()::Deletion, pos::UInt) = print("Deletion at position $pos")
foo(x::Edit) = report(x.x, x.pos)

Unfortunately, I can't make this work - or something like that to work. One workaround is to do this:

@case Indel function report((x,)::Insertion)
    pos -> print("Insertion at position $pos")
end
foo(x::Edit) = report(x.x)(x.pos)

, and it does produce efficient code, but the syntax is very weird. @MasonProtter have you got any suggestions on how to approach this problem?

isvariant feature

I was talking to Mason on Zulip, and we found the usefulness for an isvariant function, which he factored down to the following:

isvariant(st::MySumType, s::Symbol) = SumTypes.get_tag(st) === SumTypes.symbol_to_flag(MySumType, s);
SumTypes.unwrap(st::MySumType, s::Symbol) = SumTypes.unwrap(st, SumTypes.constructor(MySumType, Val{s}))

The use of it he provided looks like:

let x = MyVariant(1,2)
    if isvariant(x, :MyVariant)
        SumTypes.unwrap(x, :MyVariant)
    end
end

Should we add this function/default method to the package? Seems like a harmless enough 2 lines anyway.

Do not defined variants in main module

If you make heavy use of sum types or enums, you'll often have name clashes. E.g. in some code I have clashes between variants of genes (e.g. variant HA), and variants of proteins with the same name (variant HA).

It would be nice if each variant was defined in its own bare module, and could be accessed through the enum type. For example:

@sum_type Foo begin
    A()
    B()
end

A # NameError
Foo.A # Ok!

@dalum made a proof of concept to me yesterday for how this could be achieved by defining a module for each sum type.

Support const decorator for fields

julia> @sum_type X begin
           A(const x::Int)
           B(x)
       end
ERROR: ParseError:
# Error @ REPL[7]:2:7
@sum_type X begin
    A(const x::Int)
#     └──────────┘ ── expected assignment after `const`
Stacktrace:
 [1] top-level scope
   @ none:1

mutable sum types?

I'd like to use sum types for a package, but I would need mutable sum types, is it possible to support them?

For now I see that it is already possible with some Base.RefValue wrapping but I'm not sure this is the correct approach e.g.

julia> @sum_type S begin
           A(x::Base.RefValue{Int}, y::Base.RefValue{Int})
           B(x::Base.RefValue{Int}, z::Base.RefValue{Float64})
       end

julia> s = A(Ref(0), Ref(0))
A(Base.RefValue{Int64}(0), Base.RefValue{Int64}(0))::S

julia> s.data.data[1][] = 3
3

julia> s
A(Base.RefValue{Int64}(3), Base.RefValue{Int64}(0))::S

but I'm not sure neither if this is optimal (is this as fast as mutable structs?) nor if it safe since I'm using internal fields to modify data. I wonder if maybe a more "integrated" solution is possible.

getproperty for a SumType

I think that making the package compatible with accessing data through getproperty (and so also the dot syntax) would make this package easier to integrate with other packages, e.g. I would like that this wouldn't error

julia> using SumTypes

julia> @sum_type AT begin
           A(x::Int, y::Int)
           B(x::Int, z::Float64)
       end

julia> a = A(1, 1)
A(1, 1)::AT

julia> a.x
ERROR: type AT has no field x
Stacktrace:
 [1] getproperty(x::AT, f::Symbol)
   @ Base ./Base.jl:37
 [2] top-level scope
   @ REPL[4]:1

Document allowing field names

I came here to ask if it would be possible to add names to the fields of sum types, but then, I noticed in #30 that it appears you've already added this feature!

However, none of the examples in the README demonstrate adding them. It would be nice to add them to at least one, potentially giving the Re example some dumby names.

Memory corruption in 0.9.3

We have noticed intermittent crashes since upgrading from Spglib 0.9.2 to 0.9.3. Looks like the only real difference between these versions is the switch from enums to SumTypes: singularitti/Spglib.jl@819b7f6.

Perhaps there is memory corruption when reading an spglib_jll response into a sum type?

Frequently the result is a segfault, but here is one stacktrace we got that seems interpretable:

Failed to precompile Sunny [2b4a2ac8-8f8b-43e8-abf4-3cb0c45e8736] to "/home/runner/.julia/compiled/v1.9/Sunny/jl_GxA1Pq".
ERROR: LoadError: ArgumentError: cannot convert NULL to string
Stacktrace:
  [1] unsafe_string
    @ ./strings/string.jl:84 [inlined]
  [2] unsafe_string
    @ ./c.jl:193 [inlined]
  [3] get_error_message(code::Spglib.SpglibReturnCode)
    @ Spglib ~/.julia/packages/Spglib/rGUIW/src/error.jl:37
  [4] check_error
    @ ~/.julia/packages/Spglib/rGUIW/src/error.jl:50 [inlined]
  [5] get_spacegroup_type(hall_number::Int64)
    @ Spglib ~/.julia/packages/Spglib/rGUIW/src/symmetry.jl:291
  [...]

This appeared on our Github actions CI with Julia 1.9.4, Ubuntu, x86. Full trace here: https://github.com/SunnySuite/Sunny.jl/actions/runs/7629267203/job/20782289111?pr=217

I haven't seen the crashes yet on my Mac. On Linux/x86, reproducing might be as simple as ] add Sunny#spglib_crash and then using Sunny.

`@sum_type` doesn't work with constrained type parameter in variant

e.g. this

julia> using SumTypes

julia> @sum_type A{X<:Union{Real, SumTypes.Uninit}} begin
           B{X<:Union{Int, SumTypes.Uninit}}(a::X)
           C{X}(b::X)
       end

throws

ERROR: LoadError: constructor parameters (Any[:(X <: Union{Int, SumTypes.Uninit})]) for B, not a subset of sum type parameters [:X]
Stacktrace:
 [1] error(s::String)
   @ Base ./error.jl:35
 [2] generate_constructor_data(T_name::Symbol, T_params::Vector{Symbol}, T_params_constrained::Vector{Any}, T_nameparam::Expr, hide_variants::Bool, blk::Expr)
   @ SumTypes ~/.julia/dev/SumTypes/src/sum_type.jl:75
 [3] _sum_type(T::Expr, hidden::QuoteNode, blk::Expr)
   @ SumTypes ~/.julia/dev/SumTypes/src/sum_type.jl:31
 [4] _sum_type(T::Expr, blk::Expr)
   @ SumTypes ~/.julia/dev/SumTypes/src/sum_type.jl:6
 [5] var"@sum_type"(__source__::LineNumberNode, __module__::Module, T::Any, args::Vararg{Any})
   @ SumTypes ~/.julia/dev/SumTypes/src/sum_type.jl:3
in expression starting at REPL[6]:1

but I think this could work

Symbols not accessible in modules using only `using SumTypes: @sum_type`

julia> module Ex
       using SumTypes: @sum_type
       struct Foo end
       @sum_type ST begin
         Bar(::Foo)
       end
       end
Main.Ex

julia> Ex.Bar(Ex.Foo())
ERROR: UndefVarError: `full_type` not defined
Stacktrace:
 [1] flagtype(#unused#::Type{Main.Ex.ST})
   @ Main.Ex ~/.data/julia/packages/SumTypes/qpNRC/src/sum_type.jl:263
 [2] symbol_to_flag(#unused#::Type{Main.Ex.ST}, sym::Symbol)
   @ Main.Ex ~/.data/julia/packages/SumTypes/qpNRC/src/sum_type.jl:266
 [3] Main.Ex.Bar(_1::Main.Ex.Foo)
   @ Main.Ex ~/.data/julia/packages/SumTypes/qpNRC/src/sum_type.jl:179
 [4] top-level scope
   @ REPL[4]:1

SumType becomes less memory efficient with non-isbits fields

I noticed this when developing https://github.com/JuliaDynamics/MixedStructTypes.jl e.g.

using DynamicSumTypes

@sum_structs :on_types A{X,Y} begin
           @kwdef mutable struct B{X}
               a::Tuple{X, X} = (1,1)
               b::Tuple{Float64, Float64} = (1.0, 1.0)
               const c::Symbol = :s
           end
           @kwdef mutable struct C
               a::Tuple{Int, Int} = (1,1)
               const c::Symbol = :q
               d::Int32 = Int32(2)
               e::Bool = false
           end
           @kwdef struct D{Y}
               a::Tuple{Int, Int} = (1,1)
               c::Symbol = :s
               f::Y = 2
               g::Tuple{Complex, Complex} = (im, im)
           end
       end


vec_a = A[rand((B,C,D))() for _ in 1:10^5]

now

Base.summarysize(vec_a) #5053868 (wow!)

let's use instead

using DynamicSumTypes

@sum_structs :on_types A{X,Y} begin
           @kwdef mutable struct B{X}
               a::Tuple{X, X} = (1,1)
               b::Tuple{Float64, Float64} = (1.0, 1.0)
               const c::Symbol = :s
           end
           @kwdef mutable struct C
               a::Tuple{Int, Int} = (1,1)
               const c::Symbol = :q
               d::Int32 = Int32(2)
               e::Bool = false
           end
           @kwdef struct D{Y}
               a::Tuple{Int, Int} = (1,1)
               c::Symbol = :s
               f::Y = "s"  ### non isbit
               g::Tuple{Complex, Complex} = (im, im)
           end
       end


vec_a = A[rand((B,C,D))() for _ in 1:10^5]

now

Base.summarysize(vec_a) #8863933

Is there a way to improve this? Or is this expected? Notice that in the second case the memory occupied is very similar to just a compactification of all fields in a unique struct

Unable to dispatch on child types

Looks like I can only dispatch on the main type but not the child types. Is that expected?

julia> @sum_type Party begin
           Group(::Int)
           Name(::String)
       end

julia> invite(g::Group) = 1

julia> invite(n::Name) = 2

julia> invite(Group(1))
ERROR: MethodError: no method matching invite(::Party)

julia> invite(p::Party) = 3

julia> invite(Group(1))
3

julia> methods(invite)
# 3 methods for generic function "invite":
[1] invite(g::Group) in Main at REPL[3]:1
[2] invite(n::Name) in Main at REPL[4]:1
[3] invite(p::Party) in Main at REPL[6]:1

julia> versioninfo()
Julia Version 1.7.0
Commit 3bf9d17731 (2021-11-30 12:12 UTC)
Platform Info:
  OS: macOS (x86_64-apple-darwin19.5.0)
  CPU: Apple M1
  WORD_SIZE: 64
  LIBM: libopenlibm
  LLVM: libLLVM-12.0.1 (ORCJIT, westmere)

Doc Comments with `@sum_type`

I'm trying to run my project PandemicAIs.jl on commit e606d38. Adding the doc comment at line 13 of src/PandemicAIs.jl provokes the following error on project startup (paths have been clobbered):

$ julia --project -ie "using PandemicAIs"
[ Info: Precompiling PandemicAIs [5b8562fb-f4d8-4835-9d02-90268c707351]
ERROR: LoadError: cannot document the following expression:

#= /fake_path/PandemicAIs.jl/src/PandemicAIs.jl:20 =# @sum_type PlayerAction{C} begin
        #= /fake_path/PandemicAIs.jl/src/PandemicAIs.jl:21 =#
        Drive{C}(dest::C)
        #= /fake_path/PandemicAIs.jl/src/PandemicAIs.jl:22 =#
        DirectFlight{C}(dest::C)
        #= /fake_path/PandemicAIs.jl/src/PandemicAIs.jl:23 =#
        CharterFlight{C}(dest::C)
        #= /fake_path/PandemicAIs.jl/src/PandemicAIs.jl:24 =#
        ShuttleFlight{C}(dest::C)
        #= /fake_path/PandemicAIs.jl/src/PandemicAIs.jl:25 =#
        BuildStation
        #= /fake_path/PandemicAIs.jl/src/PandemicAIs.jl:26 =#
        DiscoverCure{C}(dest::C, cards::Vector{C})
        #= /fake_path/PandemicAIs.jl/src/PandemicAIs.jl:27 =#
        TreatDisease(target::Disease)
        #= /fake_path/PandemicAIs.jl/src/PandemicAIs.jl:28 =#
        ShareKnowledge(player::Int)
        #= /fake_path/PandemicAIs.jl/src/PandemicAIs.jl:29 =#
        Pass
    end

'@sum_type' not documentable. See 'Base.@__doc__' docs for details.

Stacktrace:
 [1] error(::String, ::String)
   @ Base ./error.jl:44
 [2] top-level scope
   @ /fake_path/PandemicAIs.jl/src/PandemicAIs.jl:13
 [3] include
   @ ./Base.jl:457 [inlined]
 [4] include_package_for_output(pkg::Base.PkgId, input::String, depot_path::Vector{String}, dl_load_path::Vector{String}, load_path::Vector{String}, concrete_deps::Vector{Pair{Base.PkgId, UInt128}}, source::Nothing)
   @ Base ./loading.jl:2010
 [5] top-level scope
   @ stdin:2
in expression starting at /fake_path/PandemicAIs.jl/src/PandemicAIs.jl:1
in expression starting at stdin:2
ERROR: Failed to precompile PandemicAIs [5b8562fb-f4d8-4835-9d02-90268c707351] to "/home/laura/.julia/compiled/v1.9/PandemicAIs/jl_yf04M3".
Stacktrace:
 [1] error(s::String)
   @ Base ./error.jl:35
 [2] compilecache(pkg::Base.PkgId, path::String, internal_stderr::IO, internal_stdout::IO, keep_loaded_modules::Bool)
   @ Base ./loading.jl:2260
 [3] compilecache
   @ ./loading.jl:2127 [inlined]
 [4] _require(pkg::Base.PkgId, env::String)
   @ Base ./loading.jl:1770
 [5] _require_prelocked(uuidkey::Base.PkgId, env::String)
   @ Base ./loading.jl:1625
 [6] macro expansion
   @ ./loading.jl:1613 [inlined]
 [7] macro expansion
   @ ./lock.jl:267 [inlined]
 [8] require(into::Module, mod::Symbol)
   @ Base ./loading.jl:1576%

Is it not possible to document the @sum_types declaration, or have I missed something?

`@cases` on tuples

@cases (ox, oy) begin
  (Some(x), Some(y)) => x + y
  ...
end

This would be useful.

Inner constructors for variants

Ref #6

There is currently no way of enforcing invariants on variants, because you can't access their inner constructor.
One easy option is to do the following:

  • Define an unexported struct struct Unsafe end
  • The inner constructor for a variant MyVariant{A,B}(::A, ::B, ::Int) is then MyVariant{A,B}(::Unsafe, ::A, ::B, ::Int)
  • An outer constructor MyVariant{A,B}(a::A, b::B, c::Int) = MyVariant{A,B}(Unsafe(), a, b, c) is added by default. This can then be overwritten.

Make constructors take explicit types

MWE of issue:

julia> @sum_type Foo begin
           A()
           B(::UInt8)
       end

julia> B(1) # doesn't work, must take UInt8
ERROR: TypeError: in new, expected UInt8, got a value of type Int64
Stacktrace:
 [1] B(_1::Int64)
   @ Main ~/code/SumTypes.jl/src/SumTypes.jl:99
 [2] top-level scope
   @ REPL[10]:1

julia> B(x::Integer) = B(UInt8(x))
B

julia> B(1)
ERROR: StackOverflowError:

The issue is that the only pre-defined constructor is B(::Any), whereas I think it should be B(::UInt8). Then the user can always add B(x) = B(convert(UInt8, x)) if they want.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.