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:
- LLVM 3.3 and Clang
- The latest version of the Boehm-Demers-Weiser conservative garbage collector
- libunwind
- libpcre
Then, depending on your platform, you need to download one of these:
- Mac OSX: crystal-darwin-0.1.0-p0.tar.gz
- Linux 32 bits: crystal-linux32-0.1.0-p0.tar.gz
- Linux 64 bits: crystal-linux64-0.1.0-p0.tar.gz
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 {{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. :-)
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!
Contribute