Too late — 'cause I got it now
there are monads all around
IO, State and lists abound
It's easy, like those people say
but my program got abstracted all away!
Maybe — o o o,
It's a monad too, I know
Why should I use another language at all?
Снова безумный адепт Haskell, и еще одна попытка доказать его практичность. Нестареющая классика.
Я постараюсь рассказать шикарную историю
Немного серьезности никогда не помешает. Поэтому сначала, без малейшего намека на юмор, расскажу логику написания этого текста. Мне хотелось (прежде всего, для себя, но надеюсь, кому-нибудь тоже будет интересно) реализовать на Haskell некую до боли близкую, неимоверно практичную задачу. Положительный результат решения этой задачи дал бы лишний повод гордиться собой, скилы и еще один довод в пользу выбора этого языка программирования. В качестве подопытной задачи я выбрал получение и обработку информации о коммитах в репозиторий на github. Собственно, она будет содержать в себе работу с github api — загрузка и парсинг json.
Полагаю, что решать ее стоит по шагам, поэтому начнем с исходной позиции, а именно пустой директории в файловой системе.
Создание модуля
Для начала, создадим новый модуль для наших целей
cabal init
Пытливый cabal задаст несколько вопросов, а в результате вы получите заготовку модуля с конфигурационным файлом project_name.cabal. Для большей эстетики добавим в модуль директорию src, и укажем ее в конфигурации
executable project-name
hs-source-dirs: src
main-is: Main.hs
Конечно, Main.hs необходимо создать)
Дальше пару слов о dependency hell. Это больная тема Haskell, в которой намечается прогресс. Вариантов решения проблемы зависимостей несколько, но мы молоды и любим все модное, поэтому будем использовать свежую фичу cabal-1.18 — sandoxes.
Собственно, для использования необходимо инициализировать песочницу и установить зависимости
cabal sandbox init
cabal install --only-dependencies
В дальнейшем для сборки модуля можно, как обычно, воспользоваться командой
cabal build
Если возникло острое желание что-нибудь поотлаживать, да и вообще, посмотреть, как оно работает изнутри (а, по законам жанра, такое желание обязательно возникнет), можно запустить ghci в созданной песочнице командой
cabal repl
Все, боязнь пустого каталога преодолена, двигаемся дальше.
http-conduit
Первая задача, которую необходимо решить — это загрузка информации о комитах в json формате. Собственно, источник очевиден, но на этом простые вещи заканчиваются. Итак, на этом этапе будем использовать пакет http-conduit за авторством солнцеликого
Для начала, надо добавить нужные зависимости в раздел build-depends конфигурационного файла
bytestring >= 0.10,
conduit >= 1.0,
http-conduit >= 1.9,
и обновить песочницу описанной выше командой.
Вот теперь можем трепетно приступить к коду. Для начала, чтобы упростить себе жизнь и работу со строками, добавим extension
{-# LANGUAGE OverloadedStrings #-}
Подключаем нужные модули
import Data.Conduit
import Network.HTTP.Conduit
import qualified Data.Conduit.Binary as CB
import qualified Data.ByteString.Char8 as BS
Весь код загрузки json будет выглядеть примерно так
main = do
manager <- newManager def
req <- parseUrl "https://api.github.com/../.."
let headers = requestHeaders req
req' = req {
requestHeaders = ("User-agent", "some-app") :
headers
}
runResourceT $ do
res <- http req' manager
responseBody res $$+- CB.lines =$ parserSink
Насколько я помню, api github требует наличия заголовка User-agent, поэтому пришлось немного расширить request. Основное действо происходит в последних двух строках, где мы получает ответ с json. Т.к. результат завернут в трансформер ResourceT, то функции для его получения должны быть вызваны с использованием runResourceT. После получения тела ответа мы отправляем его в сток, который предназначен для разбора json и выглядит он так
parserSink :: Sink BS.ByteString (ResourceT IO) ()
parserSink = do
md <- await
case md of
Nothing -> return ()
Just d -> parseCommits d
Сток в случае успеха будет просто разбирать полученный json и выводить его на экран (эта часть магии скрыта в функции parseCommits).
Aeson
Продолжаем коверкать мышление программистов и переходим к парсингу. Для него будем использовать чрезвычайно могучий пакет под названием Aeson. На самом деле, здесь все достаточно просто, но есть несколько моментов, которые с непривычки вводят в ступор:
- Т.к. Haskell строго типизирован, то нам потребуются типы, которые будут описывать заложенную в json структуру данных
- Если я ничего не перепутал, то Aeson использует lazy bytestring, в то время как в стоке оказывается strict bytestring, поэтому придется продемонстрировать навыки жонглирования типами
Итак, сначала определим типы. Можно не заморачиваться, и определить их лишь частично, отправив часть информации из json в топку. Себе оставим только url, хэш и commit message.
import qualified Data.ByteString.Char8 as BS
import Data.Aeson (FromJSON(..))
data CommitInfo = CommitInfo {
message :: BS.ByteString
} deriving (Show)
data Commit = Commit {
sha :: BS.ByteString,
url :: BS.ByteString,
commit :: CommitInfo
} deriving (Show)
Дальне нам было бы канонично использовать аппликативные функторы для сопоставления json и полей из структур данных, но мы всех обманем и воспользуемся Generic'ом.
{-# LANGUAGE DeriveGeneric #-}
import GHC.Generics (Generic)
и добавим к имеющимся структурам данных наследование от Generic
deriving(Show, Generic)
Останется только заявить о возможности создания Commit & CommitInfo из json
instance FromJSON Commit
instance FromJSON CommitInfo
Осталось всего несколько шагов до финиша, мы почти у цели
parseCommits :: BS.ByteString -> Sink BS.ByteString (ResourceT IO) ()
parseCommits rawData = do
let parsedData = decode $ BL.fromChunks [rawData] :: Maybe [Models.Commit]
case parsedData of
Nothing -> liftIO $ BS.putStrLn "Parse error"
Just commits -> liftIO $ printCommits commits
Как видите, приходится создавать lazy bytestring для отдачи на декодирование. Если парсинг прошел успешно, с помощью liftIO поднимаем полученные значения и выводим в консоль.
Finish
Все, красная дорожка, фанфары и торжественное завершение вечера. Полный пример расположен здесь. Код не является примером торжества идеалов computer science, поэтому замечания от гуру приветствуются. Надеюсь, все остальные чему-нибудь научились, или хотя бы получили удовольствие и стали ближе к миру Haskell. Да пребудет с вами сила!