REST-сервер для простого блога на Haskell

Некоторое время назад я окончательно устал от языков с динамической типизацией и решил попробовать изучить что-нибудь брутально-статическое. Haskell приглянулся мне красотой кода и бескомпромиссным стремлением явно отделить чистые функции от производящих сайд-эффекты. Я залпом проглотил несколько книжек по Haskell и решил, что пора что-нибудь уже и написать.

И тут-то меня ждало разочарование: я не был способен написать ничего кроме hello world-a. Т.е. я примерно представлял себе, как написать какую-нибудь консольную утилиту типа find или вроде того, — но первая же встреча с IO разрушала все мои представления. Библиотек для Haskell вроде бы много, а документации по ним почти совсем нету. Примеров решения типовых задач тоже очень мало.

Симптомы понятны, диагноз простой: отсутствие практики. А для Haskell это достаточно болезненно, т.к. язык крайне необычный. Даже то, что я неплохо знаю Clojure, почти никак мне не помогло, т.к. Clojure больше фокусируется на функциях, в то время как Haskell — на их типах.

Думаю, многие новички столкнулись с проблемой отсутствия практики в Haskell. Писать что-то совсем уж без интерфейса как-то не интересно, а сделать desktop- или web-приложение для начинающего хаскелиста довольно сложно. И в этой статье я собираюсь предложить простой пример, как написать сервер веб-приложения на Haskell специально для тех, кто хочет попрактиковаться в Haskell, но не знает, с какой стороны к нему подойти.

Для самых нетерпеливых: исходники здесь.

Скажу сразу: это не очередной tutorial по Yesod. Этот фреймворк чересчур строго диктует свои представления о том, как правильно делать веб-приложения, и не со всем я согласен. Поэтому базой будет маленькая библиотечка Scotty, предлагающая красивый синтаксис описания маршрутов для веб-сервера Warp.

Задача


Разработать сервер веб-приложения для простого блога. Будут доступны следующие маршруты:
  • GET /articles — список статей.
  • GET /articles/:id — отдельная статья.
  • POST /admin/articles — создать статью.
  • PUT /admin/articles — обновить статью.
  • DELETE /admin/articles/:id — удалить статью.

Все маршруты, которые начинаются с «/admin» требуют аутентификацию пользователя. Для stateless-сервиса очень удобно использовать Basic-аутентификацию, т.к. каждый запрос содержит логин и пароль пользователя.

Что понадобится?


  • Некоторые начальные знания Haskell, общее понимание монад и функторов, устройства программы, ввода-вывода и т.д.
  • Утилита cabal, умение использовать sandbox-ы, подключать библиотеки, компилировать и запускать проект.
  • MySQL и самые начальные знания о нем.

Архитектура


Для реализации архитектуры предлагаю использовать следующие библиотеки.
  • Web-сервер — Warp.
  • Маршрутизатор — Scotty.
  • Конфигурация приложения — configurator.
  • Доступ к БД: mysql и mysql-simple.
  • Пул соединений с БД: resource-pool.
  • Взаимодействие с клиентом — REST с использованием JSON, библиотека — aeson.
  • wai-extra для basic-аутентификации, т.к. приложение будет stateless.

Разобьем наше приложение на модули.
  • Main.hs будет содержать код для запуска приложения, маршрутизатор и конфигурацию приложения.
  • Db.hs — все, что связано с доступом к базе данных.
  • View.hs — представление данных.
  • Domain.hs типы и функции для работы с предметной областью.
  • Auth.hs — функции для аутентификации.

Приступаем


Давайте создадим простой проект cabal для нашего приложения.

	mkdir hblog
	cd hblog
	cabal init

Здесь вам надо ответить на пару вопросов, при этом тип проекта выберите Executable, главный файл — Main.hs, директорию с исходниками — src. Вот используемые библиотеки, которые необходимо добавить в build-depends в файл hblog.cabal:

   base                          >= 4.6        && < 4.7
 , scotty                        >= 0.9.1
 , bytestring                    >= 0.9        && < 0.11
 , text                          >= 0.11       && < 2.0
 , mysql                         >= 0.1.1.8
 , mysql-simple                  >= 0.2.2.5
 , aeson                         >= 0.6        && < 0.9
 , HTTP                          >= 4000.2.19
 , transformers                  >= 0.4.3.0
 , wai                           >= 3.0.2.3
 , wai-middleware-static         >= 0.7.0.1
 , wai-extra                     >= 3.0.7
 , resource-pool                 >= 0.2.3.2
 , configurator                  >= 0.3.0.0
 , MissingH                      >= 1.3.0.1

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

	cabal sandbox init
	cabal install —dependencies-only 

Не забудьте создать файл src/Main.hs.

Давайте посмотрим, как устроено минимальное веб-приложение на Scotty. Документация и примеры использования этого микро-фреймворка очень хороши, так что с первого взгляда все становится понятно. А если у вас есть опыт с Sinatra, Compojure или Scalatra — считайте, что вам повезло, т.к. этот опыт здесь полностью пригодится.

Вот как выглядит минимальный src/Main.hs:

{-# LANGUAGE OverloadedStrings #-}

import Web.Scotty

import Data.Monoid (mconcat)

main = scotty 3000 $ do
  get "/:word" $ do
    beam <- param "word"
    html $ mconcat ["<h1>Scotty, ", beam, " me up!</h1>"]

Первая же строка кода может повергнуть новичка в изумление: что еще за перегружаемые строки? Сейчас объясню.

Поскольку я, как и многие другие, начал изучать Haskell с книг «Learn you a Haskell for a greater good» и «Real World Haskell», для меня сразу же стала большой проблемой обработка текста. Самое лучшее описание работы с текстом в Haskell я нашел в книге «Beginning Haskell» в главе 10.

Если очень кратко, то на практике используются три базовых типа строковых данных:
  • String — список символов. Этот тип данных встроен в язык.
  • Text — тип данных, предназначенный как для ASCII, так и для UTF-символов. Находится в библиотеке text и существует в двух видах: строгой и ленивой. Подробнее — здесь
  • ByteString — предназначен для сериализации строк в поток байтов. Поставляется в библиотеке bytestring и также в двух вариантах: строгом и ленивом.

Вернемся к заголовку OverloadedStrings. Штука в том, что, учитывая наличие нескольких типов строковых данных, исходник будет пестреть вызовами вроде T.pack «Hello» там, где лексему «Hello» необходимо преобразовать в Text; или B.pack «Hello» там, где лексему нужно преобразовать в ByteString. Вот чтобы убрать этот синтаксический мусор используется директива OverloadedStrings, которая самостоятельно выполняет преобразование строковой лексемы к нужному строковому типу.

Файл Main.hs


Главная функция:

main :: IO ()
main = do

-- Здесь мы загружаем конфигурационный файл application.conf, в котором хранятся настройки соединения с базой данных
    loadedConf <- C.load [C.Required "application.conf"]
    dbConf <- makeDbConfig loadedConf
    
    case dbConf of
      Nothing -> putStrLn "No database configuration found, terminating..."
      Just conf -> do
-- Создаем пул соединений (время жизни неиспользуемого соединения — 5 секунд, максимальное количество соединений с БД -- 10)      
          pool <- createPool (newConn conf) close 1 5 10
-- Запускаем маршрутизатор Scotty
          scotty 3000 $ do
-- Доступ к статическим файлам из директории «static»
              middleware $ staticPolicy (noDots >-> addBase "static")
-- Логирование всех запросов. Для продакшена используйте logStdout вместо logStdoutDev
              middleware $ logStdoutDev
-- Запрос на аутентификацию для защищенных маршрутов
              middleware $ basicAuth (verifyCredentials pool)
                           "Haskell Blog Realm" { authIsProtected = protectedResources }

              get    "/articles" $ do articles <- liftIO $ listArticles pool
                                      articlesList articles
-- Получит из запроса параметр :id и найдет в БД соответствующую запись
              get    "/articles/:id" $ do id <- param "id" :: ActionM TL.Text
                                          maybeArticle <- liftIO $ findArticle pool id
                                          viewArticle maybeArticle
-- Распарсит тело запроса в тип Article и создаст новую запись Article в БД
              post   "/admin/articles" $ do article <- getArticleParam
                                            insertArticle pool article
                                            createdArticle article

              put    "/admin/articles" $ do article <- getArticleParam
                                            updateArticle pool article
                                            updatedArticle article

              delete "/admin/articles/:id" $ do id <- param "id" :: ActionM TL.Text
                                                deleteArticle pool id
                                                deletedArticle id

Для конфигурации приложения воспользуемся пакетом configurator. Конфигурацию будем хранить в файле application.conf, и вот его содержимое:

database {
  name = "hblog"
  user = "hblog"
  password = "hblog"
}

Для пула соединений используем библиотеку resource-pool. Соединение с БД — удовольствие дорогое, так что лучше не создавать его на каждый запрос, а дать возможность переиспользовать старые. Тип функции createPool такой:

createPool :: IO a -> (a -> IO ()) -> Int -> NominalDiffTime -> Int -> IO (Pool a)
createPool create destroy numStripes idleTime maxResources

Здесь create и destroy — функции для создания и завершения соединения с базой данных, numStripes — количество раздельных суб-пулов соединений, idleTime — время жизни неиспользуемого соединения (в секундах), maxResources — максимальное количество соединений в суб-пуле.

Для открытия соединения используем функцию newConn (из Db.hs).

data DbConfig = DbConfig {
     dbName :: String,
     dbUser :: String,
     dbPassword :: String
     }
     deriving (Show, Generic)

newConn :: DbConfig -> IO Connection
newConn conf = connect defaultConnectInfo
                       { connectUser = dbUser conf
                       , connectPassword = dbPassword conf
                       , connectDatabase = dbName conf
                       }

Ну а сам DbConfig создается так:

makeDbConfig :: C.Config -> IO (Maybe Db.DbConfig)
makeDbConfig conf = do
  name <- C.lookup conf "database.name" :: IO (Maybe String)
  user <- C.lookup conf "database.user" :: IO (Maybe String)
  password <- C.lookup conf "database.password" :: IO (Maybe String)
  return $ DbConfig <$> name
                    <*> user
                    <*> password

На вход передается Data.Configurator.Config, который мы прочитали и распарсили из application.conf, а на выходе — Maybe DbConfig, заключенный в оболочку IO.

Такая запись для начинающих возможно покажется немного непонятной, и я попытаюсь пояснить, что здесь происходит.
Тип выражения C.lookup conf «database.name» — это Maybe String, заключенный в IO. Извлечь его из IO можно так:

name <- C.lookup conf "database.name" :: IO (Maybe String)

Соответственно, у констант name, user, password тип — Maybe String.

Тип конструктора данных DbConfig такой:

DbConfig :: String -> String -> String -> DbConfig 

Эта функция принимает на вход три строки и возвращает DbConfig.

Тип функции (<$>) такой:

(<$>) :: Functor f => (a -> b) -> f a -> f b

Т.е. он берет обычную функцию, функтор и возвращает функтор с примененной к его значению функцией. Короче, это обычный map.

Запись DbConfig <$> name извлекает из name строку (тип name — это Maybe String) присваивает значение первому параметру в конструкторе DbConfig и возвращает в оболочке Maybe каррированный DbConfig:

DbConfig <$> name :: Maybe (String -> String -> DbConfig) 

Обратите внимание, что здесь уже на один String передается меньше.

Тип (<*>) похож на <$>:

(<*>) :: Applicative f => f (a -> b) -> f a -> f b

Он берет функтор, значением которого является функция, берет еще один функтор и применяет функцию из первого функтора к значению из второго, возвращая новый функтор.

Таким образом, запись DbConfig <$> name <*> user имеет тип:

DbConfig <$> name <*> user :: Maybe (String -> DbConfig)

Остался последний String-овый параметр, который мы заполним password-ом:

DbConfig <$> name 
	     <*> user 
	     <*> password 
:: Maybe DbConfig

Аутентификация


В функции main осталась последняя сложная конструкция — это middleware basicAuth. Тип функции basicAuth такой:

basicAuth :: CheckCreds -> AuthSettings -> Middleware

Первый параметр — функция, проверяющая наличие пользователя в БД, вторая — определяет, какие маршруты требуют защиты аутентификацией. Их типы:

type CheckCreds = ByteString -> ByteString -> ResourceT IO Bool

data AuthSettings = AuthSettings
    { authRealm :: !ByteString
     , authOnNoAuth :: !(ByteString -> Application)
     , authIsProtected :: !(Request -> ResourceT IO Bool)
    }

Тип данных AuthSettings достаточно сложный, и если хотите поглубже с ним разобраться — смотрите исходники здесь. Нас же интересует здесь всего один параметр — authIsProtected. Это функция, которая по Request-у умеет определить, требовать ли аутентификацию, или нет. Вот её реализация для нашего блога:

protectedResources ::  Request -> IO Bool
protectedResources request = do
    let path = pathInfo request
    return $ protect path
    where protect (p : _) =  p == "admin"
          protect _        =  False

Функция pathInfo имеет следующий тип:

pathInfo :: Request -> [Text]

Она берет Request и возвращает список строк, которые получились после разделения маршрута запроса на подстроки по разделителю «/».
Таким образом, если наш запрос начинается с «/admin», то функция protectedResources вернет IO True, требуя аутентификацию.

А вот функция verifyCredentials, которая проверяет пользователя и пароль, относится к взаимодействию с БД, и поэтому о ней — ниже.

Взаимодействие с базой данных


Утилитные функции для извлечения данных из БД с использованием пула соединений:

fetchSimple :: QueryResults r => Pool M.Connection -> Query -> IO [r]
fetchSimple pool sql = withResource pool retrieve
       where retrieve conn = query_ conn sql

fetch :: (QueryResults r, QueryParams q) => Pool M.Connection -> q -> Query -> IO [r]
fetch pool args sql = withResource pool retrieve
      where retrieve conn = query conn sql args

Функцию fetchSimple нужно использовать для запросов без параметров, а fetch — для запросов с параметрами. Изменение данных можно сделать функцией execSql:

execSql :: QueryParams q => Pool M.Connection -> q -> Query -> IO Int64
execSql pool args sql = withResource pool ins
       where ins conn = execute conn sql args

Если необходимо использовать транзакцию, то вот функция execSqlT:

execSqlT :: QueryParams q => Pool M.Connection -> q -> Query -> IO Int64
execSqlT pool args sql = withResource pool ins
       where ins conn = withTransaction conn $ execute conn sql args

Используя функцию fetch можно, например, найти хэш пароля пользователя в БД по его логину:

findUserByLogin :: Pool Connection -> String -> IO (Maybe String)
findUserByLogin pool login = do
         res <- liftIO $ fetch pool (Only login) 
			      "SELECT * FROM user WHERE login=?" :: IO [(Integer, String, String)]
         return $ password res
         where password [(_, _, pwd)] = Just pwd
               password _ = Nothing

Она нужна в модуле Auth.hs:

verifyCredentials :: Pool Connection -> B.ByteString -> B.ByteString -> IO Bool
verifyCredentials pool user password = do
   pwd <- findUserByLogin pool (BC.unpack user)
   return $ comparePasswords pwd (BC.unpack password)
   where comparePasswords Nothing _ = False
         	  comparePasswords (Just p) password =  p == (md5s $ Str password)

Как видите, если хэш пароля в БД найден, то его можно сопоставить с паролем из запроса, закодированным при помощи алгоритма md5.

Но в базе данных хранятся не только пользователи, но и статьи, которые блог должен уметь создавать-редактировать-отображать. В файле Domain.hs определим тип данных Article c полями id title bodyText:

data Article = Article Integer Text Text
     deriving (Show)

Теперь можно определить функции CRUD в БД для этого типа:

listArticles :: Pool Connection -> IO [Article]
listArticles pool = do
     res <- fetchSimple pool "SELECT * FROM article ORDER BY id DESC" :: IO [(Integer, TL.Text, TL.Text)]
     return $ map (\(id, title, bodyText) -> Article id title bodyText) res
   
findArticle :: Pool Connection -> TL.Text -> IO (Maybe Article)
findArticle pool id = do
     res <- fetch pool (Only id) "SELECT * FROM article WHERE id=?" :: IO [(Integer, TL.Text, TL.Text)]
     return $ oneArticle res
     where oneArticle ((id, title, bodyText) : _) = Just $ Article id title bodyText
           oneArticle _ = Nothing


insertArticle :: Pool Connection -> Maybe Article -> ActionT TL.Text IO ()
insertArticle pool Nothing = return ()
insertArticle pool (Just (Article id title bodyText)) = do
     liftIO $ execSqlT pool [title, bodyText]
                            "INSERT INTO article(title, bodyText) VALUES(?,?)"
     return ()

updateArticle :: Pool Connection -> Maybe Article -> ActionT TL.Text IO ()
updateArticle pool Nothing = return ()
updateArticle pool (Just (Article id title bodyText)) = do
     liftIO $ execSqlT pool [title, bodyText, (TL.decodeUtf8 $ BL.pack $ show id)]
                            "UPDATE article SET title=?, bodyText=? WHERE id=?"
     return ()

deleteArticle :: Pool Connection -> TL.Text -> ActionT TL.Text IO ()
deleteArticle pool id = do
     liftIO $ execSqlT pool [id] "DELETE FROM article WHERE id=?"
     return ()

Наиболее важными здесь являются функции insertArticle и updateArticle. Они принимают на вход Maybe Article и вставляют/обновляют соответствующую запись в БД. Но откуда взять этот Maybe Article?

Все просто, пользователь должен передать Article, закодированный в JSON, в теле PUT- или POST- запроса. Вот функции для кодирования и декодирования Article в- и из- JSON:

instance FromJSON Article where
     parseJSON (Object v) = Article <$>
                            v .:? "id" .!= 0 <*>
                            v .:  "title"       <*>
                            v .:  "bodyText"

instance ToJSON Article where
     toJSON (Article id title bodyText) =
         object ["id" .= id,
                     "title" .= title,
                     "bodyText" .= bodyText]

Для обработки JSON используем библиотеку aeson, подробнее о ней — здесь.

Как видите, при декодировании поле id — не обязательное, и если его нет в строке с JSON, то подставится значение по умолчанию — 0. Поля id не будет при создании записи Article, т.к. id должна создать сама БД. Но id будет в update-запросе.

Представление данных


Вернемся в файл Main.hs и посмотрим, как мы получаем параметры запроса. Получить параметр из маршрута можно при помощи функции param:

param :: Parsable a => TL.Text -> ActionM a

А тело запроса можно получить функцией body:

body :: ActionM Data.ByteString.Lazy.Internal.ByteString

Вот функция, которая умеет получить тело запроса, распарсить его и вернуть Maybe Article

getArticleParam :: ActionT TL.Text IO (Maybe Article)
getArticleParam = do b <- body
                     return $ (decode b :: Maybe Article)
                     where makeArticle s = ""

Осталось последнее: вернуть данные клиенту. Для этого в файле Views.hs определим следующие функции:

articlesList :: [Article] -> ActionM ()
articlesList articles = json articles

viewArticle :: Maybe Article -> ActionM ()
viewArticle Nothing = json ()
viewArticle (Just article) = json article

createdArticle :: Maybe Article -> ActionM ()
createdArticle article = json ()

updatedArticle :: Maybe Article -> ActionM ()
updatedArticle article = json ()

deletedArticle :: TL.Text -> ActionM ()
deletedArticle id = json ()


Производительность сервера


Для тестов я использовал ноутбук Samsung 700Z c 8Гб памяти и четырехядерным Intel Core i7.
  • 1000 последовательных PUT-запросов для создания записи article.
    Среднее время ответа: 40 милисекунд, это примерно 25 запросов в секунду.
  • 100 потоков по 100 PUT-запросов в каждом.
    Среднее время ответа: 1248 миллисекунд, примерно 80 параллельных запросов в секнуду.
  • 100 потоков по 1000 GET-запросов, возвращающих 10 записей article.
    Среднее время ответа: 165 миллисекунд, примерно 600 запросов в секунду.

Просто для того, чтобы было хоть с чем-то сравнивать, я реализовал точно такой же сервер на Java 7 и Spring 4 с вебсвервером Tomcat 7 и получил следующие цифры.
  • 1000 последовательных PUT-запросов для создания записи article.
    Среднее время ответа: 51 миллисекунда, это примерно 19-20 запросов в секунду.
  • 100 потоков по 100 PUT-запросов в каждом.
    Среднее время ответа: 104 миллисекунды, примерно 960 параллельных запросов в секнуду.
  • 100 потоков по 1000 GET-запросов, возвращающих 10 записей article.
    Среднее время ответа: 26 миллисекунд, примерно 3800 запросов в секунду.

Выводы


Если вам не хватает практики в Haskell, и хочется попробовать писать на нем веб-приложения, то здесь вы найдете описанный в статье пример простого сервера с CRUD-операциями для одной сущности — Article. Приложение реализовано в виде JSON REST-сервиса и требует basic authentication на защищенных маршрутах. Для хранения данных используется СУБД MySQL, для повышения производительности применён пул соединений. Поскольку приложение не хранит состояния в сессии, его очень легко масштабировать горизонтально, кроме того stateless-сервер идеально подходит для разработки микросервисной архитектуры.

Применение Haskell для разработки JSON REST-сервера позволило получить краткий и красивый исходник, который, помимо прочего, легко поддерживать: рефакторинг, внесение изменений и дополнений не потребует большого труда, т.к. компилятор сам проверит корректность всех изменений. Минусом применения Haskell является не очень высокая производительность полученного веб-сервиса в сравнении с аналогичным, написанным на Java.

P.S.


По советам из комментов провел дополнительное тестирование. Изменение числа потоков до N=8 включительно — не влияет на производительность. При уменьшении N далее, производительность падает, т.к. на моем ноуте 8 логических ядер.

Еще интересная штука. Если отключить сохранение записи в БД, то средняя задержка ответа сервиса на Haskell падает аж до 6 миллисекунд (!), в аналогичном сервисе на java это время — 80мс. Т.е. узкое место в показанном проекте — именно взаимодействие с БД, если его отключить, то Haskell быстрее аналогичного функционала на Java в 13 раз. Потребление памяти тоже в несколько раз ниже: примерно 80Мб против 400Мб.
Поделиться публикацией
Похожие публикации
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама
Комментарии 30
  • +11
    • PUT /admin/articles — создать статью.
    • POST /admin/articles — обновить статью.

    Вроде должно быть наоборот POST — создание, PUT — обновление
    • 0
      Спасибо, поправил.
      • 0
        По аналогии с
        GET /articles/:id — отдельная статья.

        обновление статьи тоже должно быть по id
        PUT /admin/articles:id — обновить статью.
    • +5
      Как то не очень компактно и понятно.

      Вот memory cache REST server на Erlang:

      {deps, [ {rest,  ".*", {git, "git://github.com/synrc/rest", "HEAD"}}]}
      


      -module(users).
          -behaviour(rest).
          -compile({parse_transform, rest}).
          -include("users.hrl").
          -export(?REST_API).
          -rest_record(user).
      
          init() -> ets:new(users, [public, named_table, {keypos, #user.id}]).
          populate(Users) -> ets:insert(users, Users).
          exists(Id) -> ets:member(users, wf:to_list(Id)).
          get() -> ets:tab2list(users).
          get(Id) -> [User] = ets:lookup(users, wf:to_list(Id)), User.
          delete(Id) -> ets:delete(users, wf:to_list(Id)).
          post(#user{} = User) -> ets:insert(users, User);
          post(Data) -> post(from_json(Data, #user{}))
      


      Использовать так:

      curl -i -X POST -d "id=vlad" localhost:8000/rest/users
          curl -i -X POST -d "id=doxtop" localhost:8000/rest/users
          curl -i -X GET localhost:8000/rest/users
          curl -i -X PUT -d "id=5HT" localhost:8000/rest/users/vlad
          curl -i -X GET localhost:8000/rest/users/5HT
          curl -i -X DELETE localhost:8000/rest/users/5HT
      


      23K RPS
      • +3
        Стоит отметить что за этой компактностью стоит несколько скрытых файлов/либ:

        0) Собственно сам REST endpoint для Erlang web-фреймворка N2O
        220 строк кода (cloc output) =>
        ➜  rest cloc ./src 
               5 text files.
               5 unique files.                              
               1 file ignored.
        
        http://cloc.sourceforge.net v 1.62  T=0.02 s (167.8 files/s, 11284.5 lines/s)
        -------------------------------------------------------------------------------
        Language                     files          blank        comment           code
        -------------------------------------------------------------------------------
        Erlang                           4             47              2            220
        -------------------------------------------------------------------------------
        SUM:                             4             47              2            220
        -------------------------------------------------------------------------------
        


        1) Макроса REST_API уже нет,
        вместо
        -export(?REST_API). 
        

        это
        -export([init/0, populate/1, exists/1, get/0, get/1, post/1, delete/1]).
        

        2) Файл c users.hrl еще 13 строк кода
        users.hrl
        -record(user, {id, name, email, proplist = [{facebook, udefined},
                                                    {github, "github.com/b0oh"},
                                                    {local, undefined},
                                                    {twitter, udefined}],
                       string = "common",
                       number = 12,
                       list_of_strings = ["one", "two", "three"],
                       list_of_numbers = [34958726345, 12],
                       nested_proplists = [{nested, [{number, 12},
                                                     {string, "common"},
                                                     {list_of_strings, ["one", "two", "three"]},
                                                     {list_of_atoms, [one, two, three]},
                                                     {list_of_numbers, [100000, 2,3 ]}]}]}).
        


        3) и последнее, это wf:to_list — вызов конвертации из соответствующего модуля, еще 178 строк кода.

        Немного больше чем написал 5HT изначально, но все равно меньше чем у автора.
        • 0
          Да уж, Erlang с Cowboy сервером тут просто «рулят и бибкают». Но Erlang для этого и создавался…
          • 0
            сравнивать производительность сервиса работающего с erts и сервиса работающего с mariadb это нормально?
            • –1
              А где написано, что я сравинивал производительность? Я привел цифру для приведенного кода.
              Переключишь с ETS на KVS — цифра изменится, но код не изменится.
              Я вообще думаю, что ненормально приводить DB throughput в статье про веб фремворк.
              Но если для Habrahabr это ок, то пусть будет :-)
              • 0
                А ну ясно. Про странность сравнения согласен. Хотя в общем-то можно показать сколько накладных расходов вносит web framework… Не в курсе, что OK для Habrahabr, что нет…
                • 0
                  У меня все показано в README сколько N2O вносит по сравнению с чистым web server.
                  Там отдельно цифры с сессиями, без сессий, с шаблонами, без шаблонов и т.д.
                  И мои цифры можно проверить, так как тесты выложены github.com/maxlapshin/fpbenchmark
          • 0
            stateless-сервиса очень удобно использовать Basic-аутентификацию

            видимо имелось в виду для stateful, опечатка наверное
            • 0
              stateless конечно. Stateful был бы, если запонимал sessionid.
            • +2
              Спасибо за статью. Если позволите, пара моих постов по связанной теме: раз, два, три, четыре и далее по ссылкам.

              По своему опыту скажу, что для написания REST'ов и микросервисов Haskell просто отлично подходит уже сегодня. Можно еще прикрутить blaze-html и писать веб спокойно.
              • 0
                И вам спасибо за блог eax.me, регулярно его читаю и нахожу очень полезным.
              • +2
                String — список обычных ASCII-символов, восьмибитных, естественно. Этот тип данных встроен в язык.

                Это не совсем так
                String = [Char]
                -- sizeOf ('x' :: Char) == 4
                

                Просто это всё же обычный список со всеми вытекающими, но юникод в нём помещается.
              • +1
                ghc-options: -O3 -threaded -with-rtsopts=-N32

                Попробуйте померить производительность с такими опциями:
                ghc-options: -O3 -threaded -rtsopts "-with-rtsopts=-N -A32m"
                • 0
                  У нас недавно на работе устраивали двухдневный хакатон. Каждый должен был выбрать язык программирования, которым он не владеет и за два дня написать программу: считать из бинарного файла данные и кое-что там посчитать, основываясь на этих данных. Я выбрал хаскель. Ничего из этого не вышло. Даже не получилось считать данные из файла)
                  Но язык, конечно, интересный.
                  • +2
                    это конечно не очень хорошо с моей стороны, но позвольте поинтересоваться, была ли у вас на хакатоне возможность обращаться к книгам, интернету, или общаться с сообществами языков. Просто уж за два то (рабочих) дня при наличии обучаеющего человека можно получить очень неслабое введение в язык, вплоть до того, что потом быть способным работать в команде, в которой есть 1-2 хорошо разбирающихся человека.
                    • 0
                      Я тоже так думал, что смогу разобраться за два дня. В итоге не получилось. Сейчас мне кажется, что все, что вы написали применимо к обычным, си-подобным языкам.
                      Можно было пользоваться чем угодно. У нас даже есть человек, который знает хаскель. Но обращаться к нему, я решил, что не спортивно.
                  • +1
                    Если это пример для начинающих изучать haskell — то человек все равно ничего не поймет. Поскольку объяснены либо очевидные вещи, либо ненужные.

                    Если это пример для человека, который умеет писать на данном языке — то тут нет ничего интересного.
                    И почитайте про линзы, вам поможет многое легче писать, и многие теоретические вещи понять (а оттуда и легкость в практике).

                    И да, зачем типы писать в `makeDbConfig`?
                    • 0
                      Но в целом, если это было написано после не столько продолжительного изучения haskell — то клево, рад видеть в наших рядах ;)
                    • +1
                      Бенчмарки хорошо бы тоже куда-нибудь выложить…
                      • 0
                        > ghc-options: -O3 -threaded -N32

                        это очень странная идея:

                        1. `-O3` все же нету, максимальный уровень оптимизации это `-O2`, но, в целом нужен ли он
                        тоже нужно смотреть, т.к. из-за очень большого инлайна он может работать даже хуже, чем
                        `-O`, что правда случается редко

                        2. `-N32` у вас 32 ядерный ноутбук? Если нет, то используйте или `-N` — количество системных
                        процессов в rts равное количеству ядер, или даже чуть меньше, в противном случае можно
                        получить существенное замедление. Тоже нужно смотреть, что лучше на бенчмарках

                        3. Установка минимального размера для выделения памяти `-A` тоже может сильно помочь,
                        как уже было указано выше.

                        ну и есть пакет ghc-gc-tune, который тоже можно использовать, чтобы подобрать параметры
                        gc, дающие максимальную производительность для данной программы.
                        • 0
                          > Text — тип данных, предназначенный как для ASCII, так и для UTF-символов. Находится в библиотеке text и существует в двух видах: строгой и ленивой. Подробнее — здесь

                          Тип данных предназначенный для хранения текстовой информации внутри программы. В Text хранятся UTF-16, т.е. не может представить все множество представимое типом Char. Является unpinned, т.е. данные могут перемещаться сборщиком мусора. Поддерживает stream-fusion, т.е. операции поддерживающие stream fusion объеденятся в одну мегаоперацию, которая не создает промежуточных структур. Т.е., например, `T.replace «a» «ab». T.replace «c» «de»` не будет создавать промежуточную строку между операциями replace (требует хотя бы -O). Но при этом не поддерживает эффективные блочные операции.

                          > ByteString — предназначен для сериализации строк в поток байтов. Поставляется в библиотеке bytestring и также в двух вариантах: строгом и ленивом.

                          Очень неточное высказываение. Тип данных ByteString предназначен для представления бинарных данных, IO взаимодействий и взаимодействий с внешними функциями (FFI). ByteString представлен массивом байтов (Word8). ByteString является pinned, т.е. гарантировано не будет перемещен сборщиком мусора (что может приводить к фрагментации памяти). Не поддерживает stream-fusion в полной мере. Может использовать блочные операции. В новых версиях есть ShortByteString являющийся unpinned.

                          Как-то так.
                          • 0
                            Что значит восклицательный знак перед типами данных в AuthSettings и зачем он нужен?
                            • 0
                              Сделать поле строгим. Т.е. если данные вычисляются то WHNF (weak head normal form), то и поля отмеченные строгими вычисляются до WHNF. Помогает избежать излишних отложенных вычислений в структурах данных и является общим правилом (уже практически принятым с сообществе).
                              • 0
                                Спасибо, а можете привести несколько примеров, когда он точно нужен, и без него нельзя, когда он точно не нужен, и с ним нельзя, и когда всё равно, с ним или без?
                                • +1
                                  уже ниже привел пример, чтобы лучше понять как оно работает. Существует достаточно простое правило:
                                  если вы знаете, что вам нужна ленивость в поле, т.е. это как-то используется алгоритмами — то не делайте его строгим, в противном случае — делайте
                                  .

                                  Данное правило не совсем хорошее, поскольку на каком-то этапе можно упустить случаи, когда ленивость поможет.

                                  Примеры структур, где нужна ленивость, например структуры использующие для построения завязывание в узел или результаты из «будущего», структуры для мемоизации, структуры в которых хранятся значения большая часть из которых может быть не вычислена, бесконечные структуры. При этом вычисления на ленивых стуктурах проще объединяются (compose)

                                  Примеры структур, где ленивость вредна, элементараные структуры, которые могут накапливать вычисления, например:
                                  `foldl' (\(x, y) c -> (x+c, y*c)) (0,1) [...]` несмотря на то, что свертка строгая в «полях» кортежа будет накапливаться вычисления и вычеслены будут только вконце.

                                  Учитывая сказанное выше, я бы сказал, что строкими нужно делать поля во всяких пользотельстких структурах, на которых не строится control flow.
                                • 0
                                  Ниже пример, поясняющий разницу:

                                  data A = A Int
                                  data B = B Int
                                  data C = C ![Int]
                                  
                                  mk constr conv = const . conv
                                  
                                  Далее в интерпретаторе:
                                  
                                  > let a = mk A id 7
                                  > :sprint a
                                  a = _  -- у нас thunk вместо значения
                                  > (\x@(A _) -> True) a -- вычисляем до WHNF
                                  True
                                  > :sprint a
                                  a = A _  -- конструктор вычислен в поле Thunk
                                  > let b = mk B id 8
                                  > :sprint b
                                  b = _
                                  > (\x@(B _) -> True) b
                                  True
                                  > :sprint b
                                  b = B 8 -- конструктор вычислен и поле тоже вычислено
                                  *Main> let c = mk C id (replicate 7 9)
                                  *Main> (\x@(C _) -> True) c
                                  True
                                  *Main> :sprint c
                                  c = C (9 : _)   -- как видим поле вычислено только до WHNF
                                  

                              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.