Funktionaali.com

Boundary of stateless blabbery

Enumerations in Essential Language Features, Part 1

This post argues that some form of enumerations (sum types), and the ability to pattern-match on them are essential features for a modern programming language. Featuring Rust, Haskell and Go.

This post is first in a series of posts discussing common programming language features and idioms.

Pattern-matching

Pattern-matching, case, match or switch statements, or, if you prefer, even if-elseif-elseif...else chains are useful control structures. And old, too. Kleene explained the concept already in the 50’s.

Here’s pattern-matching in action, using Go, Haskell and Rust:

// Switch statement in Go
switch x {
   case 1:  return "one"
   case 2:  return "two"
   default: return "too many"
}
-- case statement in Haskell
case x of
   1 -> "one"
   2 -> "two"
   _ -> "too many"
// match statement in Rust
match x {
   1 => "one",
   2 => "two",
   _ => "too many",
}

So far these are identical (modulo syntax). But what if we want to branch on something else than an integer? Say, a custom enumeration (a sum type)?

Enumerations the Modern Way

Recall that enumeration represents choice of one among a set of variants. C has had enums for a long time (although they’re a bit clumsy). Go does not really have them. Rust has them. Haskell calls them abstract data types instead.

Here’s an enum in Rust that combines different ways to represent a color:

enum Color {
   White,
   Black,
   RGB(i32, i32, i32),
   RGBA(i32, i32, i32, i32),
}

// example values
let white: Color = Color::White;
let black: Color = Color:RGB(0, 0, 0);

We can pattern-match on the different definitions in a consistent way, using the match statement:

// What does this return?
match color {
   Color::White         => false,
   Color::Black         => true,
   Color::RGB(0,0,0)    => true,
   Color::RGB(_,_,_)    => false,
   Color::RGBA(0,0,0,_) => true,
   Color::RGBA(_,_,_,_) => false,
}

Now here’s a cool feature.

What happens if we added a new way to declare colors? Say we added the color Yellow?

Our pattern-match would be no longer exhaustive - and the compiler would and will spit on error about it, which is insanely good. The compiler tells you exactly the places you need to alter your code to get the ‘Yellow’ in.

Here’s the same thing in Haskell:

data Color
   = White
   | Black
   | RGB Int32 Int32 Int32
   | RGBA Int32 Int32 Int32 Int32

let white = White
let black = RGB 0 0 0

case color of
   White        -> False
   Black        -> True
   RGB 0 0 0    -> True
   RGB _ _ _    -> False
   RGBA 0 0 0 _ -> True
   RGBA _ _ _ _ -> False

The takeaway is that good support for enumerations enables exhaustiveness checks in pattern-matches, and that’s a killer feature.

It prevents a whole class of bugs already at compile-time.

It helps maintanence and eases refactoring.

Enumerations in Go are a PITA

What about Go? The highly opinioned language decided to not include enumerations.

  • Create a Color struct with one field that “tags” the enumerations (at least they provide special identifier iota to ease some of the pain)
  • Escape the type system and resort to dynamic typing (a type switch)

Personally, I find this insane.

Yes, I know it is a concious decision to cater for the mass that’s called open-source community. The Go language is built from ground up to be used by huge teams at highly varying competence levels.

But, you know, designing for the mass is seldom a good strategy these days.

And sure, every extra feature ups the bar for newbies. But honestly, who would really rather see the following than either of the Rust or Haskell code above?

const (
    ColorWhite = iota
    ColorBlack
    ColorRGB
    ColorRGBA
)

type Color struct {
    tag int // This is (it better be!) one of the constants defined above
    red int32
    green int32
    blue int32
    alpha int32
}

func (c *Color) cfoobar() {
    switch c.tag {
        case ColorWhite: return;
        // tag is an int, so compiler won't do exhaustiveness checks
    }
}

(To be fair, there’s an external tool for Go that accomplishes static exhaustiveness checks here: https://github.com/haya14busa/gosum)

Wrapping up

Enumerations are cool, exhaustive pattern-matching is nice. And neither is a hard concept to teach or pick up on-the-go. In fact, I would like to think I just explained both in this one short blog post.

Well sure, these are concepts more math than computers, but, well, that’s just what modern programming is. We need to adopt new idioms and patterns to evolve.

comments powered by Disqus