Yesod + Fay

Проблема

Во время разработки front-end вообще и использования фреймворка Yesod в частности приходится иметь дело с JavaScript. Разумеется, пока. Дело в том, что для работы с JS рекомендуется использовать Shakespearean Template. А значит, куски JS будут лежать либо в файлах с расширением .julius, либо в Template Haskell. Проблема удобства разработки встаёт тут же. Yesod предоставляет живую перезагрузку кода. Тем самым, цикл разработки становится следующим:

  • написание кода;
  • сохранение изменений;
  • автоматическая перекомпиляция изменений (если изменялись шаблоны hamlet или обработчики);
  • создание нового js файла по адресу static/tmp/autogen-*.js;
  • перезагрузка страницы в браузере;
  • тестирование изменений.

А значит, если у нас в коде ошибка, то мы отловим её в runtime. Вдобавок, сказывается отсутствие REPL для JS. Если поднять cabal repl, то из ghci нереально будет проверить JS-код.

Что же делать? Как сместить баги из runtime в compile-time? Как добавить в ужас JS немного статической типизации?

Способы решения

На эти вопросы уже есть ответы.

GHCJS, Elm - судя по отзывам, превосходные вещи. Однако мне не довелось ещё соприкоснуться с ними. У Сноймана были какие-то мысли на их счёт, но я не вдавался в подробности и не могу ничего сказать на сей счёт конкретного.

Fay

Fay - это подмножество Haskell, которое можно успешно скомпилировать в валидный JS код. Среди возможностей Fay отмечают:

  • статическая типизация;
  • лень;
  • чистота;
  • базовые типы, с которыми коррелирует JS + алгебраические типы данных;
  • автоматическое преобразование данных из/в JSON.

Как можно заметить из их wiki, он успешно интегрирован во многие вэб-фреймворки. Но остановимся на Yesod.

Yesod + Fay

Создадим обычный проект на yesod:

$ yesod init 
Welcome to the Yesod scaffolder.
I'm going to be creating a skeleton Yesod project for you.

What do you want to call your project? We'll use this for the cabal name.

Project name: faytest
Yesod uses Persistent for its (you guessed it) persistence layer.
This tool will build in either SQLite or PostgreSQL or MongoDB support for you.
We recommend starting with SQLite: it has no dependencies.

    s      = sqlite
    p      = postgresql
    pf     = postgresql + Fay (experimental)
    mongo  = mongodb
    mysql  = MySQL
    simple = no database, no auth
    mini   = bare bones, the "Hello World" of multi-file Yesod apps
             (Note: not configured to work with yesod devel)
    url    = Let me specify URL containing a site (advanced)

So, what'll it be? pf
That's it! I'm creating your files now...

---------------------------------------

                     ___
                            {-)   |\
                       [m,].-"-.   /
      [][__][__]         \(/\__/\)/
      [__][__][__][__]~~~~  |  |
      [][__][__][__][__][] /   |
      [__][__][__][__][__]| /| |
      [][__][__][__][__][]| || |  ~~~~
  ejm [__][__][__][__][__]__,__,  \__/


---------------------------------------

The foundation for your web application has been built.


There are a lot of resources to help you use Yesod.
Start with the book: http://www.yesodweb.com/book
Take part in the community: http://yesodweb.com/page/community


It's highly recommended to follow the quick start guide for
installing Yesod: http://www.yesodweb.com/page/quickstart

If your system is already configured correctly, please run:

    cd faytest && stack build && stack exec -- yesod devel

Выберем pf

Тут следует отметить, что для работы с PostgreSQL, необходимо, чтобы были разрешены следующие зависимости:

  • во-первых, сама СУБД: postgresql-9.4
  • во-вторых, нужные для работы пакетов библиотеки: libpq-dev;
  • в-третьих, нужно сконфигурировать юзера postgres в СУБД, если вы этого ещё не сделали ранее.

После того, как зависимости разрешены, создаём базу данных и юзера.

Подсоединяемся к СУБД.

$ psql -h localhost -U postgres -W

Создаём базу и юзера.

postgres=# create database faytest;
CREATE DATABASE

postgres=# create user faytest with password 'faytest';
CREATE ROLE

Теперь заводим проект. Как поднимется, смотрим тут.

Последним пунктом значится вычисление числа Фибоначчи по его индексу. А теперь посмотрим, как это реализовано.

В структуре проекта появились следующие файлы:

./static/faygen-omEJ2n4J.js
./static/fay-runtime.js
./fay-shared
./fay-shared/SharedTypes.hs
./fay
./fay/FFIExample.hs
./fay/Home.hs
./fay/Fay
./fay/Fay/Yesod.hs

В шаблоне templates/homepage.hamlet находим строки:

    <input #fibindex placeholder="Fibonacci Index">
    <span #fibresult>

Текстовое поле для ввода и вывода значений. Где же описана логика?

В обработчике Handler/Home.hs находим строку $(fayFile "Home"). Так подключается fay/Home.hs.

Посмотрим на содержимое:

{-# LANGUAGE RebindableSyntax #-}
{-# LANGUAGE OverloadedStrings #-}

module Home where

import           FFIExample

import           DOM
import           Data.Text (fromString)
import qualified Data.Text as T
import           Fay.Yesod
import           Prelude
import           SharedTypes

main :: Fay ()
main = do
    input <- getElementById "fibindex"
    result <- getElementById "fibresult"
    onKeyUp input $ do
        indexS <- getValue input
        index <- parseInt indexS
        call (GetFib index) $ setInnerHTML result . T.pack . show

С помощью монады Fay осуществляются все преобразования с побочными эффектами, прямо как с IO. Не правда ли, код стал более читаем? Часть функций мы импортировали из DOM, call - из Fay.Yesod, остальное - из FFIExample. Как можно заметить, у нас тут и клиентская, и серверная часть. Как водится, они разделены. С помощью call мы делаем асинхронную отправку запроса по адресу /fay-command. Сервер принимает запрос и вызывает обработчик appFayCommandHandler. В Application.hs прописывается функция onCommand из Handler.Fay, которая отрисовывает на клиенте число Фибоначчи по индексу.

Для того, чтобы разобраться с тем, как в Fay реализована клиентская и серверная часть, рассмотрим их по отдельности.

Клиентская часть

Все файлы, относящиеся к клиентской части лежат в директории fay:

./fay/FFIExample.hs
./fay/Home.hs
./fay/Fay/Yesod.hs

В Home.hs лежит подключаемый fayFile. Его содержимое мы уже рассмотрели выше.

В Fay/Yesod.hs лежат функции для взаимодействия с сервером. Ровно то же самое можно обнаружить в хакадже.

В FFIExample - самое интересное: примеры реализации внешнего интерфейса вызова JS. С побочными действиями, со всеми делами.

-- | Example of defining FFI functions.
--
-- The `ffi' method is currently incompatible with 'RebindableSyntax',
-- so these are defined in another module.

module FFIExample where

import Data.Text (Text)
import DOM
import FFI

onKeyUp :: Element -> Fay () -> Fay ()
onKeyUp = ffi "%1.onkeyup=%2"

setInnerHTML :: Element -> Text -> Fay ()
setInnerHTML = ffi "%1.innerHTML=%2"

Как можно заметить, мы не передаём аргументы в функцию, а используем порядковые номера %1, %2 и так далее, как в Си.

Серверная часть

Все файлы, относящиеся к серверной части, лежат в fay-shared. В нашем случае это SharedTypes.hs:

{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE DeriveDataTypeable #-}
module SharedTypes where

import Prelude
import Data.Data
import Fay.Yesod

data Command = GetFib Int (Returns Int)
    deriving (Typeable, Data)

Command здесь - это тип, который принимает сервер и обрабатывает в Handler.Fay (напомню, что обработчик можно найти по адресу Handler/Fay.hs). Выше уже это упомяналось, рассмотрим же теперь подробнее:

module Handler.Fay where

import Fay.Convert (readFromFay)
import Import
import Prelude     ((!!))
import Yesod.Fay

fibs :: [Int]
fibs = 0 : 1 : zipWith (+) fibs (drop 1 fibs)

onCommand :: CommandHandler App
onCommand render command =
    case readFromFay command of
      Just (GetFib index r) -> render r $ fibs !! index
      Nothing               -> invalidArgs ["Invalid command"]

Нетрудно заметить, что при добавлении новых конструкторов типов, можно будет сопоставлением с образцом легко дополнить взаимодействие с сервером так, как того требует бизнес-логика приложения. Да, это устаревающий AJAX, но он ещё не совсем умер. С этим действительно можно работать.

Fay Runtime

А как это чудо выглядит после компиляции? Помните, в начале в списке файлов проекта появились static/fay-runtime.js, static/faygen-*.js? Вот это как раз оно и есть. В файле fay-runtime.js содержится Prelude и множество полезных вещей, которые мы можем использовать. В сгенерированном файле содержатся результаты компиляции файлов из директории fay.


Комментарии

comments powered by Disqus
Posted on 2015-10-03 by agr . Powered by Hakyll