Happstack Lite: Веб-фреймворк на Хаскеле

Original author: Happstack.com
image
Картинка для привлечения внимания, clckwrks — веб-фреймворк, тесно связанный с Happstack.

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


Предисловие


Happstack Lite представляет из себя простую в своей структуре и легкую в использовании версию Happstack. Для его создания разработчики:
  1. Собрали все основные типы и функции, которые вам нужны для разработки веб-приложения, в единственном модуле Happstack.Lite, так что вам не нужно рыскать по модулям в поисках того, что вам нужно.
  2. Дали функциям намного более простые сигнатуры, исключив монадные трансформеры и избавившись от большинства классов типов.
  3. Создали этот туториал, который в менее чем 2000 словах описывает все основные вещи, которые вам нужно знать, чтобы начать писать веб-приложение.

Но самое главное — Happstack Lite почти полностью совместим с Happstack! Если вы разрабатываете приложение на Happstack Lite, и вам нужна продвинутая возможность из Happstack, вы можете просто-напросто импортировать соответствующий модуль и использовать его.
Чтобы перевести проект с Happstack Lite на обычный, вам понадобится внести всего лишь 4 небольших изменения:
  1. import Happstack.Lite заменить на import Happstack.Server
  2. serve Nothing заменить на simpleHTTP nullConf
  3. добавить import Control.Monad (msum)
  4. добавить явный вызов decodeBody (подробности)


В то время как Happstack Lite легковесен по сравнению с обычным Happtsack, он по-прежнему является полнофункциональным фреймворком наряду с другими веб-фреймворками на Хаскеле.

В целях упрощения разработчики отказались от использования некоторых продвинутых библиотек, которые работают с Happstack. Если вы заинтересованы в фреймворке с типобезопасными URL, типобезопасными формами, HTML-синтаксисом в литералах и многим другим, то возможно вам стоит рассмотреть Happstack Foundation. Кривая обучения выше, но дополнительная надежность стоит того. Поскольку эти библиотеки построены поверх ядра Happstack, то изученный в данном туториале материал пригодится и при их применении.

Для более глубокого ознакомления вы можете прочитать Happstack Crash Course (который я тоже переведу, если будет проявлен интерес к этой статье — прим. пер.)

Запуск сервера


Для начала нам понадобится пара расширений языка:
{-# LANGUAGE OverloadedStrings, ScopedTypeVariables #-}

Теперь подключим некоторые библиотеки:
module Main where
import Control.Applicative ((<$>), optional)
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import Data.Text.Lazy (unpack)
import Happstack.Lite
import Text.Blaze.Html5 (Html, (!), a, form, input, p, toHtml, label)
import Text.Blaze.Html5.Attributes (action, enctype, href, name, size, type_, value)
import qualified Text.Blaze.Html5 as H
import qualified Text.Blaze.Html5.Attributes as A

Чтобы запустить приложение, мы вызываем функцию serve. Первый аргумент — конфигурация, она опциональна. Второй аргумент — наше, непосредственно, веб-приложение.
main :: IO ()
main = serve Nothing myApp

Веб-приложение имеет тип ServerPart Response. Вы можете считать ServerPart веб-эквивалентом монады IO.

(По умолчанию используется порт 8000, то есть увидеть ваше приложение вы можете по адресу http://localhost:8000/ — прим. пер.)

Статичные адреса


Вот и наше веб-приложение:
myApp :: ServerPart Response
myApp = msum
  [ dir "echo"    $ echo
  , dir "query"   $ queryParams
  , dir "form"    $ formPage
  , dir "fortune" $ fortune
  , dir "files"   $ fileServing
  , dir "upload"  $ upload
  , homePage
  ]

В самом общем виде наше приложение — просто несколько обработчиков, поставленных в соответствие статичным адресам.

dir используется, чтобы обработчик выполнялся только при успешном сопоставлении статичных компонентов пути. Например, dir "echo" успешно сработает с адресом localhost:8000/echo. Чтобы назначить обработчик для адреса "/foo/bar", достаточно просто написать dir "foo" $ dir "bar" $ handler.

Выполняется попытка последовательно применить каждый обработчик, до тех пор пока один из них не вернет реузльтат успешно. В данном случае — Response.

Мы преобразуем список обработчиков в один единственный с помощью msum.

Последний обработчик — homePage — ничем не ограничен (к нему не применяется dir — прим. пер.), поэтому он всегда будет вызван, если ни один из других обработчиков не сработает успешно.

HTML-шаблоны


Поскольку создаем мы веб-приложение, то нам понадобится создавать HTML-страницы. Мы можем делать это, используя Blaze, по которому тоже есть туториал.

Тема шаблонизации HTML вызывает масштабные разногласия в сообществе. Ни одна шаблонная система не может удовлетворить всех, так что Happstack поддерживает множество разных систем. В данном туториале применяется Blaze, потому что он поддерживается и базируется на чисто функциональных комбинаторах. Если вам нравятся шаблоны времени компиляции, но вы желаете HTML-синтаксис, можете рассмотреть HSP. Если вы негативно относитесь к шаблонам в своем коде и предпочитаете внешние XML-файлы, рассмотрите Heist.

Удобно иметь шаблонную функцию, которая сочетает в себе общие элементы для всех страниц веб-приложения, такие как импорт CSS, внешние JS-файлы, меню и т. д. В данном туториале мы будем использовать очень простой шаблон:
template :: Text -> Html -> Response
template title body = toResponse $
  H.html $ do
    H.head $ do
      H.title (toHtml title)
    H.body $ do
      body
      p $ a ! href "/" $ "На главную"


Тогда главная страница выглядит вот так:
homePage :: ServerPart Response
homePage =
    ok $ template "Главная страница" $ do
           H.h1 "Привет!"
           H.p "Писать приложения на Happstack Lite быстро и просто!"
           H.p "Зацени эти крутые приложения:"
           H.p $ a ! href "/echo/secret%20message"  $ "Эхо"
           H.p $ a ! href "/query?foo=bar" $ "Параметры запроса"
           H.p $ a ! href "/form"          $ "Обработка формы"
           H.p $ a ! href "/fortune"       $ "Печеньки-предсказания (куки)"
           H.p $ a ! href "/files"         $ "Доступ к файлам"
           H.p $ a ! href "/upload"        $ "Размещение файлов"

Функция ok устанавливает для страницы HTTP-код «200 OK». Есть и другие вспомогательные функции, например notFound устанавливает код «404 Not Found», seeOther — «303 See Other». Чтобы установить HTTP-код числом, используется setResponseCode.

Динамические части адреса


Функция dir выполняет сопоставление только со статичной частью адреса. Мы можем использовать функцию path, чтобы извлечь значение из динамической части адреса и опционально сконвертировать его в некий тип, такой как Integer. В данном примере мы просто выводим на экран динамическую часть пути. Для проверки посетите http://localhost:8000/echo/fantastic
echo :: ServerPart Response
echo =
    path $ \(msg :: String) ->
        ok $ template "Эхо" $ do
          p $ "Динамическая часть адреса: " >> toHtml msg
          p "Измени адрес страницы, чтобы вывести на экран что-то иное."


Параметры запроса


Мы также можем получить значения строковых параметров запроса. Строка запроса — это часть адреса, которая выглядит как "?foo=bar". Попробуйте посетить http://localhost:8000/query?foo=bar
queryParams :: ServerPart Response
queryParams =
    do mFoo <- optional $ lookText "foo"
       ok $ template "Параметры запроса" $ do
         p $ "foo = " >> toHtml (show mFoo)
         p $ "Измени адрес страницы, чтобы установить другое значение foo."

В случае, если параметр запроса не установлен, функция lookText вернет mzero. В данном примере мы используем optional из модуля Control.Applicative, так что в итоге получаем значение типа Maybe.

Формы


Мы можем использовать lookText и для получения данных с форм.
formPage :: ServerPart Response
formPage = msum [ viewForm, processForm ]
  where
    viewForm :: ServerPart Response
    viewForm =
        do method GET
           ok $ template "form" $
              form ! action "/form" ! enctype "multipart/form-data" ! A.method "POST" $ do
                label ! A.for "msg" $ "Напиши что-нибудь умное"
                input ! type_ "text" ! A.id "msg" ! name "msg"
                input ! type_ "submit" ! value "Отправить"
    processForm :: ServerPart Response
    processForm =
        do method POST
           msg <- lookText "msg"
           ok $ template "form" $ do
             H.p "Ты написал:"
             H.p (toHtml msg)

Мы используем ту же функцию lookText, что и в предыдущем параграфе, чтобы получить данные из формы. Вы также могли заметить, что мы используем функцию method, чтобы различать GET и POST запросы.
Когда пользователь просматривает форму, браузер запрашивает страницу /form с помощью GET. В HTML-теге form в качестве действия по нажатию кнопки мы указали открытие этой же страницы, но с помощью аттрибута выбрали метод POST.

Печеньки! (HTTP-cookies)


Данный пример расширяет пример с формой, сохраняя сообщение в куки. Это значит, пользователь может покинуть страницу, а когда вернется назад — страница будет помнить сохраненное сообщение.
fortune :: ServerPart Response
fortune = msum [ viewFortune, updateFortune ]
    where
      viewFortune :: ServerPart Response
      viewFortune =
          do method GET
             mMemory <- optional $ lookCookieValue "Печеньки-предсказания (куки)"
             let memory = fromMaybe "Твое будущее будет определено с помощью веб-технологий!" mMemory
             ok $ template "fortune" $ do
                    H.p "Сообщение из твоей печеньки-предсказания (куки):"
                    H.p (toHtml memory)
                    form ! action "/fortune" ! enctype "multipart/form-data" ! A.method "POST" $ do
                    label ! A.for "fortune" $ "Измени свою судьбу: "
                    input ! type_ "text" ! A.id "fortune" ! name "new_fortune"
                    input ! type_ "submit" ! value "Отправить"
      updateFortune :: ServerPart Response
      updateFortune =
          do method POST
             fortune <- lookText "new_fortune"
             addCookies [(Session, mkCookie "fortune" (unpack fortune))]
             seeOther ("/fortune" :: String) (toResponse ())
(Игру слов между HTTP-cookie и fortune cookie мне сохранить как-то не удалось — прим. пер.)

По сравнению с предыдущим примером появилось совсем немного нового:
  1. lookCookieValue работает точно так же, как и lookText, с той лишь разницей, что ищет значение в куках, а не параметрах запроса или форме.
  2. addCookies отправляет куки браузеру и имеет следующий тип: addCookies :: [(CookieLife, Cookie)] -> ServerPart ()
  3. CookieLife определяет, как долго куки существуют и считаются корректными. Session означает срок жизни для куки до закрытия окна браузера.
  4. mkCookie принимает имя куки, ее значение, и создает Cookie.
  5. seeOther (т. е. 303, редирект) говорит браузеру сделать новый GET-запрос на страницу /fortune.


Доступ к файлам


В большинстве веб-приложений возникает нужда предоставить доступ к статичным файлам с диска, таким как изображения, таблицы стилей, скрипты и т. д. Мы можем достичь этого с помощью функции serveDirectory:
fileServing :: ServerPart Response
fileServing =
    serveDirectory EnableBrowsing ["index.html"] "."

Первый аргумент определяет, должна ли serveDirectory создать список файлов в директории, чтобы их можно было просматривать.
Второй аргумент — список файлов индексации. Если пользователь запрашивает просмотр директории и она содержит файл индексации, то вместо списка файлов будет отображен он.
Третий аргумент — путь к директории, к которой предоставляется доступ. В данном примере мы обеспечиваем доступ к текущей директории.

На поддерживаемых платформах (Linux, OS X, Windows), функция serveDirectory автоматически использует sendfile() для доступа к файлам. В sendfile() применяются низкоуровневые операции ядра, обеспечивающие перенос файлов с накопителя в сеть с минимальной нагрузкой на процессор и максимальным использованием сетевого канала.

Размещение файлов


Обработка загрузки файлов на сервер достаточно прямолинейна. Мы создаем форму, как и в предыдущем примере, но вместо lookText используем lookFile.
upload :: ServerPart Response
upload =
       msum [ uploadForm
            , handleUpload
            ]
    where
    uploadForm :: ServerPart Response
    uploadForm =
        do method GET
           ok $ template "Размещение файла" $ do
             form ! enctype "multipart/form-data" ! A.method "POST" ! action "/upload" $ do
               input ! type_ "file" ! name "file_upload" ! size "40"
               input ! type_ "submit" ! value "upload"
    handleUpload :: ServerPart Response
    handleUpload =
        do (tmpFile, uploadName, contentType) <- lookFile "file_upload"
           ok $ template "Файл загружен" $ do
                p (toHtml $ "Временный файл: " ++ tmpFile)
                p (toHtml $ "Имя загрузки:  " ++ uploadName)
                p (toHtml $ "Тип контента:   " ++ show contentType)


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

В большинстве случаев, пользователь не хочет загрузить файл только ради того, чтобы он был удален. Обычно в обработчике вызываются moveFile или copyFile, чтобы переместить (или скопировать) файл в его перманентную локацию.

От переводчика


Автор статьи предполагает наличие базовых знаний языка Хаскель. Для установки Happstack воспользуйтесь инструкцией на сайте.

Если вас заинтересовал этот фреймворк, я рекомендую ознакомиться с его полной версией (курс по которой я тоже собираюсь перевести), а также основанном на нем clckwrks. Приятной разработки!
Share post

Comments 25

    0
    Всё бы хорошо, но с базами у них всё также уныло. Какая-то странная проблема хаскель-мира.
      +1
      В Хаскеле есть биндинги для большинства СУБД, но вместе с Happstack предпочтительнее использовать Acid-State — технологию, которая позволяет хранить данные произвольного типа и составлять запросы прямо на Хаскеле.
        0
        По ссылке 500 — Internal Server Error
        Молчу, молчу…
          0
          Хах, действительно. Еще вчера все было отлично, неужто Хабр завалил?
            0
            Можно еще здесь про Acid-State почитать.
          0
          Persistent в Yesod очень даже хорошо выглядит, прямо поразительно красивее ООП-ORM'ов в некоторых местах. AcidState выглядит тоже неплохо.
            0
            А вы им пользовались? Я вот пытался — это мрак и ад, к сожалению. AcidState в этом плане гораздо симпатичней. Но хотелось бы большего :)
              0
              Нет, как раз только сейчас балуюсь ими, потому и интересно узнать подробностей, что вам не понравилось конкретно.
                0
                Как только выходишь за пределы простых select-ов, начинается пляска и вырвиглазные конструкции, пропадает красота и лаконичность. Можно взять esqueletto, но там тоже довольно быстро начинаются проблемы. Собственно, некоторые проблемы персистеда описаны в мотивировке к esquletto blog.felipe.lessa.nom.br/?p=68
                  0
                  А, ну тогда ясно. Да, мне как-то в последние годы удавалось все проблемы выражать в упрощенных моделях, без необходимости городить джойны или какой-то сложный SQL.

                  Esqueletto не трогал, за ссылку спасибо, гляну.
                    0
                    > However, what if you didn’t know John’s key, just its e-mail (assuming that there is an uniqueness constraint on e-mails)? Unfortunately, with persistent you’ll need either two queries:
                    > Or you could do it in one query using the ad hoc Database.Persist.Query.Join.Sql module:

                    А, теперь понял о чем вы. Да, кошмар/
                      0
                      Хотя, если честно, я так и не понял, чем автора не устроил

                          maybeMichael <- selectFirst [PersonEmail ==. "michael@haskell.org"] []
                          case maybeMichael of
                            Nothing -> liftIO $ putStrLn $ "Sorry, noone found."
                            Just michael -> liftIO $ print michael
                      


                      в этом примере с выборкой по имейлу.
                  0
                  Очень хорошим вариантом мне представляется аналог вот такого (https://github.com/jonifreeman/sqltyped). Но ничего подобного для хаскеля я пока не видел.
                    0
                    Ну, sqltyped на первый взгляд — это несколько совсем другое. В смысле, Persistent и AcidState стараются наоборот, абстрагироваться от SQL'а (а AcidState еще и от персистентности и прочего, можно использовать чистые IxSet всякие).
                      0
                      Непонятно зачем городить DSL вокруг DSL-я, который изначально предназначен для работы с SQL? :) А так мы убиваем двух зайцев сразу — не скрещиваем ежа и носорога и получаем (более-менее) type safety.
                        0
                        Не очень понял. Суть «отгораживания от SQL» состоит в том, чтоб база данных (вообще персистентность объектов) была лишь деталью реализации, а не цертром вашей архитектуры. Ваши «бизнес-объекты» — обычные данные, а персистентность прикручивается незаметно в сторонке.

                        То есть, ORM как раз тем и занимается (в ООП-языках), что мапит ваши бизнес-объекты с описанием табличек, и «в идеале» вы БД можете прикручивать в последний момент. Robert Martin, в общем, уже сто раз рассказал лучше меня об этом.
                          0
                          Мартин не истина в последней инстанции. Во-первых, SQL стандартный тоже вполне может скрывать детали реализации (какая там конкретная реализация не столь важно, коль скоро мы не пишем в самом SQL ничего спеицифичного), а констрейн на СУБД в любом случае есть. Во-вторых, как только захочется использовать СУБД-специфичные вещи, то детали уже надо будет учитывать. Так что это вопрос подхода. В общем и целом, я считаю, что сам по себе SQL уже хороший уровень абстракции (для веба).
                            0
                            Ну, имеется в виду не то, чтоб «скрывать детали реализации», а в том, что представление бизнес-моделей в хаскеле (тут, наверное, всякие алгебраические типы данных приходят на ум), или же в ООП-языках (обычно модели друг друга биндят прямо в виде моделей, а не через идентификаторы), мягко говоря «иногда» отличаются от того, что хранится в БД. В БД хранятся структуры данных, оптимизированные под представление, удобное для БД. Но оперировать же лучше данными (моделями), представленными в удобном виде для программиста.

                            То есть, я к тому, что считаю, что нужно иметь как один, так и другой механизм. Отдельно ORM, связывающий данные с персистентностью, и отдельно язык запросов, который позволяет оперировать SQLем в виде, более приятным, нежели строки.

                            По поводу «СУБД-специфичных вещей» — ну, собственно, суть не в том, чтоб никогда не писать голый SQL, а в том, чтоб трогать его только через некоторый абстрактный интерфейс, который скрывает детали БД и запросов, возвращая уже сами бизнес-объекты.

                            В общем, не хотелось бы уходить в политику. Предлагаю просто рассматривать два отдельных инструмента: ORM и SQL-абстрагирование и их качество. Их нужность обсуждать уже не хотелось бы :)
                              0
                              Ну я тоже не спорю, что иметь естественный для языка (и как можно более незаметный) «морфизм» из его способа описания данных и связей между ними и хранилищем — отличная штука. :)
                                0
                                я был бы сильно рад увидеть нечто подобное myBatis для хаскеля, потому как все эти умные стрелки очень клево смотрятся в статьях по матану, но в реальности запросы бывают весьма стремными и очень vendor-specific, так что абстрагирование их в каком-нибудь подключаемом текстовом файле и call-by-name было бы попросту отлично. Все попытки сделать ORM упрутся в Hibernate, оно вам надо?
                                  0
                                  Если честно, не очень понятно, зачем SQL-запросы выносить в XML-файлы, в смысле, почему не построить красивое описание запросов прямо средствами языка (в Х-ле они позволяют это сделать). По поводу упирания в Hibernate не понимаю, почему вдруг, и при чем он здесь.
                                    0
                                    Потому что красивое описание запросов средствами языка, отличного от SQL принятого для конкретно этой базы данных, вряд ли получится. Таких примеров нет в жабомире, а уж они-то долго старались.

                                    По поводу упирания в хибернейт — вот они как раз пытались сделать красивое описание.
                                      0
                                      Ну, мне известно как минимум одна библиотека для питона — SQLAlchemy, и это в питоне, который тоже особо своей DSL'ностью никогда не выделялся. В хаскеле синтаксис и семантика позволит сделать еще приятнее.

                                      Ну а то, что у джавы получаются монстры — наверное это проблема джавы. Вон, например, на хаскеле вполне неплохо выглядят parser combinator'ы (parsec, например). То есть даже парсеры (!) можно относительно красиво выражать средствами языка (такого я действительно нигде больше не видел).
                              0
                              Ну и по поводу «персистентность прикручивается незаметно в сторонке» — это выглядит хорошо на бумаге, а в реальной жизни я не видел ни разу, чтобы об эту незаметность не спотыкались сплошь и рядом.
                                0
                                Не знаю кто как, но я как раз благодаря тому, что все «доставания данных» делал через отдельный интерфейс, смог на реальном проекте вынести кусок функционала в другую БД вполне просто. Бизнес-логика не была затронута совершенно, пришлось менять только интфейс выборки, что сильно облегчило задачу.

                                В то время, как «меняем БД на монго, пацаны!» встречается не часто, вынос отдельного куска по типу статистики или «больших ненужных данных» — вполне задача сегодняшнего дня.

                  Only users with full accounts can post comments. Log in, please.