Ary Borenzweig 19 Jun 2014

Crystal 0.1.0 released!

Yesterday we released the first official version of Crystal: 0.1.0. Yay!

This first release brings some nice things:

  • A way to install Crystal from a tar.gz
  • A way to install Crystal on Mac OSX using Homebrew
  • A (currently almost empty) Changelog, which we'll keep updated from now on
  • A couple of language features which were already there, but since we reached a milestone it's a good opportunity to talk about them :-)

Let’s talk about each of these points.

Install Crystal from a tar.gz

First, you need to fulfil some dependencies:

Then, depending on your platform, you need to download one of these:

Then uncompress it and inside it you will have a bin/crystal executable. You can create a symbolic link to that executable, and that’s it!

Install Crystal on Mac OSX using Homebrew

You just need to execute this:

brew install crystal-lang

That’s it! You should have a crystal executable in your PATH.

The language features

The language features that we consider make Crystal attractive and efficient are:

  • Function literals, function pointers and closures
  • Macros
  • Tuples
  • Structs

Function literals, function pointers and closures

You can create an argument-less function literal like this:

f = ->{ 1 + 2 }
puts f.call #=> 3

The return type is automatically inferred.

If you want a function literal with arguments, you need to specify their types:

f = ->(x : Int32) { x + 1 }
puts f.call(2) #=> 3

A function literal has access to the environment where it was created. For example:

a = 1

f = ->(x : Int32) { x + a }
puts f.call(2) #=> 3

a = 10
puts f.call(2) #=> 12

That is, it can form a closure.

The compiler figures out which variables are accessed inside the function literal and only allocates memory for the closure if it’s needed, in order to get maximum memory and speed efficiency.

A closure has also access to the self of the class where it is declared:

class Foo
  property x

  def initialize(@x)
  end

  def x_proxy
    ->{ @x }
  end
end

foo = Foo.new(1)
proxy = foo.x_proxy
puts proxy.call #=> 1

foo.x = 10
puts proxy.call #=> 10

A function literal can be passed as a block to a method using an ampersand (&):

def foo
  yield 1
end

f = ->(x : Int32) { x + 2 }
value = foo &f
puts value #=> 3

You can also get a function pointer to an existing method. In this case, you must specify the types of the arguments:

def foo(x)
  x + 1
end

f = ->foo(Int32)
puts f.call(2) #=> 3

You can also get a function pointer to a class’ instance method:

class Foo
  def initialize(@x)
  end

  def bar(z)
    @x + z
  end
end

foo = Foo.new(1)
f = ->foo.bar(Int32)
puts f.call(2)

Finally, a block can be captured and automatically converted to a function literal, but for this you must specify the arguments’ types in the block argument:

def foo(&block : Int32 -> Int32)
  block
end

a = 1
f = foo { |x| x + a }
puts f.call(2) #=> 3

Note that in this last form a closure is automatically created if needed: in the last example a was captured by the block/function literal.

This last form makes it easier to write function literals without having to specify their arguments types everywhere: you only need to specify these in a def’s block argument.

You can use a free variable as a block’s return type to make the compiler infer the return type of the block:

# U is a free variable
def foo(&block : Int32 -> U)
  block
end

f = foo { |x| x + 1 }
puts f.call(2) #=> 3

g = foo { |x| x.to_s }
p g.call(2) #=> "2"

# Or also:

h = foo &.to_s
p h.call(2) #=> "2"

(A small note on free variables: right now the compiler figures out that U is a free variable because there’s no type named U. In a future version we’ll fix this by letting the compiler know that U is free right in the method’s signature, like it is done in other languages like Java, C#, Rust, D, etc.)

If you don’t care about the return value of a passed block, you can ommit the block’s return type:

def foo(&block : Int32 -> )
  block
end

f = foo { |x| x + 1 }
f.call(2) #=> (void)

This last form allows for a very nice and comfortable way to specify callbacks:

class Foo
  def initialize
    @callbacks = [] of ->
  end

  def after_save(&block)
    @callbacks << block
  end

  def save
    @callbacks.each &.call
  end
end

foo = Foo.new
foo.after_save { puts 1 }
foo.after_save { puts 2 }
foo.save # prints 1 and 2

We believe all of these features makes the language very flexible and comfortable to use, just like in Ruby.

An implementation note: previously a closure was implemented as a void pointer using LLVM’s trampoline intrinsics. The problem with this approach is that programs had to be compiled with a flag allowing stack/heap execution, which is kind of unsafe. Now both function literals and closures are implemented as a pair of void pointers: one for the function literal definition and one for the closure environment (or nil, if none). This has the slight disadvantage that closures can’t be passed to C. On the other hand, the allow stack/heap execution flag is not needed anymore and C functions almost always have a void pointer for custom data, so you would never need to pass a closure to it anyway.

Another implementation note: when you use yield, that is, when you don’t capture a block, the compiler always inlines the method. In this way the following code:

a = 0
10.times do |i|
  a += i
end
puts a #=> 45

is exactly the same as the following code, only nicer:

a = 0
i = 0
while i < 10
  a += i
  i += 1
end
puts a #=> 45

That way Crystal has zero-cost iteration abstraction, while still being able to do stuff like this:

f = ->(x : Int32) { puts x }
10.times &f

Macros

Macros allow generating code at compile time. For example, the getter macro is simply this:

macro getter(name)
  def {{name}}
    @{{name}}
  end
end

class Foo
  getter foo

  # the above is the same as writing:
  #
  #     def foo
  #       @foo
  #     end
end

Inside a macro you can use &#123;{name}} to interpolate name.

You can also use if and for inside a macro:

macro generate_methods(names)
  {% for name, index in names %}
    {% if index != 1 %}
      def {{name}}
        puts 
      end
    {% end %}
  {% end %}
end

generate_methods [foo, bar, baz]
foo #=> 0
baz #=> 2
bar # (compile-time error, `bar` undefined)

These new macros are relatively new and experimental, so we won’t go over all of the details right now. But our idea is to have macros that allow you to generate code as painlessly and effortlessly as possible, which remaining powerful. For example, some methods are supported inside a macro:

macro generate_method_downcase(name)
  def {{name.downcase}}
    puts "Hello!"
  end
end

generate_method_downcase "HELLO"
hello

Most String and Array methods will be available inside macros, because these are very convenient for code generation.

Another cool thing is that you can execute system commands:

macro compile_time_date
  {{ system("date").stringify }}
end

build_date = compile_time_date
puts build_date

The stringify is needed in order to convert the date output to a string literal, otherwise the generated code would be an invalid program (Thu Jun 19 14:15:57 ART 2014 is an invalid program, but "Thu Jun 19 14:15:57 ART 2014" is not.)

Our idea is to extend this to allow execution of other Crystal programs that would receive AST nodes as argument and will generate code that would be embedded in the program currently being compiled. In this way you could generate a class definition from a database schema, or a file in the disk, or convert an ERB/HAML template into efficient Crystal code. We still have to materialize all of these ideas.

Macros also allow compile-time introspection. For example, you can get the instance variables names of a class:

class Foo
  def initialize(@x, @y)
  end

  def instance_vars : Array(String)
    a = [] of String
    {% for ivar in @instance_vars %}
      a.push {{ivar.stringify}}
    {% end %}
    a
  end
end

foo = Foo.new(1, 2)
puts foo.instance_vars #=> ["@x", "@y"]

In this last form, the instance_vars method is expanded after the type inference phase finishes. Because this is done at that stage, any invocation of Foo#instance_vars must know what type it returns, so that’s why we need to specify the type (Array(String) in this case.) Again, all of this is very experimental so we won’t explain all of the details here.

Tuples

Tuples are a fixed-size list where each element can have a different type:

tuple = {1, "hello"}
puts tuple[0] #=> 1
puts tuple[1] #=> hello

Tuples can be decomposed on assignment:

tuple = {1, "hello"}
int, string = tuple
puts int
puts string

This allows returning multiple values from a method dead easy:

def name_and_age
  {"Crystal", 1.8}
end

name, age = name_and_age
puts name.size #=> 7
puts age.to_i    #=> 1

Note that returning an Array won’t work, because an Array mixes the types of everything you put in it:

def name_and_age
  ["Crystal", 1.8]
end

name, age = name_and_age
puts name.size #=> undefined method 'size' for Float64
puts age.to_i

Tuples are objects, like everything else in the language, so they have methods. They also include the Enumerable module:

nums = {1, 2, 3.5}
puts nums.size #=> 3
nums.each do |value|
  puts value
end

In the future, tuples will allow us to implement variable length arguments for methods.

Structs

A struct is similar to a class, except that it is passed by value and it doesn’t have an object_id. It also inherits from Struct, which inherits from Value, while a class inherits from Reference.

Structs are very useful for wrapping other values without indirection costs. And they are specially useful for implementing immutable data structures.

For example, we used structs in a raytracer sample to represent 3D vectors. The code runs blazingly fast. With classes, not that fast.

Structs can inherit other structs, they can be generic and they can include modules. That means that most of the time you can just change a class to a struct by just changing the word class to struct and get a more efficient program if it turns out to be more efficient in that particular program.

Roadmap

Although we could still add more features to the language in a blind way, we prefer to start writing a real program (other than the compiler :-P). Our first task will be writing a web server similar to Sinatra. In fact, we already started it: it’s called Frank. While developing it we’ll be fixing language bugs and enhancing it. We really like dog food. :-)

Thanks

We’d like to thank everyone who contributed by sending pull requests, submitting issues, discussing ideas and mentioning Crystal in social networks. Big thanks to you!