Crystal язык программирования

  • 29 декабря 2020

Ary Borenszweig создал Crystal в 2011 году, и к нему вскоре присоединились Хуан Ваджнерман, Брайан Кардифф и вся команда из аргентинской компании Manas Technology Solutions. В настоящее время на GitHub также ведется полномасштабная работа с открытым исходным кодом, где задействовано примерно 250 участников. На момент написания этого руководства для программистов "Кристалл" находился на стадии версии 0.27.0. Настойчивые усилия по достижению производственной версии 1.0 принесли свои плоды, и довольно мощное сообщество уже поддерживает рост популярности Crystal.

Компилируемый язык, похожий на Ruby, — звучит привлекательно, не так ли? Программировать на более низком уровне в Crystal гораздо проще, чем в Ruby, и к тому же код Crystal выполняется намного быстрее. Поэтому его название содержит букву "С" — это отсылка к прародителю.
Помимо сильных корней Ruby, Crystal также ищет вдохновение у других современных языков, таких как Rust, Go, C#, Python, Julia, Elixir, Erlang и Swift. Этот язык программирования сочетает лучшие черты своих предшественников таким образом, как ни один другой ЯП до него не мог.


Crystal объединяет синтаксис и многие идиомы из Ruby с такими вещами, как:

• Статическая система типов, в которой типы определяются в основном автоматически.
• Автоматизированный сбор мусора, который делает использование памяти безопасным.
• Компиляция в машинный код посредством инструментария LLVM для скорости, с упором на низкий
объем потребляемой памяти.
• Макросистема оценки времени компиляции, обеспечивающая значительную гибкость
динамического подхода Ruby, но без ограничения производительности.
• Исключительная объектная ориентация: всё в Crystal является объектом, по сути.
• Поддержка групповых параметров, а также перегружаемых методов и операторов.
• Масштабируемость и параллелизм, реализованные в совместной, простой и легкой для понимания
базисной модели, называемой Fibers (Нити, проводники). Они представляют собой коммуникационные последовательные процессы (иначе говоря — пути сообщения); подобная архитектура под названием CSP используется в ЯП Go.




Crystal стремится быть кросс-платформенным, поддерживая Linux, macOS и FreeBSD как для x86/64 (64-бит), так и для архитектуры x86 (32-бит). Он также имеет поддержку ARMv6/ARMv7. Лучшие члены команды активно работают над портом Windows.
Crystal также является самодостаточным (в смысле реализации): его компилятор написан на Crystal, что позволяет увидеть этот язык программирования в действии. Название Кристалл говорит само за себя: больше прозрачности. Чтобы внести свой вклад в развитие языка, все, что вам нужно знать, — это сам язык.




Похож на Ruby, но гораздо быстрее.

Безошибочно узнаваемый аромат Ruby веет от элегантного, читаемого и менее многословного кода.
Пример “Hello world "(см. hello_world.cr) на Crystal будет кратким:
puts "Hello, Crystal World!" # => Hello, Crystal World!

Этот код выводит строку на стандартный вывод. Если вы хотите, чтобы он был еще короче, используйте 'p' для печати любого объекта:
p "Hello, Crystal World!" # => "Hello, Crystal World!"

Вам не нужно помещать свой код в класс или начинать с функции main (). Это делается просто, в одну строку!


Crystal является идеальным дополнением к Ruby, поскольку приносит гораздо большую
производительность в тех местах, где Ruby в этом особенно нуждается, в то время как Ruby может
продолжать играть свою очень динамичную роль в других частях приложения. Чтобы сравнить
производительность языков, давайте сравним программу, которая является совершенно одинаковой как для Crystal, так и для Ruby, и запустим ее в обеих языковых реализациях. Число Фибоначчи — это сумма двух его предшественников, за исключением 0 и 1, где оно возвращает само себя.

Эта программа вычисляет сумму ряда чисел Фибоначчи с помощью рекурсивного алгоритма, и является корректной для Ruby и Crystal одновременно. Программа вычисляет числа Фибоначчи от 1 до 42 и складывает их в переменную сумму, которая будет показана в конце. Фактический расчет происходит внутри метода Фибоначчи.
===
why_crystal/fibonacci.cr
def fib(n)
return n if n <= 1
fib(n - 1) + fib(n - 2)
end sum = 0
(1..42).each do |i|
sum += fib(i)
end

puts sum # => 701408732
===

Программа называется fibonacci.cr, где '.cr' — типичное расширение для "исходников" Crystal. (Ruby не возражает.) Глядя на программный код fibonacci.cr, вы увидите "знакомые черты" Ruby: переменные без указания типа, знакомое определение метода, и также итератор .each — сверх диапазона значений.
Мы приведем время его выполнения на одной машине: Ubuntu 16.04, 64-битная ОС, процессор AMD A8-6419 с 12 ГБ оперативной памяти. Код не оптимизирован как следует, но поскольку тот же самый код запускается в двух разных языках, это вполне справедливое сравнение. Код не очень хорошо настроен, но потому, что тот же самый код работает в обоих языках, это справедливое базовое сравнение.

Давайте посмотрим, как это сработает в Ruby:
$ time ruby fibonacci.cr
real 3 m 44.437s
user 3 m 43.848s
sys 0 m 0.048s

Теперь результат Кристалла на достигнутом этапе развития:
$ time crystal fibonacci.cr
real 0m12.149s
user 0m12.044s
sys 0m0.356s

Crystal выполняет ту же задачу за 12 секунд, включая время сборки (компиляции), производительность улучшена в 18,5 раза. Если бы вы собирались использовать эту программу в дальнейшей работе, вы могли бы собрать одну версию:
$ crystal build --release fibonacci.cr

Будет сгенерирован исполняемый файл fibonacci, и посмотрите на результат его запуска:
$ time ./fibonacci
real 0m10.642s
user 0m10.636s
sys 0m0.000s


Вы заметите дополнительное улучшение, дающее повышение скорости в 21 раз над ЯП Ruby!
Crystal компилируется в нативный код, но это не затрудняет скорость разработки: можно запустить
проект (даже если он содержит зависимости от других библиотек) так же просто, как здесь:
$ crystal project.cr

Это похоже на режим интерпретатора. Тем не менее, исходный код был полностью скомпилирован во временный файл, содержащий омологированный код, а затем выполнен. Запомните: Crystal не имеет интерпретатора или какой-то виртуальной машины. Итак, каковы же тогда языковые различия между Ruby и Crystal, которые объясняют этот огромный разрыв в производительности?
Crystal компилируется в исполняемый код, и для того, чтобы сделать это, компилятору
необходимо знать типы всех выражений в коде.

Но благодаря тому, что это умный компилятор, который вычисляет большинство
типов самостоятельно через алгоритм вывода, компилятор редко требует от вас уточнений типов, используемых в исходном коде. Тем не менее, весьма желательно четко пояснять ваш код — конкретным указанием типов, чтобы ваши намерения были ясны и людям, читающим его, и компилятору.




Ускорение работы в Web-пространстве.

Веб-фреймворки были оплотом для динамических языков, таких как Ruby, Python и PHP.
А что предлагает Crystal? Стандартный веб-сервер библиотеки Crystal сам по себе очень производителен. В тесте, сравнивающем веб-серверы, реализованные в Node.js, Nim, Rust и Scala, HTTP-сервер Кристалла обработал больше запросов в секунду, чем любой другой сервер.
Тесты приходят и уходят, но Crystal стабильно выдает хороший результат, несмотря на то, что пока его производительность ограничена использованием лишь одного процессора.

А как же более полноценные веб-фреймворки? Одна подобная платформа для Crystal стоит особняком, это Kemal.
Эта небольшая и гибкая веб-инфраструктура поддерживает интерфейс RESTful, очень похожий
на Синатру в ЯП Ruby. Более полное рассмотрение см. в разделе "Создание веб-приложений с помощью фреймворков Kemal и Amber" оригинального руководства для программистов. Kemal и Sinatra сравнивались с помощью wrk, современного инструмент эталонного тестирования для
HTTP-приложений в режиме с использованием одного ядра и веб-варианта типичной программы
"Hello-world".
Его исходный код на Crystal:
===
why_crystal/kemal.cr
require "kemal"
get "/" do
"Hello from Kemal!"
end

Kemal.run
===
Серверы, построенные на двух этих фреймворках, были протестированы со 100 подключенными хостами в течение 30 секунд:
$ wrk -c 100 -d 30 http://localhost:3000

В результате Kemal оказался быстрее примерно на 88-93%. Другой аналогичный тест показал, что Kemal обрабатывает до 64986 запросов в секунду со средним временем отклика 170 микросекунд на запрос, в то время как Sinatra отвечал на 2274 запросов в секунду со средним временем 43.82 миллисекунд. Оба теста показывают, что Kemal обрабатывает примерно в 28 раз больше запросов в секунду, чем Sinatra.




Связь с базами данных.

Crystal предлагается в комплекте с библиотекой баз данных для нормальной работы с SQLite, MySQL и PostgreSQL. Стефан Вилле сравнил производительность клиентской библиотеки, используемой в Redis, для разных языков, совместимых с Redis в потоковом (конвейерном) режиме.
Это означает, что клиент ставит в очередь всю серию (1 000 000 шт.) запросов, посылает их в одной большой партии, а затем получает все ответы в другой, что минимизирует влияние операционной системы и фокусирует измерения на конкретных трудозатратах, вызванных клиентской библиотекой и языком программирования.

Результаты, представленные как количество команд, обрабатываемых в секунду, достаточно убедительны. В сравнении c Ruby, Node.js, Java, io.js, Go и C, ЯП Crystal легко обходит конкурентов, более чем вдвое опережает С, и хоть как-то соперничать с ним может только Go.

Вышеприведенные факты свидетельствуют: нет ничего удивительного в том, что Crystal может быть эффективным в разных областях применения: для веб-серверов, трудоемких вычислительных задач бэкэнд-серверов, в утилитах командной строки, микросервисах, работе с базами данных и даже в играх.
Кроме скорости, какие еще преимущества приносит инструментарий Кристалла?


Больше безопасности с помощью Типов.

Crystal обеспечивает больше, чем одну лишь производительность. Компилятор Crystal также
предоставляет преимущества статической проверки типов, предотвращающей множество ошибок
"времени выполнения" (runtime errors). Следующий фрагмент, который синтаксически корректен для Ruby и Crystal одновременно, показывает, как это работает. Он вызывает метод add, который, в свою очередь, сам вызывает несколько значений:
===
why_crystal/error1.cr
def add(x, y)
x + y
end

puts add(2, 3) # => 5
#() могут быть отброшены при вызове метода
puts add(1.0, 3.14) # => 4.14
puts add("Hello ", "Crystal") # => "Hello Crystal"
#+ связывает две последовательности (т.е. строки)
===
Вы знаете, что операция конкатенации (+) допустима для целых чисел, плавающих чисел и строк, но что, если попробовать:
puts add(42, " times")
Для запуска этого в Ruby наберите в терминале:
$ ruby error1.cr.

Программа выводит значения 5, 4,14 и "Hello
Crystal", а затем рапортует об ошибке во время выполнения программы:
'+': String can't be coerced into Fixnum (TypeError).
Ruby интерпретирует код при его запуске, и методы просматриваются во время выполнения. Ruby не найдет проблему до тех пор, пока она не всплывет в программе.

Чтобы запустить тот же код в Crystal, используйте:
$ crystal error1.cr.
Кристалл компилирует программу целиком; это отдельный этап перед запуском на выполнение.
Поскольку Crystal обнаруживает ошибку во время этого процесса, он останавливается во время
компиляции и программа не запускается:

Error in error1.cr:8: instantiating 'add(Int32, String)'
add(42, " times")
^~~
in error1.cr:2: no overload matches 'Int32#+' with type String


В Crystal отсутствует метод добавления строки к целому числу. Компилятор знает это и останавливается. Потому код не доходит до стадии исполнения. Теперь рассмотрим подобную ситуацию, снова на примере кода с синтаксисом, который подходит для Ruby и Crystal:
===
why_crystal/error2.cr
str = "Crystal"
ix = str.index('z')

puts str[ix]
===

Здесь выполняется поиск местоположения символа "z" в строке str, содержащей "Crystal"; переменная ix должна сохранить порядковый номер искомого символа. Но что, если символ не присутствует в строке, как в данном случае? Как в Ruby, так и в Crystal, ix будет равен нулю, сигнализируя, что ничего не найдено.

Ruby выдает ошибку, если совпадений не обнаружено:
===
$ ruby error2.cr
error2.cr:3:in `[]': no implicit conversion from nil to integer (TypeError)
from error2.cr:3:in `main'
===
Это происходит во время исполнения программы; и желательно, чтобы во время тестирования, а не когда вы — пользователь, запустивший программу.
Crystal, тем не менее, выдаёт ошибку во время компиляции:
===
$ crystal error2.cr
Error in error2.cr:3: no overload matches 'String#[]' with type (Int32 | Nil)
===
Ошибка содержит гораздо больше информации, но это — ключевая строка (нет совпадений для 'String#[]' с таким типом значения). Компилятор улавливает вероятную ошибку
даже раньше фазы тестирования, не говоря уже о стадии компилирования. Сообщение об ошибке
также указывает на возникшую проблему гораздо вернее, чем это делается в Ruby: метод puts,
применяемый к String, не будет работать с нулевым аргументом. Другими словами: поиск элемента в позиции ix внутри 'str' завершается неудачно, когда 'ix' равно «Nil».

Вы можете исправить эту ситуацию (а также и в Ruby), тестируя ix таким образом:
===
why_crystal/error2.cr
if ix
puts str[ix]
end
===

Выражение if в Crystal может принимать значение «true» и любые другие значения, исключая «false» и «нуль» (а также нулевые указатели). Итак, в условном ветвлении «if» Crystal знает, что ix не может быть нулевым, и ошибки компиляции больше нет.
Кроме того, Crystal позволяет избежать ошибок компиляции пока длится поддержание методов,
принимающих многообразие различных типов. Превосходя ограничения синтаксиса Ruby, Crystal
позволяет создавать различные методы, которые имеют одинаковое имя, но используют различные типы аргументов; такие методы зовутся "перегружаемыми".


Нашли ошибку на сайте?
Заполните форму обратной связи и напишите свой вопрос