A Rubyist looks at Crystal (Part 2)

- ruby crystal

As promised in my previous post this article will cover some of the more advanced features of Crystal, namely macros, C bindings and concurrency.

Macros

When coming to Crystal from Ruby, one of the biggest changes is the lack of runtime introspection that enables much of Ruby’s metaprogramming techniques. However, this can be rectified to a certain degree by using macros, which are methods that receive AST nodes at compile time which they use to write new code.

That was quite a mouthful, so let’s look at an example: In Crystal, access modifiers like private need to be part of the method definition. Let’s simplify this by introducing the macro defp (a name I borrowed from Elixir), which provides a shorter syntax for defining private methods.

macro defp(name, &block)
  private def {{name.id}}
    {{block.body}}
  end
end

This macro receives a name and a block, which it uses to define a method with the appropriate name and the block’s content as method body. Note that the idcall in the interpolation ensures that we can pass in name as either a symbol, a string, or a bareword. Let’s see it in action:

class Test
  def public
    priv
  end

  defp priv do
    "private method"
  end
end

t = Test.new
t.priv
#=> private method 'priv' called for Test
t.public #=> "private method" : String

Here we use our macro to define a private method called priv, which we then try to call. As expected this will not compile and we see the usual error message. Calling our private method through a public method of course succeeds, so the method defined via a macro behaves exactly the same way as a method defined in the regular way would. This is just one example of how easy it is to extend Crystal’s syntax via macros.

Macros offer a lot more though, like conditionals, iteration, or splat arguments. Let’s see some of them in action in the following example:

macro define_abstract(klass, *names)
  abstract class {{klass.id}}
    {% for name, _index in names %}
      abstract def {{name.id}}
    {% end %}
  end
end

define_abstract Abstract, :one, :two

class Concrete < Abstract
  def one
    1
  end
end
# abstract `def Abstract#two()` must be implemented by Concrete

define_abstract is a macro which receives a class name (as constant, string, or symbol) and a variadic list of abstract method names, and then uses this information to generate an abstract class. For test purposes we then inherit from our newly defined class, but since we forgot to implement the second method, we will get a compiler error reminding us that we still have to provide implementation for Abstract#two in our Concrete class.

Macro hooks

Crystal offers some special macros which act as hooks during compile time: inherited (a subclass is defined), included (a module is included), extended (a module is extended) and method_missing (a non-existant method is called). Let’s have a look at the latter, which should be very familiar to Rubyists:

class Greeter
  def greet(name)
    "Hello #{name}!"
  end

  macro method_missing(call)
    greet({{call.name.id.stringify.capitalize}})
  end
end

g = Greeter.new
g.readers #=> "Hello Readers!" : String

Here the method_missing macro ensures that whenever we call an undefined method on instances of Greeter, we’ll transform the method name and pass it as an argument to the greet method. What a friendly class! 🤗

C extensions

Another nice fetaure of Crystal is how easy the language makes it to interface with C libraries. Let’s look at an example first:

lib LibMath
  fun nearbyint(x: Float64): Float64
  fun pow(x: Float64, y: Float64): Float64
end

LibMath.nearbyint(3.534) #=> 4.0: Float64
LibMath.pow(2, 10) #=> 1024.0: Float64

We declare a wrapper for a C library with lib, which allows us to declare the functions and types we are interested in. This example wraps two functions from macOS’ math library (see man 3 math for details), which is implicitly linked, so we don’t need to provide the compiler with any linking information.

We then use fun to specify the functions we are interested in, as well as their argument and return types (in terms of Crystal types). Et voilà, after we defined the bindings for nearbyint and pow we can call them like class methods on LibMath. This really couldn’t have been any easier!

Now let’s look at how to work with a library that’s not automatically linked.

@[Link("ncurses")]
lib LibNcurses
  fun initscr
  fun beep: Int32
end

LibNcurses.initscr
LibNcurses.beep

Here we use the attribute Link to specify that we want to link against ncurses (see man 3 ncurses), which will pass -lncurses to the linker. If needed we can also specify ldflags and a framework (the latter only on macOS), but we don’t need to do this for our simple example. We then proceed to define wrappers for the two functions initscr and beep, which will use the terminal’s bell to alert the user. Of course this would have been easier by just doing a puts "\a" from inside Crystal, but it’s a nice example of how easy the language makes it to interface with any C library.

Concurrency

Last but not least we’ll have a look at concurrency in Crystal. While a Crystal program usually runs inside a single operating system thread (except for the garbage collector), it achieves concurrency via fibers, which are lightweight processes managed by the main program, not the operating system (this is called cooperative multitasking, which allows for low overhead context switching).

Fibers start with a small stack of only 4k, which can grow up to 8MB, the typical size of a thread. This means that on modern 64 bit machines, we can spawn millions of fibers without running out of memroy. If you have previous experience with Go’s goroutines or Erlang’s processes, this should feel familiar.

Enough theory, let’s look at an example:

require "http/client"

chan = Channel(Hash(String, Int32)).new

sites = %w(
	https://crystal-lang.org
	https://twitter.com/crystallanguage
	https://salt.bountysource.com/teams/crystal-lang
)

sites.each do |site|
  spawn do
    response = HTTP::Client.head(site)
    chan.send({ site => response.status_code })
  end
end

(1..sites.size).map { chan.receive }
#=> [{"https://twitter.com/crystallanguage" => 200},
#    {"https://crystal-lang.org" => 200},
#    {"https://salt.bountysource.com/teams/crystal-lang" => 200}]

After requiring the HTTP client library, we define a channel which will be used for communcations between our fibers and the main thread of execution. This channel expects messages which are hashes with string keys and integer values.

We then iterate over our sites array, which contains the URLs of various Crystal related web sites. For each of the URLs, we spawn a new execution thread (a fiber), which will make a HEAD request to the website, and then put an appropriate message on the channel (the site’s URL as string key and the requests’ response code as the integer value).

We then receive all the messages that were put on the channel. As we can see from the provided output (which may vary every time this program gets executed), the results are not in the same order as the sites array, since the fibers executed in parallel and finished at different times.

Conclusion

This second post concludes my short introduction to Crystal for Rubyists. This time I focussed on showing off some of Crystal’s more powerful features, like its macro system, the straight-forward approach to integrating with C libraries and the Go-inspired approach to concurrency. Stay tuned for more Crystal related posts in the future!