[ANN] SumTypes.jl 0.1

Hi everyone, I’ve been playing around a bit lately with the idea of ‘sum types’ (aka tagged unions, or enums in Rust circles). These are quite the hotness these days in functional programming and I kept having trouble fully getting my head around them, so I finally just sat down and implemented them in julia, which has clarified things for me a fair amount.

Check it out at SumTypes.jl.

I think at least in a dynamic language like Julia, there’s good reason to be skeptical of any claim that these are an alternative to Union, however I think maybe there are some uses for these things, and at the very least if someone tells you

Julia is a terrible programming language, it doesn’t even have sum types, the greatest language feature ever!

you can just tell them

Big deal, Mason made a sum type package in 55 lines of julia code.


If you really don’t wanna click on the link, here’s an excerpt from the README where I explain a bit about what a sum type is and how they work:

SumTypes.jl

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). From a category theory perspective, sum types are interesting because they are dual to Tuples (whatever that means).

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 perhaps someone can find a fun use case.

Let’s explore a very fundamental sum type (fundamental in the sense that all other sum types may be derived from it):

julia> using SumTypes

julia> @sum_type Either{A, B} begin
           Left{A, B}(::A)
           Right{A, 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,B} and Right{A,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 we construct some instances of Either:

julia> Left{Int, Int}(1)
Either{Int64,Int64}(Left{Int64,Int64}(1))

julia> Right{Int, Float64}(1.0)
Either{Int64,Float64}(Right{Int64,Float64}(1.0))

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

Pattern matching on Sum types

Because of the structure of sum types, they lend themselves naturally to things like pattern matching. As such, SumTypes.jl re-exports MLStyle.@match from MLStyle.jl and automatically declares Sum types as MLStyle record types so they can be destructured:

julia> @match Left{Int, Int}(1) begin
           Either(Left(x))  => x + 1
           Either(Right(x)) => x - 1
       end
2

julia> @match Right{Int, Int}(1) begin
           Either(Left(x))  => x + 1
           Either(Right(x)) => x - 1
       end
0
28 Likes

How is sum type different to Union? Also,

If we live by what others thinks, are our lives still ours?

2 Likes

A sum type is just a container that holds values that can be picked from a union. e.g. for the Either example, the macro basically generates the code:

julia> struct Left{A, B}
           l::A
           Left{A, B}(x) where {A, B} = Either{A, B}(new{A,B}(x))
       end

julia> struct Right{A, B}
           r::B
           Right{A, B}(x) where {A, B} = Either{A, B}(new{A,B}(x))
       end

julia> struct Either{A, B}
           data::Union{Left{A,B}, Right{A,B}}
       end

Sum types are more restrictive and less expressive than unions and that’s why they’re helpful for static compilation, or cases where you want to be able to enumerate every possibility and not let people add cases in post-hoc.

Either{A,B} is kinda the prototypical sum type, and it’s the one that you’ll hear some static language enthusiasts say is “basically a union but better”, but fundamentally with union we have that A <: Union{A, B} and we have that A === Union{A, A}. Both of those (very useful!) properties don’t hold with sum types.

4 Likes

It’s like tha Maybe design heavily criticised by Rich Hickey. Julia’s union is superior. I wish there is a more convenience syntax though.

Yes, this is exactly the sort of object Rich Hickey criticized in his Maybe Not talk.

That’s not a very meaningful statement unless you say what it’s superior for. I certainly like Unions, but there are cases where sum types are nice (like type checking).

An important thing to note here is that you can build sum types if you have unions, but you can’t build unions if all you have are sum types. In some circumstances, having a more restricted type system like the one you get with sum types can be useful.

Sum types are useful if what you desire is very strong coupling between different parts of your code, and unions / subtyping are useful if you want more flexible, loosely coupled ‘open’ code.

7 Likes

If anyone is curious about seeing sum types in use, there was a nice blog post circulating a little while ago called Optionality in the type systems of Julia and Rust (originally titled Sum types in Julia and Rust).

In the blog post, the author considers building up a little game shows one way to solve their problem in Rust using sum types and pattern matching (called enums here), but then shows that julia’s enums are not up to the task.

A transcription of the Rust ‘enum’ code would look like this with SumTypes.jl:

using SumTypes

@sum_type PlayerClass begin
    Solarian(health::Base.RefValue{Int}) # using a Ref here lets us mutate the health of a Solarian
    Polarian(health::Base.RefValue{Int}) # in a real game there'd probably be more fields here
    Centaurian(health::Base.RefValue{Int})
end

struct GameState
    daytime::UInt
    player::PlayerClass
end

function homestar(p::PlayerClass)
    @match p.data begin
        ::Solarian => "sun"
        # ...
    end
end

function greet(self::PlayerClass, other::PlayerClass)
    @match (self.data, other.data) begin
        (::Centaurian, ::Polarian) => println("Hello, fellow three-star-systemer!")
        # ...
    end
end

function takedamage!(p::PlayerClass)
    @match p.data begin
        Solarian(health) => (health[] -= 42)
        # ...
    end
end

Now at the repl, let’s try and damage a Solarian:

julia> let p = Solarian(Ref(100))
           takedamage!(p)
           p
       end
PlayerClass(Solarian(Base.RefValue{Int64}(58)))

Tada!

Of course, the julia example using multiple dispatch is actually a fair deal nicer than this and can be extended with new player types quite easily later on if desired.

However, I think it’s nice to be able to show off that we can do this same business with sum types as well if we found the need.

4 Likes

I don’t understand the point of this, and I doubt I will ever have cause to use it, but hot damn I love being part of a community where people build stuff like this just for the hell of it to learn, and make something that might be really useful for someone, sometime.

21 Likes

Sorry for being thick, but I still don’t get the motivation for sum types in Julia. I understand that they are useful in Rust, but idiomatic Julia code would simply do these things differently (subtyping or traits).

Programming languages are composed of various features that (ideally) fit together nicely. One cannot just pick one particular feature and expect to have something exactly like that in another language, or claim that some other language is less powerful because it doesn’t have X.

For example, I am sure that the Rust community would be surprised if someone claimed that their language is less powerful than C because it lacks undisciplined pointer manipulation. For them it is kind of the point.

5 Likes

That is a logical argument which seems to make sense. However, history is full of logical arguments that seemed to make sense at first, but ultimately turned out to be flawed.

The existence of a SumTypes package will provide empirical data, as it will either end up being used or not.

9 Likes

I think you misunderstand. I am not arguing the package is not useful, just asking about the use case.

It seems an interesting idea, I just can’t figure out the motivation in the context of Julia. I think that asking about use cases in a package announcement is fair.

5 Likes

My motivation was purely just coming from me wanting to wrap my head around the concept. I basically ended up making this because every time I hear about sum types, I didn’t feel like I fully ‘got’ them. I’m sure there’s things I’m missing, but I feel like there’s a lot of Rust and Haskell code I’ve looked at before that I now understand.

I don’t really think these things are very useful in current modern julia. One thing I will say though is that these things may be useful in a hypothetical future where we have a statically compiled subset of the language, but that really wasn’t my motivation.

This was just tinkering on my behalf and I just wanted to share the thing I made with the community in case anyone learned anything from it or found it useful.

14 Likes

People sometimes want a missing value of “type Union{Missing,T}” (which is of course impossible with the missing we have). Isn’t it a reasonable use-case? Although DataValues.jl already exists, I think it’s conceivable to create something like it based on SumTypes.jl. There is also ResultTypes.jl that can be implemented based on SumTypes.jl IIUC.

2 Likes

Yes, those are nice examples. Both of those packages essentially are just manually making specific sum types.

It’s reasonable to ask

how much easier would it be to make these packages with SumTypes.jl?

And the honest answer is “Barely easier” because making a sum type manually isn’t very hard, and the majority of the LOC in those packages are method definitions.

That said, perhaps SumTypes.jl can offer enough utilities in the future that it’d be worth it for them to use it.

1 Like

Yeah, I’m interested in the tools that SumTypes.jl can provide. For dealing with generic sum types, especially when nested, I imagine having a uniform interface designed from a first principle could be useful.

1 Like

Typing in julia is not that perfect.

  • Empty branch type / Fields on leaf type only is very opionated

  • Single inheritance everywhere with abstract is very opionated too.

    • Except with Union. but which one should we use.
  • Unnamed union, named struct.

    • lite struct eg. namedtuple. no lite union ?
  • Traits. Are they in the language ?
    Rust has brought the whole prolog to its compiler for them.
    And Julia?

  • etc.

Actually Julia is the Killer language thanks to multidispatch. I can’t write code more than 5 minutes without relying to it. That’s the feature that keep me away from Rust. A killer language too.
And truly, the lisp wisdom that generative typesystem eg. macro is frequently greater than parametric typesystem eg. cpp template madness

But the typesystem is not at the same level. (due also to its so many implementation languages, eg. lisp/julia/c++/llvm)

IMHO, Enhancing / Fixing the typesystem in Julia is one of the great challenge of the decade.
So let’s experiment, and welcome to SumType. It’s good too, if the author has capitalized some knowledge from MLStyle

2 Likes

I wasn’t aware of the type system being broken.

5 Likes

Would that be similar to a ‘parameterized missing’, Missing{T}, or is it something else?

Don’t resort to petty attacks when you make unfounded claims and get called on it.

I bet Tamas still gives you a heart for replying to him anyway though.

11 Likes

How does having knowledge on a particular field makes one less proficient on another one? That’s absurd.

2 Likes