crails

CRails — Clean Rails (Чистые рельсы)

CRails — Clean Rails

⚠️ EXPERIMENTAL FRAMEWORK, DO NOT USE IN PRODUCTION YET. Это эксперимент. Мы замеряем производительность и смотрим, что получится на данный момент. Никаких гарантий совместимости.

Идеология — контракты прежде всего

Первый и главный документ проекта. Всё остальное в этом README — детали. Если между идеологией и любой фичей возникает конфликт, побеждает идеология.

Чистые рельсы: учить лучшим паттернам, а не магии

Расшифровка CRails — Clean Rails, Чистые рельсы: Crystal здесь инструмент, а «чистота» — продукт. Фреймворк спроектирован так, чтобы новичок, который на нём учится, перенимал лучшие паттерны, на которых индустрия работает десятилетиями. Явные концепции и абстракции проще понимать, проще передавать командам и безопаснее развивать: код, написанный против явного интерфейса, не приходится переучивать вслед за сменой подходов.

Поэтому приоритеты такие: явные контракты и интерфейсы > выразительность кода > магия (на самое последнее место; там, где Rails опирается на runtime-магию, у нас compile-time макрос — она хотя бы читается в исходниках). Эталон долговечности — интерфейсы, служащие десятилетиями: Unix-файл, C-ABI, сокет. У сокета есть интерфейс, у сервер-клиента есть интерфейс, и к ним только прикрепляется — ничего не отламывается. Контракты CRails обязаны жить так же.

Внутренний стандарт выразимости контрактов: модуль-интерфейс предпочтительнее абстрактного класса (module с abstract-методами, include вместо наследования от фреймворк-класса) — контракт без реализации, который проверяет компилятор. Контракты можно дополнять и расширять (новый интерфейс включает старый), но имя и семантика старых методов неприкосновенны: код, написанный против старого интерфейса, работает ровно так, как был задуман. Нужно сделать по-новому — создаётся новый интерфейс рядом и новые реализации под него; старый не перепрофилируется.

Две причины выбрать чистые рельсы

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

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

Контракты — это бизнес

Бизнес не существует без долгоживущих контрактов — с клиентами, партнёрами, подрядчиками. Код — не исключение: API фреймворка — это контракт с тем, кто вложил в него месяцы работы и деньги компании. Фреймворк, который в новой версии меняет собственный контракт, перекладывает эти издержки на вложившихся — а доверие после этого восстанавливается долго. Поэтому мы считаем неизменность API прямой обязанностью фреймворка, а не жестом доброй воли.

Уроки Ruby on Rails: почему мы идём другим путём

Начнём с признания заслуг: у Rails было всё, чтобы стать лучшим фреймворком для бизнеса в мире. Элегантный, читаемый код. Популярный и интуитивный паттерн работы с базой (ActiveRecord). Понятная структура, на которой можно строить огромные приложения. Но эти достоинства ослабляются одним системным свойством: явных контрактов у Rails нет, и мажорные версии регулярно пересматривают уже выпущенное. Разберём, чему это учит.

  • Урок 1: стоимость владения на дистанции выше заявленной. «Батарейки включены, ничего не нужно делать» — но мажорные релизы требуют миграций, переходы вроде RSpec 2→3 — ручного переписывания тестов, а библиотеки между собой приходится склеивать самому. Эта работа не создаёт новой ценности для бизнеса, но стоит денег и времени.
  • Урок 2: ориентированность на новичков создаёт риск для вложившихся. Обещание «писать быстро и расширять быстро» должно работать всегда, но фича может исчезнуть вместе с решением мейнтейнера (пример: TypeScript-поддержку убирали, потом возвращали — формально «ничего не поменялось», фактически доверие пострадало). Компании, которые планируют на годы, учитывают такой риск при выборе стека.
  • Урок 3: заявленная идеология со временем пересматривается. Удаляются методы (update_attributes), меняются дефолты (belongs_to стал required), меняется семантика (errors). Каждая такая миграция — бюджет и простой, а компания получает ультиматум «перепишите часть кода — тогда получите новые фичи». Дело не в погоне за современностью: архитектура, в которой эволюция требует переписывания, вынуждает выбрасывать работающее. Бизнес ожидает иного — купленное однажды остаётся хорошим, а новое добавляется рядом.
  • Урок 4: неявные контракты и много магии. Поведение, которое нельзя увидеть чтением файла; метапрограммирование там, где оно не нужно; новая зависимость — и что-то меняется в рантайме. Для больших компаний такие риски трудно оценивать и страховать.
  • У Ruby нет перспектив по производительности (интерпретатор, GVL, память), а ядро держалось на одном человеке, который более десяти лет назад отошёл от ежедневной разработки Rails.

Показательно, что PHP и Perl живы по сей день. Сложности Rails — не столько про Ruby, сколько про практику регулярных переписываний: в какой-то момент бизнес устаёт обновлять код без прироста ценности и задаётся вопросом «может, переписать на другое?» — и вдруг оказывается, что Ruby не самый быстрый, языки поинтереснее, а «удобство написания» обернулось дорогой поддержкой. Долгосрочные обещания на такой дистанции не подтверждаются. Именно этот опыт сформировал наши правила неизменности контрактов.

Ruby как язык: та же история

У Ruby были все возможности стать удобным языком на десятилетия. На практике совместимость регулярно приносилась в жертву новым возможностям:

  • Совместимость синтаксиса нарушалась регулярно. Python прошёл через ломку один раз (2→3) и старался не повторять. Ruby менялся чаще: новые версии языка периодически несли несовместимости. Классика — разделение keyword arguments в Ruby 3.0: старый код работал, а потом перестал. Недели и бюджеты уходили на перелопачивание файлов и вылавливание ошибок — без прироста функциональности.
  • Альтернативные реализации не выжили. Rubinius и другие бэкенды стало невозможно поддерживать: язык менялся быстрее, чем реализации успевали догонять. Экосистема сузилась до единственной реализации — прямое следствие темпа несовместимых изменений.
  • Циклическая зависимость язык↔фреймворк↔гемы. Новый Rails требует новый Ruby, гемы, в свою очередь, — свою версию Ruby. Обновить одно без другого нельзя: компания получает пакет «язык + фреймворк + все гемы разом» с риском, что развалится весь стек. Это дорого и противоречит изначальному обещанию низкой стоимости входа.

Почему Crystal, а не Rust

Естественное возражение: «если уж уходить от Ruby, зачем Crystal, а не Rust?». Ответ — в уровне. Rust — язык системного уровня, и не выше. Веб-разработчик — как водитель бизнес-такси: его работа — смотреть за дорогой, выбирать маршрут, отслеживать ситуацию, обеспечивать спокойствие клиента в салоне. Rust — это управление машиной не рулём и педалями, а устройством, подключённым напрямую к двигателю: можно очень эффективно слать сигналы в мотор, но это никак не стыкуется с основной работой.

Главный тейк про агентов и «эффективность». Язык программирования — модель, придуманная людьми для людей. Технически агент может писать и LLVM IR, и сразу машинный код — и отлаживать его; такой код был бы ещё эффективнее Rust. Но такую идею почти никто не продвигает. Спрашивается: почему в погоне за эффективностью все останавливаются на Rust? Ответ отрезвляет: дело не в инженерии, а в хайпе. Эффективность генерации — не настоящий критерий. Настоящий критерий — что будет с кодом потом: насколько сложно его поддерживать, насколько размываются в нём абстракции, насколько мешают ограничения модели памяти решать задачу.

  • Бизнес-приложениям не нужны его гарантии. Постоянные проверки заимствований, явное описание тысяч типов и структур — плата за задачи, которых на прикладном уровне просто нет. Большинство приложений построены по шаблону: системный программист один раз написал и оттестировал шаблон с явными деталями — остальные берут готовое. Веб-разработчику незачем писать под каждое приложение собственный HTTP-сервер — и так индустрия жила десятилетиями, в том числе когда «безопасного» Rust ещё не существовало.
  • Системный слой на Rust — нормально. JS-рантайм уровня Bun — да, без вопросов. Но переписывать на Rust прикладной Ruby-, Python- или JS-код — сомнительная затея даже при десятикратном росте скорости: такой код труднее читать и поддерживать, а польза для большинства прикладных приложений невелика.
  • Достаточная скорость вместо абсолютной. Скорость — не фича, когда её уже хватает. Более медленные, чем Crystal, языки десятилетиями живут в продакшене. Если Crystal выполняется в ~2 раза медленнее Rust — это уровень, которого с запасом хватает практически любому веб-проекту.
  • Генерировать можно на чём угодно — проверять будет человек. В эпоху агентов скорость генерации кода перестаёт быть преимуществом языка: агент напишет и на Rust. Отличие проявляется на следующем шаге — перепроверке. Для приложений без риска код можно не читать вовсе. Для приложений с риском — финансы, медицина, персональные данные — человек в контуре нужен всегда. И вот здесь скорость генерации на Rust упирается в чтение: человек, не знающий Rust, такой код проверить не сможет — на этом шаге песочный замок и разрушается. Crystal читается почти как псевдокод: компактный, без синтаксического шума, бизнес-логика видна сразу — и понять написанное можно быстро, даже без агента. Ревью работы агента, аудит безопасности, разбор инцидента — это операции чтения, и на Crystal они дёшевы.
  • Что даёт Crystal с запасом: быстрее Ruby и Python; C-ABI — любые системные расширения на C подключаются напрямую; дешёвые fiber'ы вместо ручной работы с потоками; синтаксис уровня Ruby — читаемый, элегантный, выразительный, в том числе для спек и тестов (попробуйте написать столько же тестов на Rust и столько же приятно). Язык уже доказал уровень в проде: LavinMQ — AMQP-брокер, целиком написанный на Crystal, — обходит RabbitMQ (Erlang/OTP) по throughput при меньшем потреблении памяти. Наши собственные замеры того же рода: 142k RPS hello-world, 2.7M msg/s по вебсокетам, ~1M джобов/сек — этого с запасом хватает веб-проектам на годы вперёд.

Цена модели памяти Rust

Скорость без GC и с нулевым копированием покупается ограничениями на форму самих программ. Классика, признанная самим сообществом Rust (см. книгу-мем «Learning Rust With Entirely Too Many Linked Lists»): двусвязный список — у узла два «владельца», и владение такую конструкцию не разрешает; самоссылающиеся структуры — требуют Pin и unsafe; графы и деревья с указателем на родителя — Rc-циклы утекают, сборщика циклов нет; удержание &mut через .await — запрещено. Обходные пути всюду одни и те же: Rc<RefCell<T>>, Arc<Mutex<T>> либо arena с индексами — то есть ручная симуляция того, что GC делает бесплатно.

И главный финт: RefCell проверяет заимствование в рантайме — паникой. Язык, который продают как безопасный, реально предлагает выбор из двух: либо безопасность настолько ограничивает форму программы, что простое приходится «сначала придумать, как вообще реализовать», либо ошибки переезжают из компиляции в рантайм — и вы получаете runtime-падения как в Ruby или Python, заплатив при этом полную цену сложности Rust. Причём паника — это не исключение: в Crystal ошибка ловится обычным rescue в любом месте и обрабатывается как часть логики; паника Rust в главном потоке убивает весь процесс (exit 101), единственный перехват — catch_unwind, лазейка с UnwindSafe-механикой, задуманная для FFI и границ потоков (документация честно предупреждает, что ловит не все паники), а при panic = "abort" не ловится вовсе. Фатальное borrow_mut() у RefCell — не «исключение, которое можно поймать», а падение программы.

Реальный кейс: 18 ноября 2025 года Cloudflare пережил худший сбой с 2019 года из-за одной паники в Rust-коде. Изменение прав в ClickHouse задублировало строки в конфиг-файле Bot Management, файл превысил лимит фич — и код нового прокси FL2 вызвал .unwrap() на Err: thread fl2_worker_thread panicked. Вместо деградации одной функции запросы падали в 5xx по всей сети: почти три часа не роутился core-трафик, лежали Access, Turnstile и Workers KV (пост-мортем). Один необработанный Err в «безопасном» языке — и компания, через которую идёт заметная часть интернета, стоит три часа.

Вдогонку — комедия эпохи ИИ: в инструкции для агентов (AGENTS.md, CLAUDE.md) в Rust-проектах люди массово пишут «не используй unwrap». Эквивалент для C++ звучал бы как «дорогой ИИ, не допускай утечек памяти, не оставляй битые указатели и не забывай деструкторы». Безопасность, которую приходится прописывать в промпте, — это не свойство языка, а дисциплина: ровно то, от чего C++ страдает десятилетиями. Причём заявленное преимущество всё равно частично иллюзорно: утечка памяти в Rust легальна и безопасна (mem::forget, Rc-циклы — это не unsafe), так что и по части утечек дисциплина промпта остаётся последней линией обороны. Язык, конечно, всё ещё защищает от UB — но от маркетингового «безопасный по умолчанию» это отделяет почтительное расстояние.

Код от этого теряет читаемость, поддержка дорожает, а разработчик борется со структурой памяти, забывая про бизнес-требования. А лучший код — тот, который максимально точно отражает бизнес-требования программы. Crystal (GC и простое владение) держит фокус на предметной области, а не на борьбе с языком.

Цель CRails — не «перегнать Rust во всём», а выжать из Crystal столько пользы, чтобы не приходилось задумываться о чём-то большем.

Индустрия платит за неизменность

Сравнение с языками, которые не ломают контракты, — и их рыночная судьба:

Язык Политика Результат
Java Бинарная и исходная совместимость с 1996 года: байткод, собранный 25 лет назад, запускается на современной JVM. Устаревшие API (Vector, Hashtable) не удаляют — оставляют и добавляют новые рядом. Единственная заметная ломка — Java 9 (модульная система), и именно её enterprise запомнил как ЧП Живёт в enterprise-решениях десятилетиями. Enterprise — это всегда хорошие деньги
Go «Go 1 compatibility promise» — публичный документ-обещание с 2012 года, ~14 лет без ломающих изменений Корпоративное доверие, язык №1 для инфраструктуры
C/C++ Стандарты без ломки контракта (C89→C23, C++ «don't break my code»); C-ABI стабилен ~50 лет (подробности ниже) На C-ABI держатся все межъязыковые связки в индустрии
Rust Editions: новый синтаксис — новой редакцией рядом, старая компилируется вечно, cargo fix мигрирует автоматически Самый быстрорастущий язык, любимец корпораций
Python Один раз сломал (2→3) — экосистема раскололась, миграция длилась ~десятилетие Урок всей индустрии
Ruby Совместимость нарушалась регулярно (см. выше) Enterprise-сегмент заметно сузился

Вывод: неизменность контрактов — то, чему enterprise доверяет и платит десятилетиями. Нарушенное доверие восстанавливается очень долго, а память у рынка длинная.

Живые ископаемые: код 20–60 лет в продакшене и в прибыли

Исследование: где до сих пор работают технологии, написанные 20+ лет назад, не обновляются — и при этом держат компании на плаву, генерируя прибыль десятилетиями.

Технология Возраст Что держит сегодня Почему жива
COBOL (1959) 65+ лет По оценкам ~220 млрд строк кода; ~95% транзакций банкоматов и ~43% банковских систем США Язык не менялся: код 1980-х крутится в ядре банков и генерирует деньги прямо сейчас
IBM System/360 → z/OS (1964) 60 лет Мейнфреймы — ядро расчётов ~70% Fortune 500 Реверсивная совместимость «до 60-х built-in»: бинарь, собранный полвека назад, запускается на современной машине
MUMPS (1966) 58 лет Epic Systems — истории болезней 250+ млн пациентов США Контракт языка и данных не ломался — больницы не переписывают работающее
Sabre (1960-е) ~60 лет Бронирование авиабилетов (American Airlines и рынок) Система переписывалась вокруг, но контракт данных жил
SWIFT (1973) 50+ лет Международные банковские платежи, триллионы $ в день Протокольный контракт
SS7 (1980-е) ~40 лет Вся мировая телефония, включая 4G/5G-роуминг Протокол — замороженный контракт между операторами планеты
SMTP / DNS / BGP (1983–89) ~40 лет Почта, домены, маршрутизация — весь интернет RFC-контракты: новое добавляется, старое не ломается
zlib (1995) ~30 лет Сжатие в каждом браузере, сервере, телефоне Стабильный C-ABI, гоняется на миллиардах устройств
SQLite (2000) 25 лет Самый распространённый движок БД в мире Формат файла обратно совместим с 2004; 100% branch coverage как контракт
Apache (1995), PostgreSQL (1996) ~30 лет Половина веба старой школы, тысячи SaaS Контракты модулей/SQL не ломаются

Три вывода из этой таблицы:

  1. Это не позор, а идеал. Каждая строчка COBOL в банке — это контракт, который был соблюдён. Банк вложился один раз и получает прибыль 40 лет. Вот что значит «код — актив».
  2. Компании не обновляют — и это рационально. Обновление без ломки — опция, за которую платят охотно. Ломка — катастрофа: остановись банки/телефония/DNS хоть на день — счёт убытков шёл бы на миллиарды. «Если бы они сломались — было бы очень печально» — поэтому рынок и выбирает тех, кто не ломает.
  3. Секрет долгожительства один: неизменный контракт. Язык, ABI, протокол, формат файла, SQL — что угодно, лишь бы заморожено. Никто из «ископаемых» не выжил бы, если бы COBOL 1990-го не компилировался в 2010-м.

Именно так мы хотим, чтобы жил код на CRails: приложение, написанное сегодня, должно иметь право не переписываться 20 лет — потому что контракт фреймворка заморожен, а не «будет пересмотрен в следующем мажоре».

C и C++: как эволюционировать, не ломая (и C-ABI как клей индустрии)

Отдельный кейс — семейство C/C++, потому что оно доказывает обе вещи сразу: эволюцию без крупных ломок и стандарт-контракт, на котором выживают другие языки.

  • C стандартизуют с 1989 года (C89), стандарты выходят каждые ~10 лет (C99, C11, C17, C23) — и старый код компилируется. Показательная деталь: когда в C99 понадобились новые ключевые слова, их заворачивали в зарезервированные _-имена (_Bool, _Complex), чтобы не отбирать идентификаторы у существующего кода. Вот уровень уважения к контракту: язык изменился — ваш код не заметил.
  • C++ эволюционирует по правилу «don't break my code» — это официально главный критерий комитета при приёме предложений в стандарт. Миллиарды строк C++98 собираются современными компиляторами; даже удаление давно устаревших API (auto_ptr, trigraphs) растянулось на многостандартный цикл с годами предупреждений. При этом язык за это время получил шаблоны, lambdas, concepts, модули — рост без революций.
  • C-ABI — самый успешный межъязыковой контракт в истории. ~50 лет неизменный бинарный интерфейс вызовов — и именно на нём держатся ВСЕ связки между языками: Python/Ruby/Node-расширения, биндинги Rust/Go/Crystal, JNI и P/Invoke, системные вызовы ОС, shared libraries, плагины. Ни один язык не «делает FFI» с другим языком — все делают FFI с C-ABI.
  • Ключевой момент: этот стандарт не пересматривается каждые 5 лет — он сделан один раз, и под него подстраиваются все. Свяжка Python+C, Crystal+libvips, Node+N-API возможна ровно потому, что контракт C не дрогнул за полвека. Измени его — и отвалились бы одновременно все мосты между всеми языками мира.

Честная оговорка: у самого C++ стабильного ABI между компиляторами нет (и это его известная боль) — оттого индустрия и выбрала межъязыковым контрактом именно C-ABI. Это лишний раз подтверждает правило: побеждает не самый красивый интерфейс, а самый неизменный.

C-ABI — второй после Unix-файла пример вечного контракта из нашего обещания №6: зафиксировал один раз — под тебя подстраивается весь мир.

Экономика ломки контрактов

Отдельно зафиксируем экономику ломки контрактов. Когда компания мигрирует из-за того, что фреймворк сломал сам себя, её деньги никуда не инвестируются:

  • они не идут на развитие языка и экосистемы;
  • они не идут на развитие навыков разработчиков (переписывание update_attributes → update не учит ничему новому);
  • они не создают фич для клиентов компании — прод не двигается.

Это работа без новой ценности: недели усилий уходят только на восстановление того, что уже работало. При прочих равных рациональная компания выберет вариант без таких издержек.

Эпоха ИИ: явные контракты обязательны

AI-инструменты делают проблему не мягче, а жёстче. Когда код генерируется, люди всё реже вручную проверяют каждую строку — и без явных контрактов это создаёт ещё больший риск, что что-то отвалится прямо в продакшене.

Надёжность стека — произведение, а не сумма:

R = C × T × K
    C — компилятор (статические проверки на входе)
    T — тесты (поведенческий контракт)
    K — контракты фреймворка (неизменность API)

Произведение — потому что ноль в любом множителе обнуляет всё:

Стек C T K R
Java/Spring есть есть есть высокий
Go, Rust есть есть есть высокий
CRails есть есть есть высокий (цель)
Ruby on Rails нет (интерпретатор) есть нет близок к нулю

Умножь надёжность языка на надёжность фреймворка — и числа получаются не «чуть хуже», а на порядки хуже. В эпоху ИИ побеждают стеки с максимально явными контрактами, записанными в коде и поддерживаемыми машинно. Генерируемому коду нужны проверяемые границы, а не магия.

Есть и экономическая сторона: чтение кода становится дефицитным ресурсом. Разработчики уже тратят на него меньше времени — и будут тратить ещё меньше. Явные контракты и явные интерфейсы позволяют человеческому взгляду как можно быстрее проверить, что вообще написано в проекте: посмотрел на код — увидел интерфейс, увидел, как работает система, — и всё. Не тратятся ни токены агента на чтение всего подряд, ни время человека, не прогоняются бесполезные тесты ради выяснения того, что явный интерфейс показал бы сразу. Это прямая экономия: быстрый time-to-market и низкая стоимость поддержки — причём низкая стоимость поддержки достигается именно явными контрактами. Плюс удобная расширяемость: новое прикрепляется к живому старому интерфейсу, а не вместо него.

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

Отдельно про ответственность. Каким бы хорошим ни был ИИ, компании заключают контракты не с агентами, а с другими компаниями, где работают люди, — и за готовый результат отвечают люди. Пока не произойдёт сдвиг, при котором контракты заключаются напрямую с ИИ и ИИ несёт ответственность за результат, человек остаётся в контуре: кто-то должен проверить, понять и подписаться под тем, что сделал агент. А значит, остаётся востребованным и этот подход — код, который человек может быстро прочитать и проверить, и контракты, которые не меняются под ногами.

И последнее: репутация складывается из соблюдённых обещаний. Стек с историей ломких обновнений проигрывает сравнение ещё до его начала — поэтому CRails намерен зарабатывать доверие соблюдёнными контрактами.

Обещания CRails (контракт фреймворка с вами)

  1. Контракты вечны. Контракт, замороженный в момент релиза, работает всегда. Никаких «перепишите на новый API» — никогда.
  2. Безопасность фиксируется во всех живых версиях. Дыра закрывается в вашем контракте, а не «в следующем мажоре — обновитесь».
  3. Новые возможности — новые контракты рядом. Решили по-новому делать вебсокеты или джобы? Добавляем cable_v2, jobs_v2. Старый контракт продолжает работать без изменений.
  4. Миграция — за плюшками, а не под угрозой. Версия объявляется по каждому контракту отдельно. Обновляйте то, что хотите, когда хотите. Можно не обновлять ничего — вечно.
  5. Батарейки включены — как в Rails. Единственное отличие: новые батарейки не ломают старые.
  6. Эталон — файл в Unix. Интерфейс файлового дескриптора работает сорок+ лет с любым устройством. Наши контракты обязаны жить так же: десятилетиями, а не «год-два — и новый бюджет на обновление».
  7. Ваш код — актив. Мы не ставим компанию перед выбором «удалить половину кода ради современных фич». Мы с уважением относимся к любому человеку и любому бизнесу, выбравшему этот фреймворк.
  8. Явность вместо магии. Метапрограммирование — только под капотом фреймворка, скрыто в нём самом. Всё, что трогает пользователь, максимально явно: читается просмотром файла, проверяется тестом — и не меняется после релиза. Код от этого не сильно пострадает, а поддержка — станет существенно лучше.
  9. Нет циклических зависимостей. Обновление одного контракта никогда не требует одновременно обновить язык, тулчейн или другие контракты. Радиус обновления — ровно тот, который вы сами выбрали.
  10. Компоненты не зависят друг от друга. Если action_mailer, action_pack, action_text, action_view, active_job, active_record жёстко зависят друг от друга — это не разные компоненты, а один. Правильная разбивка: каждый компонент обновляется отдельно, и можно собирать конструктор из разных версий библиотек («солянку») — это нормальное поведение во всех остальных технологиях. Обновление библиотеки джобов не тащит за собой библиотеку писем. Мы не боремся за красивые заголовки «мы обновили абсолютно всё».

Чем обещание обеспечено (механика, не честное слово)

  • Контракт = именованная версионированная поверхность API + замороженный корпус oracle-спеков (тесты 1:1 с оригиналами, см. AGENTS.md).
  • CI-правило: каждый релиз проходит спеки всех когда-либо выпущенных версий контрактов. Сломал старый контракт — релиз не выходит.
  • Эволюция только аддитивная: добавить метод можно, удалить или перевернуть дефолт — нельзя. Новый путь = новая версия контракта (V2 рядом с V1); в компилируемом языке второй фасад стоит ноль рантайма — в бинарь попадает только то, что приложение использует.
  • Депрекация — compile-time метка с указанием нового пути, не удаление.
  • Компоненты (шарды) версионируются независимо. Взаимодействие между ними — только через замороженные контракты (например, mailer ставит джобы в очередь через контракт active_job, а не лезет в его внутренности) и всегда опционально: нет зависимости — компонент работает один. CI собирает «солянку»: матрицу из разных выпущенных версий компонентов, чтобы комбинации никогда не отваливались.

Путь для Rails-приложений

Мы не отказываемся от Rails-пути: ActiveRecord-паттерн, контракты джобов, convention over configuration — всё это сохраняется. API 1:1 (единственное отличие неймспейса — CRails) нужен, чтобы Rails-приложение переносилось почти дословно. Но дальше — движение к более чистой архитектуре: контракты продумываются до релиза и не ломаются после. Этот путь мы начинаем заново, с первых версий фреймворка.

Сравнение

Ruby on Rails CRails
Контракт API не формализован, пересматривается в мажорах заморожен в момент релиза, вечен
Новые фичи часто требуют миграции всего фреймворка новый контракт рядом, старый не трогаем
Обновление атомарное: всё и сразу по одному контракту, опционально
Зависимость язык↔фреймворк циклическая: новый Rails требует новый Ruby, гемы — свой минимум обновление контракта не тянет за собой ничего
Компоненты связаны: мажор обновляет всё сразу, взять одно нельзя независимы: солянка из любых выпущенных версий
Ломающее изменение оправдывается changelog'ом невозможно: CI-матрица не выпустит релиз
Остаться на старом боль (гемы, безопасность, EOL) валидный путь навсегда
Производительность потолок Ruby (интерпретатор, GVL, память) Crystal: fiber'ы, один бинарь — см. замер ниже

Производительность: базовая Hello-страница (без БД), head-to-head

Два минимальных приложения — роут / → контроллер → render plain: "Hello", никакой базы, шаблонов и ORM. Оба в production-режиме, одна машина, wrk -t4 -c100 -d15s после warm-up. CRails — crystal build --release (нативный бинарь 5.4 MB, полный стек middleware: request-id, secure headers, логирование). Rails 8.1.4 — дефолтный Puma 8.0.2 (1 процесс × 3 потока), Ruby 3.4.5. Воспроизведение: ~/crails_compare/bench.sh, полный отчёт: ~/crails_compare/RESULTS.md.

Метрика CRails 0.1.0 (Crystal 1.21) Rails 8.1.4 (Ruby 3.4.5)
RPS ~50 000–52 000 ~390–470
Latency (avg) ~2 ms 211–256 ms
RSS idle 11 MB 99–100 MB
RSS под нагрузкой (peak) 17 MB 101–102 MB
Socket errors / non-2xx 0 0

Разгон больше чем в 100 раз по RPS при в 6–9 раз меньшей памяти (два независимых прогона, воспроизводимо). Rails упирается в 3 потока Puma по умолчанию: latency 200+ ms — это очередь на соединениях. Больше воркеров/потоков поднимут RPS, но ценой кратного роста RAM (~100 MB на процесс и так против 11–17 MB на всё приложение у CRails).

Что это

Полный клон API Ruby on Rails на Crystal, максимально 1:1 по конечному интерфейсу — чтобы разработчик, перешедший с Ruby/Rails, не почувствовал разницы. Цель: код Rails-приложения переносится на Crystal почти дословно.

Единственное отличие неймспейса: CRails вместо Rails.

  • CRails.application, CRails.env, CRails.root, CRails.logger — так, как в Rails было Rails.*.
  • ActiveRecord::Base, ActiveSupport::Concern, ActionController::Base, ActionView::FormBuilder, ActionCable::Channel::Base, ActiveJob::Base, ActiveStorage::Blob, ActionMailer::Base, ActionText, ActionMailbox — имена модулей и API 1:1.
  • ERB → ECR (единственная видимая замена, язык-то другой).
  • Команды: crails вместо rails (crails new, crails server, crails generate, crails console, crails routes, crails db:migrate).

Зачем

Эксперимент для замера монолитной архитектуры Rails MVC на Crystal:

  • сколько пользователей (RPS, одновременных соединений) выдержит монолит;
  • сколько джобов в секунду выполнит (ActiveJob-совместимый воркер);
  • сколько вебсокетов сможет держать (ActionCable-совместимый сервер);
  • сколько при этом съест памяти, как быстро стартует.

Возможно это «новый король стартапов», а возможно «полный провал» — узнаем цифрами, не эмоциями. Бенчмарки: bench/.

Исследование переносимости API (по реальному коду Rails 8.2): ~85–90% ежедневного API переносится с ощущением 1:1 — полный отчёт: /home/oleg/egui.cr/plans/crails-api-research.md.

Структура (монорепо шардов)

Папка Порт Статус
active_support.cr/ ActiveSupport Iteration 1 ✅ core_ext P0 + Duration + дата-математика + Concern/delegate/Notifications + Cache + I18n + NumberHelper (226 спеков, оракул 1:1 с гемом 8.1.4)
active_record.cr/ ActiveRecord Iteration 2 ✅ CRUD-запись + dirty + валидации + коллбэки + ассоциации + миграции + crails db:* + db/schema.cr (дамп 1:1 с schema.rb-оракулом, schema:load round-trip) (283 спека; оракулы 1:1 с гемом 8.1.4: 78 кейсов записи + дампы схемы миграций) + SQLite-адаптер ✅ (crystal-sqlite3; Rails 8 pragmas: WAL/synchronous=NORMAL/busy_timeout/foreign_keys; FTS5; интроспекция схемы через PRAGMA table_info → schema.yml; оракул-спеки против sqlite3 CLI, prerequisite для Campfire-бенчмарка)
action_pack.cr/ ActionPack (контроллеры, роутинг, params) Iteration 3 ✅ strong params, cookies/session/flash (+signed/encrypted), rescue_from, respond_to; + CSRF, rate limiting, send_file/send_data, ActionController::Live/SSE, typed params + OpenAPI codegen
action_view.cr/ ActionView (хэлперы, ECR, форм-билдер) Iteration 3 ✅ layouts/partials/collection-render, хэлперы, output-safety, FormBuilder; + CSRF-токены форм, vite-хелперы
action_mailer.cr/ ActionMailer P0 ✅ (deliver_later, SMTP-диалог; P1: .with/observers/previews)
action_mailbox.cr/ ActionMailbox P0 ✅ (InboundEmail, Router, lifecycle; P1: ingress/incinerate)
action_text.cr/ ActionText P0 ✅ (RichText, sanitize, trix-layout; P1: аттачменты/sgid)
action_cable.cr/ ActionCable Iteration 4 ✅ channels/стримы/протокол, ws-слой /cable; + stream_for/broadcast_to, pubsub PG/Redis (Iteration 7)
active_job.cr/ ActiveJob Iteration 4 ✅ perform_later/now, retry/discard; + адаптеры PG (solid_queue, recurring) / Redis / Kiq (Iteration 7, 20)
action_bot.cr/ ActionBot (боты/агенты — не порт, дифференциатор) Iteration 16 P0 ✅ единый контракт сообщений + Telegram/WhatsApp адаптеры, вебхуки/поллер/SSE-стриминг, crails generate bot (оракул с telegram-bot-ruby 2.8.1; P1: conversations/Slack, P2: Brain-LLM)
active_storage.cr/ ActiveStorage P0 ✅ (Disk-сервис, Blob/Attachment, purge; P1: variants/S3/Mirror)
active_backup.cr/ ActiveBackup (бэкапы БД/файлов — дифференциатор) Iteration 14 P0 ✅ tar.gz-архивы (GNU-tar-совместимый ustar), pg_dump -Fc / pg_restore, сторы-порт (Local/Memory), retention keep_last/keep_days, BackupJob через recurring; restore-тест в CI (18/18)
turbo.cr/ Turbo Rails (порт гема) Iteration 19 ✅ Turbo Streams (7 действий + refresh + кастомные), Broadcastable, view-хелперы, http-формат turbo-stream в action_pack; оракул 60 кейсов с turbo-rails 2.0.20, бенч 401 (fanout ×10 до 18.5M deliveries/s)
importmap.cr/ importmap-rails (порт гема) Iteration 17 ✅ pin/pin_all_from + importmap-теги, /javascripts/* через Static; оракул 21 кейс с гема 2.2.3, байты байт-в-байт (17/17), бенч 204 (~11.6×)
active_agent.cr/ ActiveAgent (ИИ-агенты — не порт, дифференциатор) P0 ✅ агентский цикл LLM+инструменты, module-порты Provider/Tool, compile-time JSON Schema тулзов, провайдеры openai/zai/kimi/deepseek/lmstudio/vllm/ollama; wire-оракул 6 кейсов, 21/21 (P2: Anthropic, мосты, HITL)
action_mcp.cr/ ActionMCP (приложение как MCP-сервер — порт wire Ruby SDK mcp) Iteration 18 ✅ Streamable HTTP /mcp (initialize/tools/call), экспорт Tool/агентов как MCP-инструментов, аутентификация Bearer + квоты + биллинг-учёт; оракул 28 кейсов, сюита 41/41
railties.cr/ Railties + CLI crails Iteration 0 + console (icr.cr) + db:* + middleware-стек (порт MiddlewareStack) + credentials + CSP/secure headers + observability (/up, /metrics, OTel-трейсы, JSON-логи) + роли worker/scheduler + crails supervise + crails deploy + Static/Vite-dev-прокси
devise.cr/ Devise (порт гема) ✅ модельный слой (26/26 оракул, JWT-ротация) + HTTP: роуты/контроллеры/вьюхи (Sessions/Registrations/Passwords/Confirmations/Unlocks/OmniAuth, оракул 74 кейса) + GitHub-провайдер + test-helpers + i18n; сюита 145/145
kiq.cr/ Sidekiq → Kiq (порт гема, Kiq/kiq_options) + CLI bin/kiq P0 ✅ (29/29 оракул с sidekiq 8.1.7, wire-совместимые пейлоады — интероп с настоящим sidekiq в обе стороны; ~26k jobs/s vs 1.4k у sidekiq) + Web UI ✅ (Kiq::Web — порт Sidekiq::Web, оракул 94/94)
live_view.cr/ Phoenix LiveView (серверная часть) Iteration 8 P0 ✅ (диффы, handle_event; P1: live_component/uploads)
hello_world/ первая итерация — Hello World на CRails ✅ работает
rails_hello/ интеграционное приложение полного стека ✅ HTTP-оракул 9/9 против Rails 8.1.4
demo/ демо-приложение (CRails Notes) ✅ работает
bench/ сценарии замеров монолита 20 отчётов: hello/ar (000, 100–101), jobs (200–203), assets (204–205), full-page/ws (300–310), liveview/turbo/sse (400–410), observability/middleware (500–501)
assets/ иконка: сине-голубой алмаз Crystal есть

Имена шардов в shard.yml — без .cr (active_record, action_pack, …); .cr в именах папок — чтобы не конфликтовать с исходниками.

Сборка и запуск

rake            # build всего: deps шардов, crails CLI, бинарь приложения
./bin/server    # сервер демо-приложения (-p PORT, -e ENV)
./bin/console   # rails-console (config/database.yml, DATABASE_URL перекрывает)
rake spec       # все тест-сьюты шардов

Дорожная карта

Iteration 0 (Hello World) — ✅ сделана (2026-10-02): crails new → crails server → curl / → 200; CLI new/server/routes/watch; цифры в bench/results/000-hello.md (hello RPS ~142k против Rails 8.1+Puma ~233, RSS 16 MB против 110 MB; актуальный head-to-head с полным стеком production-middleware — в разделе «Сравнение» выше: ~50k против ~400–470 RPS, 17 MB против 102 MB). Приложение: hello_world/hello/ (сгенерировано crails new, пересоздаётся с нуля за ~2 s компиляции).

Актуальное состояние — PLAN.md в корне (детали по каждой итерации — там и в PLAN.md шардов):

  • Фаза 1 (порт ядра) — закрыта: Iterations 0–6 — active_support, active_record (+SQLite-адаптер), action_pack/action_view (полный MVC-контур), active_job + action_cable, хвост стека (mailer/storage/ text/mailbox), devise + JWT, rails_hello с HTTP-оракулом против Rails 8.1.4; live_view P0.
  • Фаза 2 (продакшен-готовность) — почти закрыта: Iteration 7 (PG-очереди/кэш/pubsub, Redis — опция) ✅; Iteration 11 (observability + deploy: /up, /metrics, OTel, graceful shutdown, crails deploy) ✅; Iteration 12 (безопасность: rate limiting, secure headers/CSP, CSRF, cookies.signed/encrypted, credentials, AR-encryption, HTTP-caching) — осталось: sandboxed-миграции, crails check; Iteration 13 (Vite P0+P1: manifest, Static, dev-прокси) — остался --vite-флаг; Iteration 14 (send_file/ send_data) ✅; Iteration 15 (supervision: роли worker/scheduler, crails supervise с backoff) — осталось: memory watchdog, bench отказов.
  • Фаза 3 (дифференциаторы) — идёт: turbo.cr (Turbo Streams) ✅, kiq.cr (порт Sidekiq + Web UI + CLI) ✅, importmap.cr ✅, action_mcp.cr (приложение как MCP-сервер) ✅, streaming-first (ActionController::Live/SSE, ~15× vs Rails+Puma) ✅, typed params + OpenAPI codegen ✅; live_view, action_bot, active_agent, active_backup — P0 готов. Осталось: порт ActiveModel (высветление из active_record), DX-итерация (hot-reload шаблонов, error pages), P1/P2-хвосты.

Приходящим из Ruby: практические отличия семантики и типов (nilable-файндеры, type narrowing, кейворды vs Hash, виртуальные атрибуты) — в SEMANTICS.md в корне.

License

MIT (когда будет что лицензировать).

Repository

crails

Owner
Statistic
  • 0
  • 0
  • 0
  • 0
  • 0
  • about 1 hour ago
  • October 2, 2026
License

Links
Synced at

Wed, 14 Oct 2026 12:49:42 GMT

Languages