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 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
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.
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
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.
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
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
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
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
1: "pants" means bad.