Состоялся релиз языка программирования Crystal 0.18.0.
Цели языка:
- синтаксис, похожий на Ruby (но совместимость с ним не является целью);
- статическая типизация (но без необходимости явного указания типов переменных и аргументов методов);
- вызов кода на Си с помощью биндингов, написанных на Crystal;
- исполнение и генерация кода во время компиляции (макросы);
- трансляция в эффективный нативный код.
Как это выглядит:
# A very basic HTTP server
require "http/server"
server = HTTP::Server.new(8080) do |context|
context.response.content_type = "text/plain"
context.response.print "Hello world, got #{context.request.path}!"
end
puts "Listening on http://0.0.0.0:8080"
server.listen
Текущее состояние:
- проект находится в стадии alpha: язык и стандартная библиотека всё ещё подвергаются значительным изменениям;
- компилятор написан на Crystal.
Этот выпуск включает огромное количество новых возможностей как в языке, так и в стандартной библиотеке. Как обычно, не обошлось и без поломки обратной совместимости.
Union как объект первого класса
Union-типы существуют в языке с самого первого дня, но, несмотря на это, они являлись типами без имени.
Разумеется, вы могли бы написать Int32 | String
, но Union(Int32, String)
написать не получится,
как, например, Tuple(Int32, String)
, чтобы обозначить кортеж из двух типов.
Теперь это стало возможным. И вы также можете добавить собственные методы.
В качестве примера возьмём метод для парсинга JSON строки в Union. Чтобы это сделать, мы попытаемся спарсить строку для каждого типа в Union, как можно увидеть здесь
Благодаря этому мы теперь можем написать следующее:
require "json"
array = Array(Int32 | String).from_json(%([1, "hello", 2]))
array # => [1, "hello", 2]
Также возможно использовать объединения в маппингах, даже использовать сложные объекты:
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)]
Hash, Enumerable и автораспаковка блоков
Мы не отрицаем, что Ruby очень сильно повлиял на Crystal, как в части синтаксиса, так и в большей части стандартной библиотеки.
В Ruby существует модуль Enumerable.
Вам нужно только определить метод each
который будет генерировать некоторые значения,
подключить модуль с помощью include Enumerable
, и вы получите можество методов, таких, как
map и select.
Например:
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]
Hash, являющийся отображением ключей на значения, также является Enumerable
.
Но есть некоторая магия, происходящая в Hash
. Обратите внимание:
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"]
Так мы можем итерировать Hash
и получить его ключи и значения, и мы также можем использовать метод
map
, и трансформировать ключи и значения. Но как это работает?
Кто-то может подумать, что Hash
реализует each
следующим образом:
class Hash
def each
# for each key and value
yield key, value
# end
end
end
Тогда возможно, что Enumerable.map
реализован вот так:
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
Тем не менее, похоже, что это не так, потому что если мы определим наш собственный метод map
, который не использует распаковку, это работает как ожидается:
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"]
Что происходит?
Ответом является то, что если метод генерирует массив, и блок принимает более одного аргумента, массив распаковывается. Например:
def foo
yield [1, 2]
end
foo do |x, y|
x # => 1
y # => 2
end
Таким образом Hash
генерирует
массив из двух элементов,
не два элемента, и тогда, используя each
, map
и select
, если мы укажем более одного аргумента в блоке, Ruby распаковывает это для нас.
Решение в Ruby является очень удобным и мощным:
это позволяет нам итерировать хэш как если бы это была последовательность ключей и значений,
не заставляя нас заботиться о том, как это реализовано внутри;
и когда мы хотим добавить методы в Enumerable
, мы не нуждаемся в использовании распаковок чтобы “сделать это правильно”,
мы можем просто рассматривать каждый сгенерированный элемент как единственный объект.
В Crystal мы решили сделать то же самое, кроме кортежей, потому что их размер известен во время компиляции.
Это означает, что первый сниппет c Hash
теперь работает точно также, как и в Ruby, и код для Enumerable
остаётся таким же, а расширения для него продолжат работать.
Распаковки в yield
и аргументах блоков
Теперь можно очень просто передать аргументы блока какому-либо методу:
def foo
yield 1, 2
end
def bar
foo do |*args|
yield *args
end
end
bar do |x, y|
x # => 1
y # => 2
end
Именованные кортежи и аргументы могут быть созданы с помощью строковых литералов
Именованные кортежи были добавлены в предыдущем выпуске. Но до этого момента в качестве ключей можно было использовать только идентификаторы.
{foo: 1, bar: 2}
Теперь стало возможным использовать строковые литералы. Это позволяет создавать именованные кортежи с ключами, которые содержат пробелы и другие символы:
{"hello world": 1}
Это изменение нарушает обратную совместимость, так как такой синтаксис используется, чтобы обозначить хэш со строковыми ключами.
Теперь только =>
означает Hash, и :
всегда означает что-то именованное.
Почему это полезно? Рассмотрим библиотеку html_builder, которая предоставляет эффективный DSL для генерации 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>)
Мы говорим, что он эффективный, потому что HTML.builds
создает построитель строк, а другие методы добавляют что-то в него.
Например, метод a
добавляет <a ...></a>
, и так далее.
В этом случае аргумент, переданный методу a
, является именованным аргументом (href), который на стороне метода находится в именованном кортеже,
что позволяет избежать дополнительных выделений памяти.
Проблема заключается в том, что если мы хотим иметь атрибут "data-foo"
, то мы не можем сделать это:
нам приходилось использовать Hash
, который гораздо медленнее. Теперь это стало возможным:
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>)
Это только один юзкейс, но можно придумать ещё. Например, генерирование JSON объектов с ключами, которые имеют проблемы:
require "json"
{"hello world": 1}.to_json # => "{\"hello world\":1}"
Переменные классов теперь наследуются
Переменные классов теперь работают почти как переменные экземпляров классов в Ruby: они доступны в подклассах, с таким же типом, но каждый подкласс может иметь разное значение переменной.
Например:
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
Прочие изменения
Были также добавлены другие незначительные возможности, такие как возможность использовать макросы в ранее недоступных местах, улучшенные сообщения об ошибках при неудачном приведении типов с помощью as
, а также некоторые улучшения в стандартной библиотеке.
С полным списком изменений можно ознакомиться в примечаниях к выпуску.
Перемещено tailgunner из opensource