Pull to refresh
0
Typeable
Functional programming, High assurance, Consulting

Зачем мы транспилируем Haskell в JavaScript

Reading time6 min
Views6.5K

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


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


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


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


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

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


  • Языки для фронтенд-разработки, транслируются в JS:

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

        — Bluespec — высокоуровневый функциональный язык описания аппаратуры, изначально был расширением Haskell, транспилируется в Verilog.
        — Clash — также функциональный, с похожим на Haskell синтаксисом, генерирует код на VHDL, Verilog или SystemVerilog.
        — Verilator — в отличие от предыдущих двух, работает в другую сторону и преобразует подмножество Verilog в C++ или SystemC.
  • Транспиляторы языков ассемблера для различных архитектур или под разные процессоры из одной системы архитектур (например, между 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 для работы с внешними плагинами.
  • Отладка становится сложнее, особенно пошаговый режим. Однако такое требуется крайне редко, большинство ошибок оказывается в логике реализации.
  • Меньшее количество документации по сравнению с фреймворками на JS.

Соавтор: Катерина Галкина


Другие интересные статьи в нашем блоге:


  1. Сильные стороны функционального программирования
  2. Как мы выбираем языки программирования в Typeable
  3. Сравнение Elm и Reflex
  4. Создаем веб-приложение на Haskell с использованием Reflex. Часть 1

Версия на английском языке: https://typeable.io/blog/2021-04-05-js-transpilation.html

Tags:
Hubs:
+14
Comments13

Articles

Change theme settings

Information

Website
typeable.io
Registered
Founded
Employees
11–30 employees
Location
Россия