Haskell, как что-то очень близкое, или получаем комиты из github api

от автора

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 за авторством солнцеликого Edward Snow Michael Snoyman. В целом, conduit — это замечательное решение для работы с потоками данных. У меня врядли получится хорошо об этом рассказать, поэтому добро пожаловать в блог человека по фамилии eax. Я расскажу совсем чуть-чуть и по периферии.

Для начала, надо добавить нужные зависимости в раздел 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. Да пребудет с вами сила!

ссылка на оригинал статьи http://habrahabr.ru/post/205830/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *