Posts

About

Crystal vs Ruby - Part 2 (Typing, Classes, and more)

September 23, 2015

Introduction

In part 1 I introduced the Crystal programming, compared its compilation process to Ruby’s, and showed off its speed. This post will compare how typing works in Ruby vs Crystal and explore some unique aspects of Crystal.

Typing

Ruby

Ruby uses dynamic typing. This means that type errors will not be caught until runtime.

Crystal

Crystal uses inferred typing at compile time. This means that for most objects you do not need to specify a type. Inferring the typing means before compile time means that type errors in Crystal are caught before runtime. It also allows Crystal’s syntax to stay close to Ruby’s. In some cases types cannot be inferred and must be provided such as for an empty array or empty hash.

x = 1               # x is an Int32

x = if false        # x is an Int32 | String
        1
    else
        "1"
    end
[]                          #=> Syntax error: for empty arrays use '[] of ElementType'
[] of Int32                 #=> Empty array that can hold Int32s
[] of (Int32 | String)      #=> Empty array that can hold both In32s or Strings

{}                                  #=> Syntax error: for empty hashes use '{} of KeyType => ValueType'
{} of String => Int32               #=> Empty hash with String keys and Int32 values
{} of String|Symbol => Int32|Bool   #=> Empty hash with String or Symbol keys and Int32 or Bool values

Classes

Crystal has the concept of abstract classes built into the language unlike Ruby. Crystal’s use of abstract classes is very similar to that of Java’s.

abstract class Fish
    abstract def name

    def type
        "Fish"
    end
end

class StingRay < Fish
    def name
        "sting ray"
    end
end

x = StingRay.new
puts x.name             #=> "sting ray"
puts x.type             #=> "Fish"
Fish.new.type           # can't instantiate abstract class Fish:Class

In Ruby you are probably familar with getters and setters for a class. You can use attrreader for getters, attwriter for setters, and attr_accessor for both. Crystal uses getter, setter, property instead. These are implemented using macros which will be discusses later on.

class Item
    getter id
    property name
    setter size

    def initialize(id, name, size)
    @id = id
    @name = name
    @size = size
    end
end

x = Item.new(1, "name", 2)
x.id
x.name
x.name = "test"
x.size = 1
x.id = 2        # Error
x.size          # Error

There are also a few other special macros that are similar to these. For example getter!(*name) will create a getter method that will raise an exception if the instance variable is nil. For a complete list of these macros see this API doc.

Other interesting concepts

Macros

Macros are used for generating code at compile time. They are analogous to metaprogramming in Ruby. The following is an example on how the getter macro works for classes:

macro getter(name)
    def {{name}}
    @{{name}}
    end
end

class Foo
    getter foo

    # the above is the same as writing:
    #
    #     def foo
    #       @foo
    #     end
end

Tuples

Tuples are a fixed-size list where each element can have a different type. These are useful since arrays handle typing differently. Each element’s types in a array of different types is the union of all the types. So for example, each element of ["1", 1] is either a String or Int32. For tuples each element has its own type, in {"1", 1} the first element is a String and the second is a Int32. This allows you call methods on elements that only apply to that elements type and not others.

s, x = ["1", 1]
puts s.size      #=> undefined method 'size' for Int32

s, x = {"1", 1}
puts s.size      #=> 1

to_proc

In ruby you may be familar with blocks and some shortcuts using to_proc.

[1, 20, 300].map { |n| n.to_s }     #=> ["1", "20", "300"]
[1, 20, 300].map &:to_s             #=> ["1", "20", "300"]

These shortcuts work great in Ruby until your proc needs to take a variable. Also what if you wanted to chain procs together? In Crystal both of these can easily be accomplished.

[1, 20, 300].map &.to_s         #=> ["1", "20", "300"]
[1, 20, 300].map &.modulo(3)    #=> [1, 2, 0]
[1, 20, 300].map &.to_s.size    #=> [1, 2, 2]

Resources


Written by Jacob Oakes
I am a software architect who enjoys learning new things, clean code, and automated tests.