[001.3] Language basics II

Learn about classes, modules, generics, and macros.

Subscribe now

Language basics II [08.22.2017]

3. Language Basics

    13. Classes & Modules

    14. Generics

    15. Macros

Welcome again. After going through the basic data types in the previous episode, this time we’ll start working with classes and modules.

Of course, being an object-oriented language, Crystal allows you to define classes through the class keyword.

class Greeter

  @name : String



  def initialize(name)

    @name = name

  end

  def salute

    puts "Hello #{@name}!"

  end

end

Instance members are prefixed with an @, and the constructor is defined as a special initialize method. We also have a shorthand syntax for directly assigning an argument to an instance variable in the constructor.

  def initialize(@name)

  end

Also, note that Crystal’s compiler requires us to specify the types of all members of a class. If we remove the type declaration, we’ll get an error.

Nevertheless, the compiler is smart enough to infer the type of the instance variable if we declare its type in the constructor argument.

  def initialize(@name : String)

  end

After declaring our class, we can initialize instances of it by calling new, much like in Ruby.

g = Greeter.new("world")

And, of course, we can invoke methods on it.

g.salute

Instance variables cannot be accessed outside the class by default, so let’s create a getter method for that.

  def name

    @name

  end

[Outside class]

g.name

Now, if we add another instance variable, and want to define a getter, the resulting code becomes pretty similar to the previous one:

[Inside class]

  @other = "foo"

  def other

    @other

  end

In a dynamic language, we could use metaprogramming for generating these getters on the fly, but we are in a compiled language. Crystal’s answer to this problem is macros, which are evaluated in compile time.

Macros expressions in Crystal use the following syntax. Note that, inside a macro, we can use only a reduced subset of expressions, such as conditionals and simple iterations. For instance, we can iterate over an array and declare the getters.

  {% for var in ["name", "other"] %}

    def {{ var.id }}

      @{{ var.id }}

    end

  {% end %}

Here, we use double braces for outputting a macro variable, and use the id method to output the variable as text, and not as a quoted string.

Alternatively, we could declare a macro named getter that can be invoked once for each field.

[Outside class]

macro getter(var)

  def {{ var.id }}

    @{{ var.id }}

  end

end

[Inside class]

getter name

getter other

Actually, since getter is such a common use case, it is included in Crystal’s standard library, as well as setter and property, which generates both a getter and setter.

[Remove macro getter]

property name

property other

[Outside class]

g.other = "bar"

g.other

Ok, let’s move on to the next thing. Crystal, like Ruby, has support for modules. Modules have a set of methods, and can be included in a class as a means to insert shared functionality.

Let’s change our Greeter class into a reusable module, which adds the greet method. We will assume there is a name method where we are including the module, that provides the name of the greetee.

module Greeter

  def salute

    puts "Hello #{name}!"

  end

end

Let’s create an Assistant class, with the greeting functionality.

class Assistant

  include Greeter

end

Assistant.new.salute

Note that Crystal, in compile time, will detect that the name method is not defined by the Assistant class. We can actually enforce that method to be present by declaring it as abstract in the module, so we get a clearer error message if we forget to implement it.

  abstract def name

Now let’s define name in Assistant. It’s a good time to point out that, in Crystal, you can reopen a class to add methods to it.

class Assistant

  def name

    "world"

  end

end

[Open output console from playground]

Modules can also be used as a namespace to provide standalone methods, which are declared by prepending self before the method name. Know that class methods can be defined using the same syntax.

[Clear code]

module Greeter

  def self.salute(name)

    puts "Hello #{name}!"

  end

end

Greeter.salute("world")

To close this chapter, we’ll point out that Crystal also supports class inheritance, using a similar syntax to Ruby’s, including the concepts of public, protected and private visibility. It also includes the concept of abstract classes with abstract methods that must be defined by children, exactly like the abstract methods in modules we’ve just seen.

abstract class Animal

  protected abstract def sound

  def talk

    puts sound

  end

end

class Dog < Animal

  protected def sound

    "woof!"

  end

End

Dog.new.talk

Crystal also has support for generics, which you will find across the standard library container classes such as Array and Hash, or you can easily define yourself.

class MyBox(T)

  getter content

  def initialize(@content : T)

  end

end

box = MyBox(String).new("foo")

box.content

Between this episode and the previous one we have covered most of the language basics. However, we strongly recommend that you take a moment and head on to the Crystal docs to review any concept you’d like to understand in more detail, or grasp others that we have glossed over such as enums or closures.

[Open https://crystal-lang.org/docs]

In the next episodes we’ll be building some simple projects showcasing database access and setting up a simple HTTP server. Happy Crystalling!