A Rubyist looks at Crystal (Part 2)
As promised in my previous post this article will cover some of the more advanced features of Crystal, namely macros, C bindings and concurrency.
This is a cross-post from my blog, the original post can be found here.
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 id call 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!