LINUX.ORG.RU

Crystal 0.18.0

 ,


0

0

Состоялся релиз языка программирования 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

★★★★★

Последнее исправление: DeadEye (всего исправлений: 1)

О, заодно и вводный курс по языку в новости сразу, удобно. (на самом деле нет)

loz ★★★★★
()

Минорщина (и новость слишком длинная). Ждем релиза или хотя бы оператора match.

tailgunner ★★★★★
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.