Yesod и Haste: мёд и дёготь

Приветствую!

Сегодняшняя короткая заметка посвящена моему знакомству с Haste и попытке связать его с Yesod. К сожалению, кроме мёда я нашёл и дёготь.

Подготовка

Haste - это Haskell-диалект, заточенный специально под веб-разработку. В него заложены довольно-таки интересные идеи, и одна из них - попытка отказа от нативного JavaScript. Haste позволяет писать настоящий Haskell-код, компилирующийся в .js-файлы. И это не могло не привлечь моё внимание: я захотел связать этот инструмент с моим Yesod-проектом с целью замены .julius-шаблонов Haskell-кодом.

Установка Haste тривиальна, ведь, в отличие от того же PureScript, Haste не требует никаких JS-зависимостей:

$ cabal install haste-compiler
$ haste-boot

Минут через десять установка завершится. Теперь мне доступна команда hastec, это и есть наш компилятор. Его использование в общем и целом таково:

$ haste Form.hs -o Form.js

Form.hs - исходный Haste-код, Form.js - сгенерированный JS-код. Всё предельно просто.

И вот какой была моя задумка: беру Yesod-виджет (состоящий, как вы помните, из трёх шаблонных файлов) и заменяю .julius-шаблон Haste-файлом. Например, для формы регистрации нового пользователя было вот так:

templates/
    home/
        register/
            form/
                form.hamlet
                form.lucius
                form.julius

а стало вот так:

templates/
    home/
        register/
            form/
                form.hamlet
                form.lucius
                Form.hs

Файл назван с большой буквы по привычке, чтобы было единство с остальными .hs-файлами. Затем я выполняю вот такую команду:

$ hastec templates/home/register/form/Form.hs -o static/js/Form.js

В результате в каталоге static/js/ появится файл Form.js. После этого мне останется лишь подключить его к проекту:

-- Foundation.hs
...
    pageContent <- widgetToPageContent $ do
        ...
        -- Локальные стили и скрипты.
        addScript $     StaticR js_default_js
        addStylesheet $ StaticR css_default_css
        addScript $     StaticR js_Form_js
...

Кстати, после добавления нового статического скрипта не забываем выполнить команду touch Settings/StaticFiles.hs, иначе проект не соберётся.

Помимо прочего, компилятор hastec позволяет оптимизировать генерируемый JS-код. Выполняем:

$ hastec --help

и читаем, там всё понятно изложено. К сожалению, получить production-оптимизацию “из коробки” не получится:

$ hastec --opt-all templates/home/register/form/Form.hs -o static/js/Form.js
No Java runtime present, requesting install.
hastec: user error (Couldn't execute Google Closure compiler: Command 'java' failed with error  code 1)

Google Closure compiler требует наличия JDK.

Вот, собственно, и всё. Теперь заглянем внутрь.

Мёд

Вот содержимое файла Form.hs:

import Haste
import Data.Maybe

main = do
    createProfileButton <- elemById "createProfileButton"
    setClass (fromJust createProfileButton) "disabled" True

Красиво и понятно: мы получаем доступ к кнопке #createProfileButton и делаем её неактивной с помощью Bootstrap-класса .disabled.

Как видите, это настоящий Haskell-код, и это не может не радовать. В модуле Haste.DOM присутствует множество стандартных обвязок для нативных JS-функций, лично я сориентировался весьма быстро.

Какое же главное преимущество мы получаем, используя Haste? Мы получаем compile-time проверку! Больше никаких проблем с опечатками в именах JS-функций, никаких ошибок с несимметричными круглыми/фигурными скобками, никакой путаницы с количеством аргументов у функций. Теперь подобные ошибки будут отлавливаться на стадии компиляции, а про нагромождение JS-скобок вообще можно забыть.

Казалось бы, ура! Правда? Эх, я бы очень хотел сказать “да”, но, помимо мёда, есть и дёготь.

Дёготь

Да, и к сожалению, его довольно много.

Во-первых, Haste-код не является частью Yesod-проекта. Да, мы запихнули Haste-файлы в наши виджеты, но в действительности они не стали частью виджетов. Ведь мы вынуждены компилировать Haste-файлы компилятором hastec, который не имеет ни малейшего представления о Yesod. Следовательно, мы должны организовать некий отдельный шаг сборки проекта, например скрипт, который будет пробегать по всем Haste-файлам и компилировать их, складывая результаты в каталог static/js/.

Во-вторых, поскольку Haste-код не является частью Yesod-проекта, мы не можем использовать в нём наши любимые type-safe URL-ы. И это очень обидно. Более того, мы при всём желании не смогли бы интегрировать type-safe URL-ы в Haste-код, ведь hastec не поддерживает Template Haskell.

И в-третьих, избавиться от ручного написания JS-кода нам вряд ли удастся, и это хуже всего.

В реальном проекте мы едва ли будем писать весь JS-код с нуля. Скорее всего, мы захотим использовать какие-то готовые библиотеки, типа JQuery и тому подобных. К сожалению, перевести такой библиотечный код в Haste-код невозможно, ведь разработчики Haste думали только о чистом JavaScript. Разумеется, мы можем интегрировать чистый JS-код в Haste-код с использованием FFI-механизма. Но это сводит к нулю главный плюс Haste - избавление от необходимости писать JS-код вручную.

Более того, даже если мы напишем FFI-JS обёртку, наподобие такой:

getText :: JQuery -> IO String
getText = ffi "(function (DOMElement) { return DOMElement.text(); })"

мы потеряем и второй плюс Haste, а именно проверку JS-кода на стадии компиляции. Как видите, JS-функция записана в виде обычной строки, и это не имеет никаких преимуществ перед тем, как если бы мы вернулись к сырым .js-файлам. В частности, взгляните на этот FFI-пример. Мало того, что мы пишем-таки JS-код, так ещё и создаём дополнительный .js-файл.

Выводы

Да, я считаю прекрасной саму идею замены JavaScript на Haskell. И если вы не собираетесь использовать в вашем Yesod-проекте много библиотечного JS-кода, то Haste весьма уместен. В самом деле, весь нативный JS-код можно заменить на Haste-код, а немногочисленные библиотечные функции обернуть в FFI. Конечно, у вас в любом случае не будет type-safe URL, но делать нечего.

Однако, если вы собираетесь использовать много библиотечного JS-кода, Haste, на мой взгляд, только усложнит проект. Зачем оборачивать в FFI кучу функций из какого-нибудь JQuery Validation? Нет, игра явно не стоит свеч. Впрочем, решать вам.