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. Да пребудет с вами сила!
