About this ‘A Language a Day’ Advent Calendar
This article became a part of my book ‘A Language a Day’, which you can get in both electronic and paper format. Languages covered in the book: C++, Clojure, Crystal, D, Dart, Elixir, Factor, Go, Hack, Hy, Io, Julia, Kotlin, Lua, Mercury, Nim, OCaml, Raku, Rust, Scala, and TypeScript.
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