Skip to content
GitHub Repository Forum RSS-Newsfeed

Crystal 0.16.0 released!

Ary Borenzweig

Crystal 0.16.0 has been released!

This is a huge release that includes a major breaking change that was announced a few months ago: types of global, class and instance variables need to be a bit more explicit.

This release also includes other minor breaking changes and a lot of new goodies.

The new global type inference algorithm

The new rules are explained in the official docs, but let’s quickly review the change.

Before this release, the type of global, class and instance variables was inferred globally in the program by analyzing all uses. For example:

class Some
  def initialize(@var)
  end
end

Some.new(1)

In the above snippet, Some’s @var was inferred to be an Int32. If you did this:

Some.new(1)
Some.new("hello")

then it would have been inferred to be Int32 | String (a union type). And even in the following code, @var was inferred to be an Int32 | String:

class Some
  def initialize(@var)
  end

  def var=(value)
    @var = value
  end
end

some = Some.new(1)
some.var = "hello"

In this release, all of the above snippets won’t compile anymore: the compiler now needs to know the type of @var in a “obvious” way. For example, assuming the intended type for @var is Int32, then we could write:

class Some
  # Since only Int32 is accepted in the constructor, @var is inferred to be Int32
  def initialize(@var : Int32)
  end
end

Another common ways is using literals and constructors:

class Some
  def initialize
    @int = 0            # Inferred to be Int32
    @string = "hello"   # Inferred to be String
    @bools = [] of Bool # Inferred to be Array(Bool)
    @time = Time.new    # Inferred to be Time
  end
end

The reason of this change is to allow, in the future, implementing incremental compilation and improving overall compile times and memory usage. Right now there aren’t many big projects written in Crystal. Probably the biggest one is the compiler itself, and it takes 16 seconds to compile it from scratch, and 1GB of memory. But bigger projects will exist, and even though a programmer’s computer should be fast and have a lot of memory, that’s no reason to have her wait, or waste CPU and memory. Yes, there are popular programming languages that can sometimes reach huge compile times, but that’s no excuse for us to do the same.

For the old global type inference to work, the whole code had to be held in memory, as a big tangled web, because a change in the type of an instance variable could impact any other method. With this change, methods can be analyzed locally. And once they are, their type can be inferred and it can’t change anymore.

Note that types in method arguments are not mandatory, and will never be.

The good side of this change is that since the types of instance variables must now be obvious to the compiler, they will also be obvious for someone reading the code. The programmer, too, has to stop analyzing the whole code to figure out what an instance variable is supposed to be.

Our guess is that static type languages lovers will love this change, while more dynamic type languages lovers will probably hate it, some a bit, others with fury.

The good news is that even after this change explicit types are still not that many. As an example, these are some diffs that were needed in some projects to upgrade to the new version:


In general, few type annotations were needed. That sometimes depends on the programmer’s style: he might feel more comfortable with more explicit types, so this change affects him less. In other cases more annotations are needed, but understand that these projects have been around for a long time now, and adding 30 type annotations at once instead of writing them when declaring a class is definitely more annoying.

Another reason for why not many type annotations were needed is that many were already there, since the language was born:

class House
  def initialize
    @rooms = [] of Room # This is a type annotation
  end
end

Empty arrays and hashes always needed their type specified, and these are very common when initializing an object.

As can be seen above, many important Crystal shards have already been updated and will work with this release.

If you haven’t upgraded yet, the recommended approach is to ask the old compiler (0.15.0) these types, by doing crystal tool hierarchy your_program.cr and then adding the necessary type annotations that the new compiler (0.16.0) will ask. To have both versions side by side you can use the excellent crenv by pine.

The goodies

Putting the big breaking change aside, this release includes many goodies.

FreeBSD and musl libc support

Thanks to ysbaddaden (you might know him from shards) FreeBSD and musl libc support is included in this release.

His contribution will also make it easier to port Crystal to other platforms (but, before you ask it in the comments section, no, there’s still no Windows support, and this change probably doesn’t help much in that regard.)

EDIT: a FreeBSD package is now in the releases page.

Named arguments everywhere

Before this release, named arguments could only target arguments that had a default value:

def method(x, y = 1)
  x + y
end

method 10           # OK
method 10, y: 20    # OK
method x: 10        # Error
method y: 20, x: 10 # Error

Now, all of the above compile. This can be specially useful for methods that have a long list of arguments. For example, which one is more readable:

require "oauth2"

# Option 1
client = OAuth2::Client.new(
  "some_host",
  "some_client_secret",
  "some_client_id"
)

# Option 2
client = OAuth2::Client.new(
  host: "some_host",
  client_secret: "some_client_secret",
  client_id: "some_client_id"
)

Regardless of which option you find more readable, the first one is actually wrong: the method arguments are (host, client_id, client_secret), and they are being passed in a wrong order. But, because all of them are strings, the compiler doesn’t complain. The second option is more robust because we don’t need to remember the correct order and we use descriptive names.

More big numbers

BigFloat (thanks to Exilor ) and BigRational (thanks to will) were added to the standard library, and together with BigInt should be enough for math programs and other use cases.

Binary search methods were added in Array and Range (thanks to MakeNowJust).

For example, let’s solve x3 + x2 + x - 2:

answer = (-Float64::INFINITY..Float64::INFINITY).bsearch { |x| x ** 3 + x ** 2 + x - 2 >= 0 }
puts answer # => 0.810536

JSON and YAML improvements

Enums, BigInt and BigFloat can now be mapped to JSON and YAML very easily. For example:

require "json"
require "big"
require "big/json"

enum Color
  Red   = 1
  Green = 2
  Blue  = 3
end

class Lollipop
  JSON.mapping({
    color:    Color,
    diameter: BigFloat,
  })
end

json = %({"color": 2, "diameter": 12.3456789123456789})
lollipop = Lollipop.from_json(json)
p lollipop # => #<Lollipop:0x10c962f30 @color=Green, @diameter=12.3456789123456789>

json = %({"color": "Blue", "diameter": 12.3456789123456789})
lollipop = Lollipop.from_json(json)
p lollipop # => #<Lollipop:0x1033a4f00 @color=Blue, @diameter=12.3456789123456789>

Other goodies

Make sure to read the changelog for other minor goodies (and a few minor breaking changes as well.)

Thanks

We’d like to thank everyone that made this release possible, by testing the new changes and reporting bugs, upgrading code to the latest version, sending pull requests, commenting suggested features, adding docs and more.

Contribute