LEARN X · ЗА 15 МИН

Crystal

Crystal за 15 минут: синтаксис как у Ruby, скорость как у C. Типы, union-типы и nil, классы, дженерики, исключения — весь язык в комментариях кода.

Crystal — это компилируемый язык со скоростью C и синтаксисом, почти неотличимым от Ruby. Статическая типизация с мощным выводом типов: типы редко пишут руками, но компилятор знает их все. Главная фишка — union-типы и обязательная проверка nil на этапе компиляции. Весь язык ниже — в комментариях к рабочему коду.

Вывод и комментарии

# Это однострочный комментарий — начинается с #

puts "Привет, Crystal!"   # puts печатает строку и перенос строки
print "без переноса"       # print — без \n в конце
print "\n"

p "строка"   # p печатает inspect-представление: с кавычками -> "строка"
p 42         # для чисел p и puts выглядят одинаково -> 42
p [1, 2, 3]  # p удобен для отладки -> [1, 2, 3]

pp 2 + 2     # pp печатает И выражение, И результат -> 2 + 2 # => 4

Переменные и типы

Crystal статически типизирован, но тип почти всегда выводится автоматически.

x = 42          # переменные не объявляют через var — просто присваивание
y = 3.14
name = "Аня"

# .class возвращает тип значения в рантайме
puts 42.class      # => Int32   (целое по умолчанию 32 бита)
puts 3.14.class    # => Float64 (дробное по умолчанию)
puts true.class    # => Bool
puts "hi".class    # => String
puts 'A'.class     # => Char    (одиночный символ в одинарных кавычках)
puts :ok.class     # => Symbol  (символ — лёгкая неизменяемая метка)

# Явные типы целых: Int8/Int16/Int32/Int64, беззнаковые UInt8..UInt64
big = 9_000_000_000_i64   # суффикс _i64 -> Int64
byte = 255_u8             # суффикс _u8  -> UInt8
puts big.class            # => Int64

# Подчёркивания в числах — для читаемости
million = 1_000_000

# Константы — с Большой Буквы
PI = 3.14159

Строки

name = "Crystal"

# Интерполяция через #{...} — внутри любое выражение
puts "Язык: #{name}, длина имени: #{name.size}"

# Конкатенация и повтор
puts "abc" + "def"   # => abcdef
puts "ха" * 3        # => хахаха

# Полезные методы строк
s = "  Hello World  "
puts s.strip          # убрать пробелы по краям -> "Hello World"
puts s.upcase         # ВЕРХНИЙ регистр
puts s.downcase       # нижний регистр
puts "Hello".reverse  # => olleH
puts "a,b,c".split(",") # => ["a", "b", "c"]
puts "Hello".includes?("ell") # => true
puts "Hello"[1]       # индексация -> 'e' (Char)
puts "Hello"[1..3]    # срез по диапазону -> "ell"

# Многострочный текст (heredoc)
text = <<-TEXT
  Первая строка
  Вторая строка
  TEXT
puts text

Union-типы и nil

Фишка Crystal: nil — это отдельный тип, и компилятор заставляет его обрабатывать. Переменная может иметь union-тип вроде String | Nil.

# Тип со знаком вопроса: String? — это сокращение для String | Nil
name : String? = "Аня"
name = nil          # допустимо: тип union включает Nil

# Union возникает сам, когда ветки дают разные типы
value = rand < 0.5 ? "строка" : 100
puts value.class    # либо String, либо Int32 — тип: (String | Int32)

# nil нельзя использовать как обычное значение без проверки.
# Компилятор НЕ даст вызвать метод на возможном nil:
maybe : String? = "hi"

# Проверка через if сужает тип (type narrowing):
if maybe
  puts maybe.upcase   # здесь компилятор знает: maybe — это String
end

# .try выполняет блок только если значение не nil
puts maybe.try &.size    # => 2 (или nil, если maybe был nil)

# .not_nil! утверждает «точно не nil» (упадёт в рантайме, если nil)
puts maybe.not_nil!.upcase

# Оператор || для значения по умолчанию
greeting = maybe || "гость"
puts greeting

Операторы и условия

a = 10
b = 3

# Арифметика
puts a + b      # 13
puts a - b      # 7
puts a * b      # 30
puts a / b      # 3.333... (деление даёт Float)
puts a // b     # 3  (целочисленное деление)
puts a % b      # 1  (остаток)
puts a ** b     # 1000 (степень)

# Сравнения возвращают Bool
puts a > b      # true
puts a < b      # false
puts a == b     # false
puts a != b     # true

# Логические: && (и), || (или), ! (не)
puts (a > 5) && (b < 5)   # true
puts (a > 100) || (b == 3) # true

# if / elsif / else
if a > b
  puts "a больше"
elsif a == b
  puts "равны"
else
  puts "b больше"
end

# unless — это «если НЕ»
unless a == b
  puts "a и b различны"
end

# Постфиксная форма — компактно
puts "чётное" if a % 2 == 0

# case — мощный switch, ветка вычисляется и возвращает значение
grade = case a
        when 0..4   then "плохо"
        when 5..7   then "норм"
        when 8..10  then "отлично"
        else             "вне диапазона"
        end
puts grade

Циклы

i = 0
while i < 3      # while — пока условие истинно
  puts "while #{i}"
  i += 1
end

j = 0
until j == 3     # until — пока условие ЛОЖНО (обратный while)
  puts "until #{j}"
  j += 1
end

# times — повторить N раз
3.times { |n| puts "шаг #{n}" }  # n: 0,1,2

# Диапазоны: .. включает конец, ... исключает
(1..3).each { |n| puts n }   # 1, 2, 3
(1...3).each { |n| puts n }  # 1, 2

# each по коллекции
["a", "b", "c"].each { |x| puts x }

# break и next работают как обычно
(1..10).each do |n|
  next if n.even?   # пропустить чётные
  break if n > 5    # выйти после 5
  puts n            # 1, 3, 5
end

Массивы и хеши

# Массив — Array(T), тип элементов выводится из литерала
nums = [1, 2, 3, 4, 5]       # Array(Int32)
puts nums.class              # => Array(Int32)

# Пустой массив требует явного типа
empty = [] of String         # Array(String)
empty << "первый"            # << добавляет в конец

puts nums[0]      # 1 (индекс с нуля)
puts nums[-1]     # 5 (отрицательный — с конца)
puts nums.size    # 5
puts nums.first   # 1
puts nums.last    # 5
puts nums.sum     # 15

# Функциональные методы возвращают новые коллекции
puts nums.map { |n| n * 2 }        # => [2, 4, 6, 8, 10]
puts nums.select { |n| n.even? }   # оставить чётные -> [2, 4]
puts nums.reject { |n| n.even? }   # убрать чётные   -> [1, 3, 5]
puts nums.reduce { |acc, n| acc + n } # свёртка -> 15

# Hash(K, V) — словарь ключ→значение
ages = { "Аня" => 25, "Петя" => 30 }   # Hash(String, Int32)
puts ages["Аня"]          # 25
ages["Лена"] = 22         # добавить пару
puts ages.keys            # => ["Аня", "Петя", "Лена"]
puts ages.values          # => [25, 30, 22]
puts ages.has_key?("Петя") # => true

# []? возвращает nil вместо ошибки, если ключа нет
puts ages["Кто-то"]?      # => (пусто, nil)

ages.each do |name, age|
  puts "#{name}: #{age}"
end

Методы

# def — определение метода. Тип возврата выводится автоматически.
def greet(name)
  "Привет, #{name}!"   # последнее выражение — возвращаемое значение
end
puts greet("Crystal")

# Аргументы по умолчанию и именованные
def power(base, exp = 2)
  base ** exp
end
puts power(3)         # => 9  (exp по умолчанию 2)
puts power(2, 10)     # => 1024
puts power(base: 5, exp: 3) # именованные аргументы -> 125

# Можно указывать типы и тип возврата явно
def add(a : Int32, b : Int32) : Int32
  a + b
end
puts add(2, 3)

# Метод с вопросительным знаком по соглашению возвращает Bool
def adult?(age)
  age >= 18
end
puts adult?(20)   # => true

# Блоки: yield вызывает переданный блок
def twice
  yield 1
  yield 2
end
twice { |n| puts "блок получил #{n}" }

# Захват блока в переменную через &block
def apply(x, &block : Int32 -> Int32)
  block.call(x)
end
puts apply(5) { |n| n * 10 }   # => 50

# Splat: *args собирает переменное число аргументов в Tuple
def sum_all(*nums)
  nums.sum
end
puts sum_all(1, 2, 3, 4)   # => 10

Классы и ООП

class Animal
  # property создаёт и геттер, и сеттер для @name
  property name : String
  # getter — только чтение
  getter species : String

  # initialize — конструктор; @name — это поле объекта (instance variable)
  def initialize(@name : String, @species : String)
  end

  def speak
    "#{@name} издаёт звук"
  end
end

a = Animal.new("Рекс", "собака")
puts a.name        # геттер от property -> Рекс
a.name = "Барон"   # сеттер от property
puts a.speak
puts a.species     # только чтение (getter)

# Наследование через < (символ «меньше»)
class Dog < Animal
  def initialize(name : String)
    super(name, "собака")   # вызвать конструктор родителя
  end

  # переопределение метода
  def speak
    "#{@name}: Гав!"
  end
end

puts Dog.new("Шарик").speak   # => Шарик: Гав!

Структуры и модули

struct — значимый тип (копируется при передаче), module — для примесей и пространств имён.

# struct похож на class, но передаётся по значению (как Int)
struct Point
  getter x : Int32
  getter y : Int32

  def initialize(@x, @y)
  end

  def +(other : Point)
    Point.new(@x + other.x, @y + other.y)
  end
end

p1 = Point.new(1, 2)
p2 = Point.new(3, 4)
sum = p1 + p2          # вызовет наш оператор +
puts "#{sum.x}, #{sum.y}"   # => 4, 6

# module как примесь (mixin): include добавляет методы в класс
module Greetable
  def greet
    "Привет от #{name}"   # модуль рассчитывает, что у класса есть name
  end
end

class User
  getter name : String
  include Greetable      # подмешать методы модуля
  def initialize(@name)
  end
end
puts User.new("Аня").greet

# module как пространство имён
module Math2
  TAU = 6.283
  def self.double(x)   # self. — метод на самом модуле
    x * 2
  end
end
puts Math2::TAU          # доступ через ::
puts Math2.double(21)    # => 42

Типизация и дженерики

# Дженерик-класс: T — параметр типа, подставляется при создании
class Box(T)
  getter value : T
  def initialize(@value : T)
  end
end

int_box = Box(Int32).new(42)
str_box = Box(String).new("hi")
puts int_box.value   # 42
puts str_box.value   # hi
puts int_box.class   # => Box(Int32)

# Дженерик-метод: тип выводится из аргумента
def first_of(arr : Array(T)) forall T
  arr.first
end
puts first_of([10, 20, 30])   # => 10

# Тип-ограничение в union: метод примет и то, и другое
def describe(x : Int32 | String)
  "значение #{x} типа #{x.class}"
end
puts describe(5)
puts describe("текст")

# as приводит тип (когда вы знаете больше компилятора)
values = [1, "two", 3] of Int32 | String
n = values[0].as(Int32)
puts n + 1   # => 2

Обработка исключений

# begin / rescue / else / ensure
begin
  raise "что-то сломалось"   # raise возбуждает исключение
rescue ex : Exception
  puts "Поймали: #{ex.message}"   # доступ к сообщению
else
  puts "выполнится, если ошибок не было"
ensure
  puts "ensure выполнится ВСЕГДА"
end

# Свой класс исключения
class MyError < Exception
end

def risky(n)
  raise MyError.new("число слишком мало") if n < 10
  n * 2
end

begin
  risky(5)
rescue ex : MyError
  puts "MyError: #{ex.message}"
end

# rescue прямо в методе — без begin
def safe_divide(a, b)
  a // b
rescue DivisionByZeroError
  0   # вернуть 0 при делении на ноль
end
puts safe_divide(10, 0)   # => 0

Особенности языка

# 1) КОМПИЛЯЦИЯ. Crystal компилируется в нативный бинарник:
#    crystal run app.cr     — скомпилировать и запустить
#    crystal build app.cr   — собрать исполняемый файл
#    crystal build --release app.cr   — с оптимизацией для прода

# 2) ВЫВОД ТИПОВ. Типы почти не пишут руками, но проверка строгая
#    на этапе компиляции — ошибки типов ловятся ДО запуска.

# 3) NIL-БЕЗОПАСНОСТЬ. Невозможно вызвать метод на возможном nil —
#    компилятор требует явной проверки. Меньше runtime-падений.

# 4) Кортежи (Tuple) — неизменяемые, фиксированной длины, разных типов
tuple = {1, "два", 3.0}
puts tuple[0]   # 1
puts tuple[1]   # два

# 5) Named Tuple — кортеж с именованными полями
person = {name: "Аня", age: 25}
puts person[:name]   # => Аня

# 6) Макросы выполняются на этапе компиляции (метапрограммирование)
macro define_const(name, value)
  {{name.id}} = {{value}}
end
define_const(ANSWER, 42)
puts ANSWER   # => 42

# 7) Совместимость с C: можно вызывать C-библиотеки через lib
#    Это даёт доступ к огромной экосистеме нативных библиотек.

# Итог: Ruby-синтаксис + статическая типизация + скорость C + nil-безопасность.
Поддержать проект