Skip to content

Instantly share code, notes, and snippets.

@evancz
Last active March 12, 2018 13:14
Show Gist options
  • Star 16 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save evancz/06fe634245a3aab4a61b to your computer and use it in GitHub Desktop.
Save evancz/06fe634245a3aab4a61b to your computer and use it in GitHub Desktop.
Imagine conversations and questions for a time when people say "union type" instead of "algebraic data type". Think about how the responses might work if you said "ADT" instead.

Union Types

If Elm community is going to begin referring to "union types" instead of "algebraic data types" we should think about how that will work in practice. What discussions will we have? Will we find ourselves in awkward spots trying to explain things?

The following question/answer pairs simulate things I'd expect to see in a world of "union types". The pairs are grouped by what background I expect the questions to come from so we know the subtext. I have also added some meta comments in italic to explain my phrasing.

One thing to consider is that a lot of learners will not have a person to ask, so the path to arriving at these answers needs to be easy if you are just searching online.

General Questions

What is a union type?
It is a way to put together different types. Maybe a User ID can be an Int or a String. Union types let us create a new UserID type out of smaller parts that represents all the possibilities.

type UserID = OldID Int | NewID String

In Java you'd need like three different class, but we can do it all in one line!

Why do I need to name all the types in the union type?
Think of them as "tags". The tags let us distinguish between them when we have a big list of different types. Depending on which tag it is, we will do different stuff.

How come union types can be recursive and type aliases cannot?
A union type actually creates a brand new type. A type alias is just a shorter name to refer to an existing type, so it gets "expanded" anywhere it is used. That means in a recursive type alias, it'd keep getting expanded infinitely!

From JavaScript, Java, etc.

The essential question here is "how do I live without subclasses or dynamic types?" I think these are also the most crucial questions to handle well.

A crucial thing to remember here is that we're likely to get tons of XY questions so be on the lookout! The best answer may be to ask more questions, dig out the real problem, and bring things back to a more general question like "What are union types?"

I want to make four different kinds of widgets. How do I do it?
You should look into "union types". It's a way to put together a bunch of different types. Each widget can have the type that makes the most sense. Maybe one has graph data as a list of ints but another has user records as a list of names and ages. Union types make it so you can put these all different types in the same list or array or whatever.

I have 3 kinds of bad guys and want to store them all in the same list. Normally I'd make them the subtype of the BadGuy class. How do I do it in Elm?
If you want to use lots of types together, use a union type. This means you can have heterogeneous lists without ever having to worry about up- or down-casting things.

Notice that I say heterogeneous lists are allowed! People typically frame it as, "oh sorry you can't do that, you have to do it with algebraic data types" which is weak and apologetic for no reason. Elm lets you have lists with all sorts of different types, just put them together with a union type. Even better, we can argue that this is an improvement because now we don't have to worry about classes that need to be the subclass of two different things.

Now I have a union type that looks really redundant, this is lame!

type BadGuy
    = Goomba { x:Int, y:Int }
    | Koopa { x:Int, y:Int, sliding:Bool } 

Maybe you can use extensible records. Would something like this work?

type alias BadGuy =
    { x:Int
    , y:Int
    , details : Details
    }

type Details
    = Goomba
    | Koopa { sliding:Bool }

This means if it works on something with an x and y field, it'll also work on any BadGuy so there's no code duplication. You can do lots of other cool stuff with extensible records, so take a look at the docs to see if there's something even better for your case. There are many ways to mix extensible records and union types!

Union types seem harder to extend than subclasses.
Once you get used to extensible records and union types you can do a lot! Check out this post to see how you can write code with the same extensibility properties as Object Oriented language. I find "the Elm way" is usually nicer than this, but it is possible!

I think this is sort of getting at the idea of "open" vs "closed" unions. In Elm a union type is "closed" meaning it cannot be extended arbitrarily by anyone. You can create an "open union" if you do some tricks, though it can always be done other ways that probably are nicer to work with. I'm not sure if it'd be good to have "open" and "closed" as part of the vocabulary we use. Generally speaking, the more concepts you need to introduce in an explanation, the worse the explanation is, so I'd lean towards not using these terms in general.

@scriptin
Copy link

I'd suggest adding a question like "How do I model a chain of inheritance?" For example, I have a Java gamedev framework with classes like Entity, Enemy extends Entity, Goomba extends Enemy. How do I model that in Elm?

I think the general answer for both Elm and Java is "prefer composition over inheritance". As your BadGuy example shows, all types of enemies have something in common - their position and "details" (a "type" of enemy). So this can be modeled as:

type alias KoopaState = { sliding: Bool }

type Details
  = Player
  | Goomba
  | Koopa KoopaState

type alias Position = { x: Int, y: Int }

type alias Entity =
  { id: Int
  , position: Position
  , details: Details
  }

This eliminates the need to do a chain of inheritance. It works the same in Java and often suggested as a good practice, so it can be another selling point for Elm (and other languages with similar type systems).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment