Об авторе: Соутаро является ведущим разработчиком Ruby в Square, работающим над Steep и статической типизацией; вместе с Матцем и другими разработчиками ядра он работает над спецификацией RBS, которая будет поставляться с Ruby 3.

Мы с радостью анонсируем новый язык сигнатуры типов для Ruby 3 — RBS. Одной из давно заявленных целей Ruby 3 было добавление инструментов проверки типов. После продолжительных обсуждений с Матцем и командой разработчиков Ruby мы решили предпринять инкрементный шаг, добавив в Ruby 3 язык сигнатуры типов RBS, который будет поставляться вместе с сигнатурами для stdlib. Инструменты командной строки RBS также будут поставляться вместе с Ruby 3, так что вы сможете генерировать сигнатуры для собственного кода Ruby.

Справочная информация

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

Разработчики языков программирования, разумеется, знают об этой проблеме и пытаются её компенсировать, используя возможности других языков. В C# есть функция dynamic, переносящая проверку типов со стадии компиляции на этап выполнения: присвоение и чтение любого типа разрешено в процессе компиляции, но может вызвать ошибку при выполнении программы для обеспечения безопасности. Это почти эквивалентно языкам с динамической типизацией! Что насчёт обратной ситуации? Мы видим, что такие языки с динамической типизацией, как PHP и Python, реализуют опции проверки типов. У нас также есть типизированные диалекты языков с динамической типизацией, используемых в производственной среде, например TypeScript.

Ещё четыре года назад Матц объявил, что Ruby 3 будет поддерживать статическую проверку типов. После просмотра нескольких средств проверки типов, разработанных сообществом, команда разработчиков Ruby решила заложить основу для последующего развития сообществом. Ruby 3 будет поставляться со способностью писать сигнатуры типов для программ на Ruby, а также со встроенными сигнатурами типов для стандартных библиотек Ruby. Язык сигнатуры стандартного типа сделает определение типов в коде Ruby переносимым между средствами проверки типов и вдохновит сообщество на написание типов для собственных гемов и приложений.

Мы назвали язык и библиотеку RBS.

Как выглядит RBS?

Сигнатуры пишутся в файлах с расширением .rbs, которые отличаются от кода Ruby. В некотором роде файлы .rbs похожи на файлы .d.ts в TypeScript или файлы .h в C/C++/ObjC. Преимущество отдельных файлов в том, что для запуска проверки типа не нужно изменять код Ruby. Вы можете безопасно подписаться на проверку типа, не внося ни единого изменения в рабочий процесс.

Сигнатуры типов для классов Ruby в RBS будут выглядеть так.

# sig/merchant.rbs

class Merchant
  attr_reader token: String
  attr_reader name: String
  attr_reader employees: Array[Employee]

  def initialize: (token: String, name: String) -> void

  def each_employee: () { (Employee) -> void } -> void
                   | () -> Enumerator[Employee, void]
end

Файл merchant.rbs определяет класс Merchant, что помогает читателю понять обзор класса.

У класса есть три атрибута: token, name и employees. Типом token и name является String. RBS также поддерживает обобщённые классы, такие как Array. Например, тип атрибута employees — Array класса Employee.

RBS также описывает определённые в классе методы и их типы. Класс определяет методы initialize и each_employee. В качестве именованных аргументов для метода initialize требуются token и name. Метод each_employee принимает блок или возвращает экземпляр Enumerator.

RBS — это язык для описания структуры программ Ruby. Он даёт разработчикам обзор кода и представление о том, какие классы и объекты в нём определены. Самым большим преимуществом является то, что определение типа может быть проверено на соответствие как на стадии разработки, так и в режиме выполнения!

Ключевые фичи RBS

Разработка системы типирования для такого языка с динамической типизацией, как Ruby, отличается от ординальных языков со статической типизацией. В мире уже существует огромное количество кода на Ruby, и система типов должна поддерживать как можно больший объём этого кода.

Это заставляет разработчиков системы типирования идти на компромиссы в отношении сложности и корректности для совместимости с существующим кодом. Возможно, нам придётся вводить функцию проверки типов для поддержки шаблона в существующем коде Ruby, который в противном случае может быть ошибочным. Однако добавление функции делает систему типов громоздкой и сложной для понимания. Поэтому мы сосредоточились на наиболее важных шаблонах кода, чтобы минимизировать её сложность.

Продемонстрируем две важные характеристики кода Ruby и возможности присвоения типа для них.

Утиная типизация

Утиная типизация — популярный в среде рубистов стиль программирования, который предполагает, что объект будет реагировать на определённый набор методов. Преимущество утиной типизации в её гибкости. Она не требует наследования, миксинов или объявлений реализации. Если у объекта есть конкретный метод, он работает. Проблема в том, что это предположение скрыто в коде, что делает его визуально сложнее.

Чтобы учесть утиную типизацию, мы ввели типы интерфейса, которые представляют собой наборы методов, независимых от конкретных классов и модулей.

Если мы хотим определить метод, требующий конкретный набор методов, мы можем написать его с помощью типов интерфейса.

interface _Appendable
  # Требует оператор `<<`, который принимает объект `String`.
  def <<: (String) -> void
end

# Передача `Array[String]` или `IO` работает.
# Передача `TrueClass` или `Integer` не работает.
def append: (_Appendable) -> String

Этот код лучше традиционной утиной типизации, поскольку определяет ожидаемую реализацию явного интерфейса класса или модуля и предоставляет подсказки для документации и расширений для редактора кода. Таким образом, ранее неявный интерфейс представлен в качестве надёжной практичной документации.

Неоднородность

Неоднородность — ещё один шаблон, позволяющий выражению иметь различные типы значений. Он популярен в Ruby и применяется:

  • когда вы определяете локальную переменную, хранящую экземпляры двух разных классов;
  • когда вы пишете гетерогенную коллекцию;
  • когда вы возвращаете из метода два разных типа значений.

Чтобы приспособиться к этому, RBS допускает типы объединения и перегрузку методов.

class Comment
  # Комментарий могут создавать User или Bot
  def author: () -> (User | Bot)

  # Две перегрузки с блоками/без них
  def each_reply: () -> Enumerator[Comment, void]
                | { (Comment) -> void } -> void

  ...
end

Типы объединения и перегрузка методов часто встречаются в коде Ruby и стандартных библиотеках.

Программирование на Ruby с использованием типов

Мы предоставляем язык для написания типов. Так что же мы можем делать с файлами RBS?

Ниже список основных преимуществ наличия типов. Мы можем писать типы в файлах RBS, чьи инструменты помогают с:

  • поиском дополнительных ошибок: мы сможем обнаруживать неопределённый вызов метода, неопределённую константную ссылку и многое другое, что может пропустить язык с динамической типизацией;
  • проверкой на нулевое значение (Nil safety): у средств проверки типов, основанных на RBS, есть концепция опциональных типов, допускающих значение nil. Средство проверки типов может проверить возможность выражения принимать значение nil и безопасно обнаруживать неопределённый method для nil:NilClass.
  • улучшением интеграции IDE: парсинг файлов RBS даёт IDE лучшее понимание кода Ruby; автозавершение имён методов выполняется быстрее; выдача отчёта об ошибках по ходу работы позволяет выявить больше проблем. Рефакторинг становится более надёжным!
  • управлением утиной типизацией: типы интерфейсов можно использовать для утиной типизации, что помогает пользователям API более чётко понимать, что они могут делать; это более безопасная версия утиной типизации.

Разумеется ничто не бывает просто так. Как мы создаём инструменты для RBS, чтобы облегчить разработчикам работу с ним?

Мы разработали средства статической проверки типов поверх RBS. Steep — это средство статической проверки типов, реализованное в Ruby и основанное на RBS. Sorbet — это средство статической проверки типов, имеющее собственный язык определения типов, RBI, но в будущем планируется поддержка RBS.

Кроме того, мы разрабатываем и работаем над дополнительными инструментами для расширения набора RBS. Средство проверки типов выполнения RBS — это один из проектов Ruby Google Summer of Code, использующий сигнатуры типов RBS для реализации проверки типов выполнения. type-profiler — это исследовательский проект для создания файлов RBS из исходного кода Ruby на основе метода анализа программ, называемого Abstract Interpretation. Также существует проект поддержки для Rails.

Sorbet и RBS

Sorbet — это наиболее широко используемое на сегодняшний день средство статической проверки типов для Ruby. RBS не пытается вытеснить Sorbet и его формат сигнатуры типа RBI. Матц и команда разработчиков Ruby тесно сотрудничают с командой Sorbet и высоко ценят их усилия и те улучшения, которые команда Sorbet вносит в разработку.

Цель RBS в том, чтобы предоставить основу для описания информации о типах программ Ruby. Такие средства статической проверки типов, как Sorbet или Steep, могут использовать определение типов, написанное в RBS. Для упрощения взаимодействия гем RBS поставляется с переводчиком с RBI на RBS, а переводчик с RBS на RBI уже разрабатывается.

Заключение

В этом посте я представил вам RBS — новый проект Ruby 3 для работы с типами. Я объяснил, что можно написать, используя RBS, ключевые идеи его дизайна, а также его преимущества и инструменты, поставляющиеся с ним. Вы пишете определения типов для кода Ruby, а наши инструменты анализируют ваш код. Мы знаем, что не все рубисты перейдут на типизированный Ruby, но мы уверены, что его стоит попробовать!

Читайте также:

Читайте нас в Telegram, VK и Яндекс.Дзен


Перевод статьи Soutaro Matsumoto: The State of Ruby 3 Typing

Предыдущая статьяWebRTC: фреймворк ICE, STUN и сервера TURN
Следующая статьяМетод опорных векторов: примеры на Python