Skip to content
GitHub Repository Forum RSS-Newsfeed

Crystal 0.18.0 released!

Ary Borenzweig

Crystal 0.18.0 has been released!

This is a huge release with many new language and standard library features, and a few breaking changes as well.

Let’s review them.

Union as a first class citizen

Union types exist in the language since day one. However, they were types without a name. Sure, you could write Int32 | String in type expressions, but you couldn’t write Union(Int32, String) the same way you can write Tuple(Int32, String) to denote a tuple of two types.

Now you can. And you can also add class methods to the Union type. This seemingly small change adds a lot of power to the language.

For example, we can define a method to parse a JSON string into a Union. To do this we try to parse the string for each type in the union, as can be seen here (the code could be just these 9 lines, but it’s more complex because it has fast paths for primitive types)

With that, we can now write:

require "json"

array = Array(Int32 | String).from_json(%([1, "hello", 2]))
array # => [1, "hello", 2]

We can also use unions in mappings, even unions of complex objects:

require "json"

struct Point
  JSON.mapping x: Int32, y: Int32
end

struct Circle
  JSON.mapping center: Int32, radius: Int32
end

class Result
  JSON.mapping shape: Point | Circle
end

result = Result.from_json(%({"shape": {"x": 1, "y": 2}}))
result # => Result(@shape=Point(@x=1, @y=2))

result = Result.from_json(%({"shape": {"radius": 1, "center": 2}}))
result # => Result(@shape=Circle(@center=2, @radius=1))

shapes = Array(Point | Circle).from_json(%([{"x": 1, "y": 2},
  {"radius": 1, "center": 2}]))
shapes # => [Point(@x=1, @y=2), Circle(@center=2, @radius=1)]

In short, more expressive power and type safety.

Hash, Enumerable and block auto-unpacking

We won’t deny it, Crystal has a lot of inspiration in Ruby, be it in some of its syntax and a huge part of its standard library.

In Ruby there’s the Enumerable module. You just need to define an each method that yields some values, include Enumerable, and you get a lot of collection methods like map and select. For example:

class Foo
  include Enumerable

  def each
    yield 1
    yield 2
    yield 3
  end
end

foo = Foo.new
foo.map { |x| x + 1 }      # => [2, 3, 4]
foo.select { |x| x.even? } # => [2]

Ruby’s Hash, a mapping from keys to values, is also Enumerable. But there’s something a bit magical happening in Hash. Take a look:

hash = {1 => "a", 2 => "b"}
hash.each do |key, value|
  # Prints "1: a", then "2: b"
  puts "#{key}: #{value}"
end

hash.map { |key, value| "#{key}: #{value}" } # => ["1: a", "2: b"]

So, we can iterate a Hash and get its keys and values, and we can also use map on it, and transform the keys and values. But how does it work?

One would think that Hash implements each like this:

class Hash
  def each
    # for each key and value
        yield key, value
    # end
  end
end

Then maybe Enumerable’s map is implemented like this:

module Enumerable
  def map
    array = []
    # We need a splat because Hash yields multiple values
    each do |*elem|
      array.push(yield *elem)
    end
    array
  end
end

However, that doesn’t seem to be the case, because if we define our own map method that doesn’t use a splat, it works as expected:

module Enumerable
  def map2
    array = []
    # We don't use a splat
    each do |elem|
      array.push(yield elem)
    end
    array
  end
end

hash = {1 => "a", 2 => "b"}
hash.map2 { |key, value| "#{key}: #{value}" } # => ["1: a", "2: b"]

What’s going on?

The answer is that if a method yields an array, and the block specifies more than one argument, the array is unpacked. For example:

def foo
  yield [1, 2]
end

foo do |x, y|
  x # => 1
  y # => 2
end

So Hash is actually yielding a two-element array, not two elements, and when using each, map and select, if we specify more than one block argument, Ruby unpacks it for us.

Ruby’s solution is very convenient and powerful: it lets us iterate a hash as if it were a sequence of keys and values, without us having to care if it’s internally implemented as such; and when we want to add methods to Enumerable we don’t need to use splats to “get it right”, we can just treat each yielded element as a single object.

In Crystal we decided to do the same, although for tuples, because their size is known at compile-time. This means that the first Hash snippet now works exactly the same as in Ruby, and Enumerable’s code remained the same, and extensions to it will continue to work well.

Splats in yield and block arguments

Splats now work in yield and block arguments. This makes it trivial to forward block arguments to another method:

def foo
  yield 1, 2
end

def bar
  foo do |*args|
    yield *args
  end
end

bar do |x, y|
  x # => 1
  y # => 2
end

Named tuples and arguments can be created with string literals

Named tuples were introduced in the previous release but only allowed identifiers as keys.

{foo: 1, bar: 2}

Starting from this release, we can use a string literal too. This makes it possible to have named tuples with spaces and other symbols:

{"hello world": 1}

This is a breaking change, as that syntax used to mean a Hash with string keys. Now, only => means Hash, and : always means a named-something.

Why is this useful? Consider a library like html_builder, that provides an efficient DSL for generating HTML:

require "html_builder"

html = HTML.build do
  a(href: "http://crystal-lang.org") do
    text "crystal is awesome"
  end
end

puts html # => %(<a href="http://crystal-lang.org">crystal is awesome</a>)

We say it’s efficient because HTML.builds creates a string builder, and methods append to it. For example the a method appends "<a ...></a>", and so on. And, in this case, the argument to a is a named argument (href), which on the method side gets captured as a named tuple, iterated and appended to the string builder, so no memory allocations other than that for the string builder exist.

The problem is, if we wanted to have a "data-foo" attribute we couldn’t do it: we’d had to use a Hash, which is much slower. Well, now we can:

require "html_builder"

html = HTML.build do
  a(href: "http://crystal-lang.org", "data-foo": "yes") do
    text "crystal is awesome"
  end
end

puts html # => %(<a href="http://crystal-lang.org" data-foo="yes">crystal is awesome</a>)

This is just one use case, but one can imagine many more uses cases. For example, generating JSON objects with keys that have spaces:

require "json"

{"hello world": 1}.to_json # => "{\"hello world\":1}"

Class variables are now inherited

Class variables now work more like Ruby’s class instance variables: they are available in subclasses, with the same type, but each subclass has a different value for it.

For example:

class Foo
  @@value = 1

  def self.value
    @@value
  end

  def self.value=(@@value)
  end
end

class Bar < Foo
end

p Foo.value # => 1
p Bar.value # => 1

Foo.value = 2

p Foo.value # => 2
p Bar.value # => 1

Bar.value = 3

p Foo.value # => 2
p Bar.value # => 3

OpenSSL and TLS improvements

@jhass and @ysbaddaden took the lead on improving and stabilizing OpenSSL and TLS-related functionality in the standard library. Be sure to read the changelog to see all the additions and changes. Huge thanks to them!

For Functional Languages fans…

User defined classes can now be generics with a variable number of type arguments. The built-in Tuple, Union and Proc use this. For example, Proc is Proc(*T, R), with T being the arguments types and R the return type.

With that, and because T and R can be queried at compile time, we added a partial method:

add = ->(x : Int32, y : Int32) { x + y }
add.call(1, 2) # => 3

add_one = add.partial(1)
add_one.call(2)  # => 3
add_one.call(10) # => 11

add_one_and_two = add_one.partial(2)
add_one_and_two.call # => 3

One could even define a curry method if we wanted too. But we leave that as an exercise to the reader (hint: use a different struct to represent a curried method).

And more…

There are more little features, like being able to use macros in more places, better error message when an as cast fails, and several enhancement to the standard library.

Thanks

Thank you everyone who contributed to this release! <3

Contribute