Crystal at a Glance — A Language a Day, Advent Calendar 2019 Day 6/24

About this ‘A Language a Day’ Advent Calendar 2019

Welcome to Day 6 of this year’s A Language a Day Advent Calendar. Today’s topic is introduction to the Crystal programming language.

Facts about the language

Some facts about Crystal

  • Based on Ruby
  • Static type checking
  • Everything is an object
  • Compiled language (but also allows script execution)
  • Appeared in 2014
  • Website: crystal-lang.org

Installing and running Crystal

The website gives a lot of instructions of how to install the compiler on different platforms. You can also play with it online. On Mac, run the following command:

$ brew install crystal

Having the compiler installed, you can run your program:

$ crystal run helloworld.cr

Actually, it also works:

$ crystal helloworld.cr

Alternatively, you can build an executable file:

$ crystal build helloworld.cr
$ ./helloworld

Hello, World!

The simplest program that outputs a message is here:

puts "Hello, World!"

If you prefer using classes straight on, then you can also put your main code to a class (or even more than a single class):

class HelloWorld
    puts "Hello, World!"
end

class HolaMundo
    puts "¡Hola, Mundo!"
end

This time, you get two messages in console.

An interesting fact is that the language supports non-standard quotation for strings: it stars with the % symbol followed by a string quoted in a pair of balanced symbols:

puts %[Hello, World!]
puts %|Hi there!|

Variables

To define a variable, just assign a value to it. There is no need to use any keywords as you can see in the next sample:

name = "John"
puts "Hello, " + name + "!"

The compiler deduces the type of the content, and checks if the variable is used properly. You can force the type by specifying it explicitly:

name: String = "John"
puts "Hello, " + name + "!"

name = "Karl" # OK
name = 30     # Error: type must be String, not (Int32 | String)

Functions

Functions (actually, methods of a program) are defined between the def and the end keywords. The indentation of the function body is not a requirement from the compiler.

name = "John"

def greet_person(who : String)
    puts "Hi, " + who + "!"
end

greet_person(name)

In this example, the function argument, who, is type restricted. You can omit that if you don’t need it. But if you have one, make sure to have spaces on both sides of the colon.

When calling a method, parentheses are optional:

greet_person(name) # OK
greet_person name  # Also OK

Also notice that you cannot directly use the global variable name inside the method’s body.

A Factorial example

Let us implement it without recursion to see how we can use ranges in Crystal.

def factorial(n : Int): Int
    result = 1
    (2..n).each do |x|
        result *= x
    end
    result 
end

puts factorial 4 # 24
puts factorial 5 # 120
puts factorial 6 # 720

A range like 2..n covers all integer numbers between 2 and n including both 2 and n. There is also a three-dot variant 2...n which excludes n.

The |x| constructs names the topic variable as it is done in Ruby. The return keyword is optional at the end of the function if you have a calculated expression there.

Proc

A proc in Crystal is a function pointer and can be defined in the following way:

add = -> (x : Int32, y: Int32) { x + x }

This is an anonymous function, which you can use via the call method:

puts add.call(3, 4)
puts add.call(5, 6)

Classes

Class creation is more or less straightforward in Crystal. Use the class keyword. To access instance variables (data members belonging to the objects), use the @ prefix, as shown below.

class Person
    def initialize(name : String, age : Int32)
        @name = name
        @age = age
    end

    def info
        puts "#{@name}'s age is #{@age}."
    end
end

p = Person.new("John", 30)
p.info

(Notice how you do variable interpolation in strings; "#{@n}".)

To create an instance of a class, use the new method. Notice that if you want to initialise an object, define the initizlize method (a constructor), which will be automatically called when you call new in the program. The arguments of new will be passed to initialize.

In the example above, we do nothing fancy when initialising the object, so you can directly use @-names in the constructor to simplify the code:

def initialize(@name : String, @age : Int32)
end

Getters and setters

While you can read from p.@name outside of the class, you cannot write to it. Use getters and setters instead. You can create your own method, say:

def get_name
    @name
end

But you’d better declare explicit getters and setters:

class Person
    setter name : String
    getter name : String

    def initialize(@name : String, @age : Int32)
    end
end

p = Person.new("John", 30)
p.name = "New name"
puts p.name

Class variables

Class-level variables (those that are shared between instances of that class) are marked with the @@ prefix. You can grasp the idea of their usage in the following example of a class that counts created objects.

class Counter
    @@count : Int32 = 0
    @count  : Int32

    def initialize
        @@count += 1
        @count = @@count
    end

    def info
        "Counter #{@count} of #{@@count}"
    end
end

x = Counter.new
y = Counter.new

puts x.info # Counter 1 of 2
puts y.info # Counter 2 of 2

Inheritance

Inheritance can be expressed with the < symbol. Children classes can redefine methods of the base class. Notice that unlike other languages you can redefine the method in the same class if, for some reason, you define it more than once. The most recent version wins in this case:

class X
    def my_method
        puts "Method 1"
    end
end

class X
    def my_method
        puts "Method 2"
    end
end

x = X.new
x.my_method # Method 2

A Polymorphic example

The following example uses an abstract class, which has two children in the hierarchy. To make a class abstract, use the keyword abstract.

abstract class Animal
end

class Dog < Animal
    def info: String
        "Dog"
    end
end

class Cat < Animal
    def info: String
        "Cat"
    end
end

zoo = [Dog.new, Cat.new]
zoo.each do |x|
    # puts typeof(x)
    puts x.info
end

The program prints the messages defined in the children classes. Notice that if you will not make the base class abstract, the program will not work, and even cannot be compiled.

Uncomment the line with typeof(x), and you can see that the type of the x item is always Animal. While the Animal class is abstract, the program knows how to reach the info method of the Dog and Cat classes. (It is said in the documentation that the type of x should be Animal+ in this case.)

Concurrent computation

Concurrent computation is implementing via the so-called fibers, which are a lightweight form of a thread that do not require heavy context switching, and is managed by Crystal itself, not by an operating system.

Sleep Sort

In our Sleep sort implementation, we’ll use fibers and channels. To create a new fiber, use the spawn keyword. To wait for the fiber to finish, we can use a delay that is longer than the maximum delay in the algorithm, but it’s better to use channels to avoid this problem. Reading from a channel waits until some data appear there. To know when to stop, we just count the number of readings (which is the same as the size of the data array). Here is the complete program:

collector = Channel(Int32).new

data = [10, 4, 2, 6, 2, 7, 1, 3] of Int32

data.each do |n|
    spawn do
        sleep n.seconds
        collector.send(n)
    end
end

data.each do
    puts collector.receive
end

By the way, notice how the type of array elements is annotated: of Int32.

Get more

Let me stop this brief overview of the Crystal language here. It does resemble Ruby but has its own look and feel. Enjoy the language, and you can continue with the following resources:

The program sources for this article are located on GitHub.

Next: Day 7. Scala

Leave a Reply

Your email address will not be published. Required fields are marked *