Crystal 0.18.0 released!
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.