Авторы: Денис Редозубов, Екатерина Галкина

Сегодня мы расскажем, почему мы пишем фронтенд на Haskell и компилируем его на JavaScript. Собственно такой процесс и называется транспиляцией:

Транспиляция - это процесс преобразования программы, написанной на языке X, в эквивалентную программу на языке Y. В отличие от компиляции, языки X и Y имеют примерно одинаковый уровень абстракции.

Зачем нужна транспиляция?

В целом транспиляция может служить следующим основным целям:

  1. Миграция между разными версиями одного и того же языка. Языки программирования не стоят на месте. Они активно развиваются и с каждой новой версией приобретают новые удобные и привлекательные функции. К сожалению, может случиться так, что новые языковые возможности поддерживаются не сразу и не везде, поэтому возникает проблема обратной совместимости версий. В этом случае такой транспилятор от версии к версии выполняет своего рода десугарирование выражения в более старые и обычно менее выразительные версии. Babel - пример транспилятора, переводящего JS-код в его подмножество, поддерживаемое браузерами. Обратное преобразование также возможно, когда необходимо перевести проект на более новую версию языка, но у вас мало времени, чтобы сделать это вручную. Например, вы можете использовать 2to3 для преобразования кода Python 2.x в Python 3.
  2. Перевод с одного языка программирования на другой в зависимости от требований к исполняющей системе и / или пожеланий разработчиков. Например, для работы в браузере требуется код на JS (который сегодня используется чаще всего) или WASM ( что в настоящее время менее распространено). С другой стороны, разработка должна соответствовать другим требованиям, которые лучше выполнить на другом языке. Этот исходный язык может поддерживать уникальные механизмы, такие как автоматическое распараллеливание, или относиться к совершенно другой парадигме. Код, сгенерированный транспиляторами, может либо выглядеть почти идентично исходному коду (что упрощает отладку), либо трансформироваться до неузнаваемости по сравнению с исходным кодом. Существуют утилиты, позволяющие сопоставить перенесенный код с исходным кодом (например, SourceMap для JS).

Приведем несколько примеров:

  1. Языки, использованные для разработки веб-интерфейса и переведенные на JS:
  • TypeScript - это надмножество JavaScript с необязательными аннотациями типов, проверяемыми во время транспиляции.
  • CoffeeScript - более выразительный - по сравнению с JS - язык, дополненный синтаксическим сахаром в стиле Python и Haskell.
  • Elm - это чисто функциональный язык, который имеет статическую типизацию (и в целом очень похож на Haskell) и позволяет создавать веб-приложения в декларативном стиле под названием The Elm Architecture (TEA).
  • PureScript также является чисто функциональным языком со статической типизацией и синтаксисом, подобным Haskell.
  • ClojureScript - это расширение языка Clojure (который, в свою очередь, является диалектом Лиспа), используемым для веб-программирования на стороне клиента.

2. Языки описания оборудования:

  • Bluespec - это высокоуровневый язык описания оборудования, изначально возникший как расширение Haskell и перенесенный в Verilog.
  • Clash также функционален и использует синтаксис, подобный Haskell, генерирует VHDL, Verilog или SystemVerilog.
  • Verilator, в отличие от двух предыдущих языков, работает иначе, конвертируя подмножество Verilog в C ++ или SystemC.

3. Транспилеры языков ассемблера для разных архитектур или разных процессоров в одной архитектурной системе (например, между 16-битным Intel 8086 и 8-битным Intel 8080).

Почему бы не разрабатывать на чистом JS?

Как видно из приведенных выше примеров, обсуждение транспиляции в целом неизбежно затрагивает тему перевода на JS. Рассмотрим подробнее его цели и потенциальные преимущества:

  • Транспиляция в JS позволяет запускать приложение в веб-браузерах.
  • Разработчики используют те же инструменты, что и для внутренней разработки, поэтому вам не нужно изучать другие библиотечные инфраструктуры, менеджеры пакетов, линтеры и т. Д.
  • Становится возможным использовать язык программирования, который больше соответствует предпочтениям команды и требованиям проекта. Вы также можете получить такие механизмы, как строгая статическая типизация, чуждая классическому стеку внешнего интерфейса.
  • Логика, общая для внешнего и внутреннего интерфейса, может быть организована отдельно и повторно использована. Например, подсчет общей стоимости заказа может оказаться нетривиальной задачей из-за специфики предметной области. На стороне клиента необходимо отображать общую стоимость заказа, а при обработке запроса сервера все нужно перепроверять и заново пересчитывать. Вы можете написать бизнес-логику, используемую для расчета общей стоимости заказа, только один раз на одном языке и использовать ее в обоих случаях.
  • Используются механизмы генерации кода и дженерики, которые позволяют убедиться, что сериализация и десериализация JSON или даже двоичное представление будут работать без сбоев. Мы использовали этот подход, чтобы ускорить синтаксический анализ запросов, требующих большого объема обработки, что повысило производительность в ряде ситуаций.
  • Упрощается процесс отслеживания совместимости API между клиентом и сервером. Когда клиентское и серверное приложения развертываются синхронно и кеши браузера используются правильно, не должно быть проблем несовместимости, которые могут возникнуть во время асинхронного развертывания. Например, если одна часть приложения обращается к другой части с помощью API, и API изменяется, есть шанс забыть об изменениях на стороне клиента и потерять параметры запроса или отправить тело запроса в недопустимом формате. Этого можно избежать, если клиентское приложение написано на том же языке. В идеале приложение даже не компилируется, если клиентская функция не соответствует текущей версии API.
  • Разработчики с одинаковыми навыками участвуют как в бэкэнд-, так и в фронтенд-задачах, что дает командам дополнительную организационную гибкость и улучшает фактор шины. Таким образом становится проще назначать задачи и загружать каждого члена команды. Это также важно, когда требуется срочное исправление - наименее занятый член команды берет на себя задачу независимо от того, к какой части проекта она относится. Один и тот же человек может исправить проверку поля во внешнем интерфейсе, запрос БД и логику обработчика на сервере.

Наш опыт с JS-транспиляцией

Мы выбрали инструменты фронтенд-разработки с учетом следующих факторов:

  • Мы хотели использовать язык со строгой статической типизацией.
  • У нас уже была довольно большая база кода для бэкэнда Haskell.
  • Большинство наших сотрудников имеют значительный опыт коммерческой разработки на Haskell.
  • Мы хотели воспользоваться преимуществами одного стека.

В настоящее время здесь, в Typeable, мы разрабатываем интерфейс на Haskell и используем веб-фреймворк Reflex и функциональное реактивное программирование (FRP). Исходный код на Haskell транслируется в код JavaScript с помощью GHCJS.

TypeScript и другие расширения JS не подходят для нас, поскольку они предлагают более слабую типизацию, а их система типов недостаточно развита по сравнению с Haskell. В целом эти языки слишком сильно отличаются от тех, к которым наша команда привыкла.

Мы выбрали Reflex вместо таких альтернатив, как Elm и PureScript - в первую очередь потому, что мы хотели использовать тот же стек разработки, что и для бэкэнда. Более того, Reflex избавляет вас от необходимости следовать определенной архитектуре приложения и, в некоторой степени, является более гибким и низкоуровневым. Подробное сравнение Elm и Reflex можно найти в нашем посте по этой теме.

Выводы

Мы смогли воспользоваться преимуществами JS-транспиляции, о которых говорилось выше:

  • Все части проекта разрабатываются с использованием одного и того же стека, а члены команды - универсальные программисты.
  • Упрощенно структура проекта состоит из ряда пакетов: описание API, описание бизнес-логики, бэкэнд и интерфейс. Первые два пакета - это части, общие для внешнего и внутреннего интерфейса, при этом основная часть кода используется повторно.
  • Мы используем servant библиотеку, которая позволяет нам описывать API на уровне типов и проверять во время компиляции, используют ли и серверные обработчики, и клиентские функции правильные параметры требуемых типов и соответствуют ли текущей версии API (если вы забыли сменить клиента функция на интерфейсе, он просто не будет построен).
  • Функции сериализации и десериализации JSON, CSV, двоичное представление и т. Д. Генерируются автоматически и одинаково во внутреннем и внешнем интерфейсе. Об уровне API почти не нужно думать.

Конечно, есть сложности:

  • Вам все равно придется использовать чистый JS FFI для работы с внешними надстройками.
  • Отладка усложняется, особенно в пошаговом режиме. Однако это необходимо в очень редких случаях; большинство ошибок обнаруживается в логике реализации.
  • Доступно меньше документации по сравнению с JS-фреймворками.