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


ссылка на оригинал статьи https://habr.com/ru/articles/794983/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *