This post is over 6 months old. Some details, especially technical, may have changed.

alias, union and case in Crystal

So I've been diving into Crystal lately. It's nice. This isn't a post about why I think it's nice, it's a post about a few of the features of Crystal so let’s get straight down to it.

alias

alias is a very common feature that we can see across many languages but it can be used to build upon other features in Crystal. As the name suggests you use alias to give a type a different name.

alias TableOfContents = Hash(String, Int32)

In the example above we give the type Hash(String, Int32) a custom alias of TableOfContents which allows us to refer to this type as TableOfContents

def lookup(chapter : String, tableOfContents : TableOfContents) : Int32
  tableOfContents.fetch(chapter)
end

So instead of using Hash(String, String) in the method signature above we can use TableOfContents. So why is this useful? Well for one thing it allows us to reduce complex type names into smaller names. The example above isn't exactly verbose but longer types can be more readable.

Now of course size isn't everything so another advantage of alias is that we can add meaning to types within our problem domain. You're less likely to wonder what this hash of string to string is or should contain when we give a name like TableOfContents. In fact, we could enrich the signature above more by giving specific types an alias.

alias ChapterTitle = String
alias PageNumber = Int32
alias TableOfContents = Hash(ChapterTitle, PageNumber)

From this we can infer that for a given chapter title the table of contents is capable of finding the page number that the chapter starts on.

union

Unlike many languages the return type of methods can actually be more than one type. Crystal uses type inference so declaring types is not mandatory. Take a look at this,

def doAThing(isAThing : Bool)
  if isAThing
    "I did a thing"
  else
    42
  end
end

What type is this? It could be an Int32 it could be a String. In other statically typed languages the type most likely resolves to some common base class like Object or Any which makes sense but is actually a tiny bit pants1.

In Crystal this type will be inferred as String|Int32. This is a union and is essentially a type of its own that could be either a String or a Int32. We can explicitly give this method a return type which means the compiler can verify our assumption

def doAThing(isAThing : Bool) : String|Bool

You may have noticed I just wrote the wrong return type and attempting to compile the code results in a helpful compilation error message

Error in ./src/something.cr:14: instantiating 'doAThing(Bool)'

doAThing(false)
^

in ./src/something.cr:6: type must be (String | Bool), not (String | Int32)

def doAThing(t : Bool) : String|Bool
    ^

Union types give us the rigidity and predictability of types while, at the same time, allowing a certain amount of flexibility as if we were working in a purely dynamic language.

case

So we've got these nice union types how do we deal with them in code? Well similar to other languages Crystal has methods like is_a?(type_name) and responds_to?(method_name) that allows us to inspect the current type of the variable but it also supports a kind of pattern matching for types using case.

def readFile(filename : String) : String|Error
  # read file return a string of its contents
rescue an_error
  an_error
end

maybeContents = readFile("somefile.txt")

case maybeContents
when String
  puts maybeContents
when Error
  puts "We had a wee error doing that"
end

In each when of the case the type of maybeContents will be the specific type that was matched against. So maybeContents will start as a String|Error but be available as a String in the first when and an Error in the second when.


1: "pants" means bad.

Published in Crystal on January 14, 2016