Обзор языка Crystal

6f808877dfe5abd47a12065c7f0b0cf1.jpg

Привет, Хабр!

История 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}" %}
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 в рамках практических онлайн-курсов. С полным каталогом курсов можно ознакомиться по ссылке.

© Habrahabr.ru