Movatterモバイル変換


[0]ホーム

URL:


Skip to content
GitHub RepositoryForumRSS-Newsfeed

Crystal 0.18.0 released!

Ary Borenzweig
Changelog Source

Crystal 0.18.0 has been released!

This is ahuge release with many new language and standard library features, anda 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 writeInt32|String in type expressions, but you couldn’t writeUnion(Int32,String)the same way you can writeTuple(Int32,String) to denote a tuple of two types.

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

For example, we can define a method to parse a JSON string into aUnion. To do this we tryto parse the string for each type in the union, as can be seenhere(the code could be justthese 9 lines, butit’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"structPointJSON.mappingx:Int32,y:Int32endstructCircleJSON.mappingcenter:Int32,radius:Int32endclassResultJSON.mappingshape:Point|Circleendresult=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 syntaxand a huge part of its standard library.

In Ruby there’s theEnumerable module. You justneed to define aneach method that yields some values,includeEnumerable, and you geta lot of collection methods likemapandselect. For example:

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

Ruby’sHash, a mapping from keys to values,is alsoEnumerable. But there’s something a bit magical happening inHash. Take a look:

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

So, we can iterate aHash and get its keys and values, and we can also usemap on it,and transform the keys and values. But how does it work?

One would think thatHash implementseach like this:

classHashdefeach# for each key and valueyieldkey,value# endendend

Then maybe Enumerable’smap is implemented like this:

moduleEnumerabledefmaparray=[]# We need a splat because Hash yields multiple valueseachdo|*elem|array.push(yield*elem)endarrayendend

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

moduleEnumerabledefmap2array=[]# We don't use a splateachdo|elem|array.push(yieldelem)endarrayendendhash={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 thanone argument, the array is unpacked. For example:

deffooyield[1,2]endfoodo|x,y|x# => 1y# => 2end

SoHash is actuallyyieldingatwo-element array, not two elements, and whenusingeach,map andselect, 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 hashas if it were a sequence of keys and values, without us having to care ifit’s internally implemented as such; and when we want to add methods toEnumerable we don’t need to use splats to “get it right”, we can justtreat each yielded element as a single object.

In Crystal we decided to do the same, although for tuples, because their sizeis known at compile-time. This means that the first Hash snippet now worksexactly the same as in Ruby, andEnumerable’s code remained the same, andextensions 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 forwardblock arguments to another method:

deffooyield1,2enddefbarfoodo|*args|yield*argsendendbardo|x,y|x# => 1y# => 2end

Named tuples and arguments can be created with string literals

Named tuples were introduced in theprevious releasebut only allowed identifiers as keys.

{foo:1,bar:2}

Starting from this release, we can use a string literal too. This makes it possible tohave 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 likehtml_builder,that provides an efficient DSL for generating HTML:

require"html_builder"html=HTML.builddoa(href:"http://crystal-lang.org")dotext"crystal is awesome"endendputshtml# => %(<a href="http://crystal-lang.org">crystal is awesome</a>)

We say it’s efficient becauseHTML.builds creates a string builder, and methodsappend to it. For example thea method appends"<a ...></a>", and so on. And,in this case, the argument toa is a named argument (href), which on the methodside 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 aHash, which is much slower. Well, now we can:

require"html_builder"html=HTML.builddoa(href:"http://crystal-lang.org","data-foo":"yes")dotext"crystal is awesome"endendputshtml# => %(<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, generatingJSON 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 availablein subclasses, with the same type, but each subclass has a different value for it.

For example:

classFoo@@value=1defself.value@@valueenddefself.value=(@@value)endendclassBar<FooendpFoo.value# => 1pBar.value# => 1Foo.value=2pFoo.value# => 2pBar.value# => 1Bar.value=3pFoo.value# => 2pBar.value# => 3

OpenSSL and TLS improvements

@jhass and@ysbaddaden took thelead on improving and stabilizing OpenSSL and TLS-related functionality in the standard library.Be sure to read thechangelogto 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-inTuple, Union and Proc use this. For example,Proc isProc(*T,R), withT being the argumentstypes andR the return type.

With that, and becauseT andR can be queried at compile time, we added apartial method:

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

One could even define acurry method if we wanted too. But we leave that as an exerciseto 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 errormessage when anas cast fails, and several enhancement to the standard library.

Thanks

Thank you everyone who contributed to this release! <3

Contribute

Found a typo, or want to help improve this page?Edit on GitHub orfile an issue.


[8]ページ先頭

©2009-2025 Movatter.jp