И тут-то меня ждало разочарование: я не был способен написать ничего кроме 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Мб.
Комментарии (30)
5HT
07.05.2015 18:00+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 RPSnetslavehq
07.05.2015 22:56+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 -------------------------------------------------------------------------------
ivlis
08.05.2015 02:40Да уж, Erlang с Cowboy сервером тут просто «рулят и бибкают». Но Erlang для этого и создавался…
qnikst
08.05.2015 13:29сравнивать производительность сервиса работающего с erts и сервиса работающего с mariadb это нормально?
5HT
08.05.2015 14:00-1А где написано, что я сравинивал производительность? Я привел цифру для приведенного кода.
Переключишь с ETS на KVS — цифра изменится, но код не изменится.
Я вообще думаю, что ненормально приводить DB throughput в статье про веб фремворк.
Но если для Habrahabr это ок, то пусть будет :-)qnikst
08.05.2015 14:13А ну ясно. Про странность сравнения согласен. Хотя в общем-то можно показать сколько накладных расходов вносит web framework… Не в курсе, что OK для Habrahabr, что нет…
5HT
08.05.2015 14:16У меня все показано в README сколько N2O вносит по сравнению с чистым web server.
Там отдельно цифры с сессиями, без сессий, с шаблонами, без шаблонов и т.д.
И мои цифры можно проверить, так как тесты выложены github.com/maxlapshin/fpbenchmark
afiskon
07.05.2015 18:53+2Спасибо за статью. Если позволите, пара моих постов по связанной теме: раз, два, три, четыре и далее по ссылкам.
По своему опыту скажу, что для написания REST'ов и микросервисов Haskell просто отлично подходит уже сегодня. Можно еще прикрутить blaze-html и писать веб спокойно.D_Bushenko Автор
07.05.2015 19:22И вам спасибо за блог eax.me, регулярно его читаю и нахожу очень полезным.
VoidEx
07.05.2015 18:56+2String — список обычных ASCII-символов, восьмибитных, естественно. Этот тип данных встроен в язык.
Это не совсем так
String = [Char] -- sizeOf ('x' :: Char) == 4
Просто это всё же обычный список со всеми вытекающими, но юникод в нём помещается.
Dimchansky
07.05.2015 21:30+1ghc-options: -O3 -threaded -with-rtsopts=-N32
Попробуйте померить производительность с такими опциями:
ghc-options: -O3 -threaded -rtsopts "-with-rtsopts=-N -A32m"
khdavid
07.05.2015 23:56У нас недавно на работе устраивали двухдневный хакатон. Каждый должен был выбрать язык программирования, которым он не владеет и за два дня написать программу: считать из бинарного файла данные и кое-что там посчитать, основываясь на этих данных. Я выбрал хаскель. Ничего из этого не вышло. Даже не получилось считать данные из файла)
Но язык, конечно, интересный.qnikst
08.05.2015 18:22+2это конечно не очень хорошо с моей стороны, но позвольте поинтересоваться, была ли у вас на хакатоне возможность обращаться к книгам, интернету, или общаться с сообществами языков. Просто уж за два то (рабочих) дня при наличии обучаеющего человека можно получить очень неслабое введение в язык, вплоть до того, что потом быть способным работать в команде, в которой есть 1-2 хорошо разбирающихся человека.
khdavid
08.05.2015 21:40Я тоже так думал, что смогу разобраться за два дня. В итоге не получилось. Сейчас мне кажется, что все, что вы написали применимо к обычным, си-подобным языкам.
Можно было пользоваться чем угодно. У нас даже есть человек, который знает хаскель. Но обращаться к нему, я решил, что не спортивно.
JagaJaga
08.05.2015 03:21+1Если это пример для начинающих изучать haskell — то человек все равно ничего не поймет. Поскольку объяснены либо очевидные вещи, либо ненужные.
Если это пример для человека, который умеет писать на данном языке — то тут нет ничего интересного.
И почитайте про линзы, вам поможет многое легче писать, и многие теоретические вещи понять (а оттуда и легкость в практике).
И да, зачем типы писать в `makeDbConfig`?JagaJaga
08.05.2015 03:25Но в целом, если это было написано после не столько продолжительного изучения haskell — то клево, рад видеть в наших рядах ;)
qnikst
08.05.2015 13:21> ghc-options: -O3 -threaded -N32
это очень странная идея:
1. `-O3` все же нету, максимальный уровень оптимизации это `-O2`, но, в целом нужен ли он
тоже нужно смотреть, т.к. из-за очень большого инлайна он может работать даже хуже, чем
`-O`, что правда случается редко
2. `-N32` у вас 32 ядерный ноутбук? Если нет, то используйте или `-N` — количество системных
процессов в rts равное количеству ядер, или даже чуть меньше, в противном случае можно
получить существенное замедление. Тоже нужно смотреть, что лучше на бенчмарках
3. Установка минимального размера для выделения памяти `-A` тоже может сильно помочь,
как уже было указано выше.
ну и есть пакет ghc-gc-tune, который тоже можно использовать, чтобы подобрать параметры
gc, дающие максимальную производительность для данной программы.
qnikst
08.05.2015 13:46> 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.
Как-то так.
baltazar_bz
08.05.2015 14:37Что значит восклицательный знак перед типами данных в AuthSettings и зачем он нужен?
qnikst
08.05.2015 14:44Сделать поле строгим. Т.е. если данные вычисляются то WHNF (weak head normal form), то и поля отмеченные строгими вычисляются до WHNF. Помогает избежать излишних отложенных вычислений в структурах данных и является общим правилом (уже практически принятым с сообществе).
baltazar_bz
08.05.2015 14:50Спасибо, а можете привести несколько примеров, когда он точно нужен, и без него нельзя, когда он точно не нужен, и с ним нельзя, и когда всё равно, с ним или без?
qnikst
08.05.2015 15:11+1уже ниже привел пример, чтобы лучше понять как оно работает. Существует достаточно простое правило:
если вы знаете, что вам нужна ленивость в поле, т.е. это как-то используется алгоритмами — то не делайте его строгим, в противном случае — делайте
.
Данное правило не совсем хорошее, поскольку на каком-то этапе можно упустить случаи, когда ленивость поможет.
Примеры структур, где нужна ленивость, например структуры использующие для построения завязывание в узел или результаты из «будущего», структуры для мемоизации, структуры в которых хранятся значения большая часть из которых может быть не вычислена, бесконечные структуры. При этом вычисления на ленивых стуктурах проще объединяются (compose)
Примеры структур, где ленивость вредна, элементараные структуры, которые могут накапливать вычисления, например:
`foldl' (\(x, y) c -> (x+c, y*c)) (0,1) [...]` несмотря на то, что свертка строгая в «полях» кортежа будет накапливаться вычисления и вычеслены будут только вконце.
Учитывая сказанное выше, я бы сказал, что строкими нужно делать поля во всяких пользотельстких структурах, на которых не строится control flow.
qnikst
08.05.2015 14:56Ниже пример, поясняющий разницу:
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
tytar
Вроде должно быть наоборот POST — создание, PUT — обновление
D_Bushenko Автор
Спасибо, поправил.
netslavehq
По аналогии с
обновление статьи тоже должно быть по id