суббота, 30 июля 2011 г.

Работа с BSON в Haskell


Разобрав как работать с MongoDB, мы не разобрали главного. Что же делать с полученными из БД данными? На самом деле у опытного Haskell-программиста даже не возникнет такого вопроса.
Но статья не для них. Статья для тех "горячих" новичков, которые прочитали короткий мануал, либо несколько глав из книги по Haskell и сразу принялись за дело.

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

Ну ладно, что уж там. Если вы новичок, то вряд ли это введение вас остановит. Приступим к делу.

Извлекаем Value

Вообще BSON используется не только в MongoDB. Binary JSON, как гласит расшифровка, является форматом общего назначения и довольно часто используется для передачи данных по сети.
Итак, допустим мы сделали запрос к базе данных и получили Document, а если нет - то создадим его сами:
{-# LANGUAGE OverloadedStrings #-}
import Data.Bson
doc = ["id" =: (1 :: Int), "post" =: u "Hello World!", "author" =: u "kreed"]

Теперь у нас есть BSON документ. Сейчас мы узнаем, как извлечь из него Value. Для этого у нас есть две функции:
valueAt :: Label -> Document -> Value
-- и look :: Monad m => Label -> Document -> m Value
Использовать valueAt можно лишь в том случае, когда вы полностью уверены в наличии записи с искомым ключом (Label). Если вы ошибетесь, это вызовет ошибку исполнения.
Функция look, напротив, является безопасным вариантом, т.к. с её помощью мы можем обернуть результат в Maybe-монаду.  Однако это надо указать явно!
 Пример:
valueAt (u "id") doc -- вернет 1 :: Value
valueAt (u"wrong") doc -- exception

look (u"id") doc :: Maybe Value -- вернет Just 1 :: Maybe Value
look (u"wrong") doc :: Maybe Value -- вернет Nothing

Заметьте, мы явно указали предполагаемый тип. Такие моменты описываются в туториалах или книгах. Но описываются лишь на примере функции read.

Извлекаем значение из Value

Мы извлекли Value, теперь нам надо получить значение из Value.
Для этого мы воспользуемся функцией cast:
cast :: forall m a. (Val a, Monad m) => Value -> m a

Для безопасности мы также будем использовать Maybe-монаду. Для этого уже подготовлена функция cast':
cast' :: Value -> Maybe a

Пример:
v = valueAt (u "id") doc
cast v :: Maybe Int -- вернет Just 1
cast v :: Maybe String -- вернет Nothing
-- можно и так, но опасно:
cast v :: IO Int
cast' v :: Maybe Int -- вернет Just 1

К сожалению, при использовании этих функций без контекста, не сразу понятна зачем нужна функция cast'. Ведь и там, и там - приходится указывать тип явно.

Но при использовании в определенном контексте это имеет смысл:
fmap (+5) $ cast' v
-- вместо
fmap (+5) $ cast v :: Maybe Int

Значение можно получить и напрямую из Document. Для этого есть функция lookup:
lookup :: (Val v, Monad m) => Label -> Document -> m v
lookup (u"id") doc :: Maybe Int

Bson.Mapping или ближе к real world

Алгебраические типы с полями в Haskell подобны BSON документу. И там, и там есть ключ (название поля), есть значение определенного типа.
Вы наверное поняли, к чему я веду? Сейчас мы научимся конвертировать алгебраические типы в BSON и обратно!

Для этого нам необходимо включить TemplateHaskell и DeriveDataTypeable расширения и импортировать некоторые модули.
{-# LANGUAGE TemplateHaskell, DeriveDataTypeable #-}
import Data.Bson.Mapping
import Data.Data

Теперь объявим наш алгебраический тип, пускай это будет пост в блоге:
data Post = Post {
      _id :: Int
    , post :: String
    , author :: String
    } deriving (Show, Read, Eq, Ord, Data, Typeable)
$(deriveBson ''Post)

Обратите внимание на последние две строчки. Все указанные тайп-классы обязательны. Последняя строка - обязательная конструкция для TH. Без нее мы не сможем провести конвертацию.
Также наименование полей в алгебраическом типе должны соответствовать ключам в BSON документе.

Для конвертации мы имеем две функции:
toBson :: Bson a => a -> Document
fromBson :: (Bson a, Monad m) => Document -> m a

Попробуем их применить:
tryMapping = do
  let post = Post 23 "Hello World!" "WeAre"
  print $ toBson post
  print =<< (fromBson (toBson post) :: IO Post)

Выполнение функции выведет следующее:
[ _id: 23, post: "Hello World!", author: "WeAre", _cons: "Post"]
Post {_id = 23, post = "Hello World!", author = "WeAre"}

Заметили? В BSON документе у нас появилось еще одно поле "_cons : Post". Это поле указывает конструктор типа нашего алгебраического типа. Без него обратная конвертация, т.е. из BSON в алгебраический тип - невозможна!

Bson.Binary или конвертируем в ByteString!
Для передачи нашего BSON документа по сети или для записи его в файл, нам необходимо конвертировать его в ByteString.
Сделать это просто, для этого существует модуль Data.Bson.Binary и функции putDocument, getDocument.
Для их использования нам также понадобятся Put и Get монады, которые живут в модулях Data.Binary.Get и Data.Binary.Put.

Напишем функции для конвертации:
import Data.Binary.Get
import Data.Binary.Put
import Data.Bson (Document)
import Data.Bson.Binary
import Data.ByteString.Lazy (ByteString)

bsonToBs :: Document -> ByteString
bsonToBs = runPut . putDocument

bsToBson :: ByteString -> Document 
bsToBson = runGet getDocument


Итоги

Подводя итоги, хочется сказать, что все эти моменты понятны опытному программисту. Но человек, который пришел с императивного ЯП, человек у которого чешутся руки - вряд ли будет знать, что делать с Put и Get монадами, где и для чего указывать явный тип. Вряд ли он когда-нибудь видел использование TemplateHaskell.

Но это все базовые знания. Ведь Haskell не из тех языков, которые можно понять быстрее в практике.

Комментариев нет:

Отправить комментарий