Обзор языка Crystal
Привет, Хабр!
История Crystal начинается в 2011 году, когда команда энтузиастов решили создать язык, который бы исправил некоторые из тех ограничений и проблем, с которыми они сталкивались, работая с Ruby. Они мечтали о языке, который бы позволял писать код, легкий для понимания и поддержки, но при этом обладающий высокой производительностью и эффективностью выполнения. Так родился Crystal, язык, который наследует синтаксис Ruby.
На первый взгляд, код на Crystal может показаться почти идентичным коду на Ruby — это было сделано намеренно, чтобы разрабы, уже знакомые с Ruby, могли без труда перейти на использование нового языка. Однако, несмотря на внешнее сходство, Crystal вносит ряд улучшений: система статической типизации с автоматическим выводом типов, обработка параллельных вычислений и возможность компиляции в машинный код.
Основной синтаксис
Crystal автоматически выводит типы переменных. Несмотря на это, можно явно указывать типы:
name : String = "Habr"
age : Int32 = 10
Crystal поддерживает стандартные управляющие структуры, такие как if
, else
, case
, а также циклы while
и until
:
if age >= 18
puts "Adult"
else
puts "Minor"
end
Определение методов в Crystal очень похоже на Ruby, нво с добавлением типов аргументов и возвращаемого значения:
def add(a : Int32, b : Int32) : Int32
a + b
end
ООП в Crystal реализовано через классы и модули, подобно Ruby, но с более строгой системой типов:
class Person
property name : String
property age : Int32
def initialize(@name : String, @age : Int32)
end
end
Crystal использует модель акторов и каналы для обработки конкурентных операций:
spawn do
puts "Hello from the parallel universe!"
end
Макросы в Crystal позволяют генерировать код во время компиляции:
macro tag(name, content)
{% puts "<#{name}>#{content}#{name}>" %}
end
tag("p", "Hello, Habr!")
Crystal обрабатывает ошибки через систему исключений, аналогичную Ruby, но требует явного указания возможных типов исключений:
begin
# опасный код
rescue ex : DivisionByZeroError
puts "Cannot divide by zero!"
end
Crystal может легко взаимодействовать с C:
@[Link(ldflags: "-lsqlite3")]
lib LibSQLite3
fun open(filename : String, out db : SQLite3) : Int32
end
Типизация в Crystal
Статическая типизация означает, что тип каждой переменной, параметра и метода известен на этапе компиляции. Статическая типизация позволяет Crystal предотвращать целый ряд ошибок еще до запуска программы.
Однако статическая типизация часто ассоциируется с необходимостью явного указания типов, и все это фиксится с помощью инференции типов — способности компилятора автоматически определять типы на основе контекста использования переменных и выражений.
Рассмотрим простой пример:
def add(a, b)
a + b
end
puts add(1, 2) # 3
Здесь не указаны типы для параметров a
и b
, но благодаря инференции типов Crystal понимает, что оба параметра и результат их сложения должны быть целыми числами, по дефолту в основном Int32.
Если попытаться сложить число и строку:
puts add(1, "two")
Компилятор Crystal выдаст ошибку, поскольку не сможет найти подходящую перегрузку метода add
, которая бы соответствовала таким типам аргументов.
Crystal также поддерживает универсальные типы:
array = [1, 2, 3] # автоматом становится Array(Int32)
Инференция типов распространяется и на универсальные типы, позволяя компилятору автоматически определять конкретные типы элементов в коллекциях и других структурах данных.
Конкуренция и параллелизм
Модель акторов в Crystal — это абстракция, которая позволяет рассматривать каждую единицу параллельного выполнения как актора, способного обрабатывать сообщения, выполнять задачи и взаимодействовать с другими акторам, все это дает высокий уровень изоляции между акторами
Акторы могут:
Создавать других акторов.
Отправлять сообщения другим акторам.
Обрабатывать входящие сообщения.
В Crystal модель акторов реализована через использование волокон и каналов.
Волокна представляют собой легковесные потоки выполнения, которые позволяют выполнять множество задач параллельно внутри одного операционного потока. В отличие от традиционных потоков, переключение контекста между волокнами происходит быстрее, поскольку оно управляется самим языком, а не ос.
Каналы в Crystal типизированы, что означает, что канал для передачи сообщений определенного типа может передавать только сообщения этого типа.
Создадим несколько волокон для параллельной обработки данных, передаваемых через канал:
channel = Channel(Int32).new
# создаем волокна-производители
5.times do |producer_number|
spawn do
5.times do |i|
value = producer_number * 10 + i
puts "Producer #{producer_number} sending: #{value}"
channel.send(value)
sleep rand(0.1..0.5) # имитация задержки
end
end
end
# создаем волокно-потребитель
spawn do
25.times do
received = channel.receive
puts "Consumer received: #{received}"
end
end
sleep 3 # даем время для выполнения волокон
В следующем примере юзаем каналы для сигнализации о завершении асинхронных задач:
done_channel = Channel(Nil).new
# асинхронная задача 1
spawn do
sleep 1 # имитация длительной операции
puts "Task 1 completed"
done_channel.send(nil) # отправляем сигнал о завершении
end
# асинхронная задача 2
spawn do
sleep 2 # имитация еще более длительной операции
puts "Task 2 completed"
done_channel.send(nil) # отправляем сигнал о завершении
end
2.times { done_channel.receive } # ожидаем сигналов о завершении обеих задач
puts "All tasks completed"
Главное волокно ожидает два сигнала о завершении, прежде чем выводить сообщение о том, что все задачи выполнены.
Полезные библиотеки
Установка библиотек в Crystal осуществляется через систему управления зависимостями под названием Shards. Shards аналогичен Bundler в Ruby, npm в Node.js или pip в Python и используется для управления библиотеками, на которых зависит проект.
Каждый проект на Crystal, использующий внешние зависимости, должен иметь файл конфигурации shard.yml
в корневой директории проекта. Этот файлик содержит метаданные проекта и список зависимостей, например:
name: awesome_app
version: 0.1.0
dependencies:
kemal:
github: kemalcr/kemal shards install
version: "~> 0.26.1"
После настройки файла shard.yml
в терминале юзается команда shards install.
Команда скачает и установит все указанные в файле shard.yml
зависимости в папку lib/
проекта. Shards также создаст файл shard.lock
, который содержит версии всех установленных зависимостей
Библиотеки импортируются с помощью require.
Kemal
Kemal — это минималистичный веб-фреймворк для Crystal, вдохновленный Sinatra из Ruby. Kemal поддерживает RESTful приложения, предлагая такие функции маршрутизации, шаблоны, поддержку WebSocket.
Например, определение маршрута веб-сокета может выглядеть так:
require "kemal"
# маршрут WebSocket
ws "/echo" do |socket|
socket.on_message do |message|
socket.send(message) # эхо-ответ
end
end
Kemal.run
Amber
Amber предлагает MVC архитектуру, ORM, систему шаблонов, веб-сокеты и многое другое, к примеру:
require "amber"
class WelcomeController < Amber::Controller::Base
def index
render("index.ecr")
end
end
Amber::Server.configure do |app|
pipeline :web do
plug Amber::Pipe::Logger.new
plug Amber::Pipe::Session.new
end
routes :web do
get "/", WelcomeController, :index
end
end
Amber::Server.start
Crystal DB
Для работы с бд в Crystal существует несколько библиотек. Основная библиотека crystal-db предоставляет общий интерфейс для работы с различными бдшками. На основе crystal-db
построены адаптеры для конкретных СУБД, такие как crystal-mysql
, crystal-sqlite3
и crystal-pg
для MySQL, SQLite и PostgreSQL соответственно.
Для ORM в Crystal можно использовать библиотеку Granite или Jennifer.
Async
Async позволяет легко создавать асинхронные задачи и управлять ими:
require "async"
Async do
# асинхронная задача
sleep 1
puts "Hello from Async!"
end
puts "Hello from Main Thread!"
Метапрограммирование
Метапрограммирование позволяет программам генерировать и трансформировать код во время компиляции.
Допустим, есть класс, и нужно автоматически сгенерировать геттеры и сеттеры для его свойств. Это можно сделать с помощью макросов:
class User
property name : String
property age : Int32
end
macro define_properties(properties)
{% for property in properties %}
@{{property.id}} : {{property.type}}
def {{property.id}}
@{{property.id}}
end
def {{property.id}}=(value : {{property.type}})
@{{property.id}} = value
end
{% end %}
end
class Person
define_properties name: String, age: Int32
end
person = Person.new
person.name = "Alice"
person.age = 30
puts person.name # => Alice
puts person.age # => 30
Макрос define_properties
генерирует свойства с геттерами и сеттерами для класса Person
, используя переданные ему аргументы.
Можно также динамически создавать методы на основе данных, например, массива строк:
class CommandHandler
macro register_commands(commands)
{% for command in commands %}
def handle_{{command.id}}
puts "Handling command: {{command.id}}"
end
{% end %}
end
register_commands [ "start", "stop", "restart" ]
end
handler = CommandHandler.new
handler.handle_start # => Handling command: start
handler.handle_stop # => Handling command: stop
handler.handle_restart # => Handling command: restart
Макрос register_commands
создает методы handle_start
, handle_stop
, и handle_restart
в классе CommandHandler
, используя переданный ему массив команд.
Crystal позволяет использовать аннотации для добавления метаданных к классам, методам и переменным:
annotation Route
end
@[Route(path: "/users")]
class UsersController
@[Route(path: "/")]
def index
# Вывод списка пользователей
puts "Список пользователей"
end
@[Route(path: "/:id")]
def show(id : Int32)
# Показать пользователя по ID
puts "Пользователь #{id}"
end
end
macro generate_routes
{% for klass in @type.ancestors %}
{% for method in klass.methods %}
{% if method.annotation(Route) %}
puts "Маршрут: {{method.annotation(Route).path}}, Метод: #{klass}.{{method.name}}"
{% end %}
{% end %}
{% end %}
end
generate_routes
Здесь определили аннотацию Route
, которую затем используем для маркировки методов в классе UsersController
с метаданными о пути маршрута. Макрос generate_routes
проходит по всем методам, помеченным аннотацией Route
, и выводит информацию о маршрутах.
Crystal позволяет использовать макросы для генерации кода на основе типов переменных или параметров.
macro generate_accessor(property, type)
def {{property.id}}
@{{property.id}} : {{type}}
end
def {{property.id}}=(value : {{type}})
@{{property.id}} = value
end
end
class User
@name : String
@age : Int32
generate_accessor name, String
generate_accessor age, Int32
end
user = User.new
user.name = "Alice"
user.age = 30
puts user.name # => Alice
puts user.age # => 30
Макрос generate_accessor
генерирует геттеры и сеттеры для свойств класса User
, используя указанные типы.
Макросы в Crystal могут использоваться для динамического создания перечислений:
macro create_enum(name, values)
enum {{name.id}}
{% for value in values %}
{{value.id}}
{% end %}
end
end
create_enum Status, [Active, Inactive, Suspended]
puts Status::Active # => Active
puts Status::Inactive # => Inactive
puts Status::Suspended # => Suspended
Макрос create_enum
создает перечисление Status
с указанными значениями.
Растущее сообщество Crystal постоянно работает над улучшением языка, добавлением новых библиотек и фреймворков.
Больше про языки программирования рассказывают эксперты OTUS в рамках практических онлайн-курсов. С полным каталогом курсов можно ознакомиться по ссылке.