Last active
May 11, 2020 13:39
-
-
Save drchaos/0a9d98a9058da47ffcf6758902303e7e to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Возьмем для примера Influx Line Protocol, поскольку он прост. Приходят к нам в порт данные и | |
мы должны их как-то распарсить, а потом что-то с ними сделать. Для простоты возьмём только поля (Fileds) и засунем их | |
в Map. | |
> type Name = Text | |
> type Value = Text | |
> type Fields = Map Name Value | |
Это нетипизированные данные, а мы хотим прочитать оттуда RADIUS Packet Attributes которые весьма даже типизированны | |
https://hackage.haskell.org/package/radius-0.6.1.0/docs/Network-RADIUS-Types.html | |
В итоге мы хотим на первом этапе такую функцию. | |
> parseRADUIS :: Fields -> [PacketAttribute] | |
Задачи парсинга нету и нам надо просто преобразовать строки. Тело функции я показывать не буду для простоты. | |
> toLBS :: Text -> ByteString | |
Поэтому мы просто ищем в ассоциативном контейнере Map. Вот сигнатура этой функции | |
> lookup :: key -> Map key val -> Maybe val | |
Maybe - это монада. Что нам это даёт? To что мы можем сделать функцию | |
> parseTextAttr :: (ByteString -> PacketAttribute) -> Name -> Fields -> Maybe PacketAttribute | |
Первый параметр это функуция, которая делает из ByteString PacketAttribute, в нашем случае это просто конструктор АДТ | |
реализация с явным bind (>>=) | |
> parseTextAttr mkAttr n fields = do | |
> lookup n fields >>= return . mkAttr . toLBS | |
В do нотации | |
> parseTextAttr mkAttr n fields = do | |
> val <- lookup n fields | |
> return $ mkAttr $ toLBS $ val | |
Теперь делаем из этого список аттрибутов, но нам нужен [PacketAttribute]. | |
> parseRADUIS :: Fields -> [Maybe PacketAttribute] | |
> parseRADUIS = | |
> [ parseTextAttr AcctSessionIdAttribute "session_id" fields | |
> , parseTextAttr CallingStationIdAttribute "src_addr" fields | |
> , parseTextAttr CalledStationIdAttribute "dst_addr" fields | |
> ] | |
Мы берём функцию, которая фильтрует список от Nothing | |
> catMaybes :: [Maybe a] -> [a] | |
и получаем то что хотели | |
> parseRADUIS :: Fields -> [PacketAttribute] | |
> parseRADUIS = catMaybes | |
> [ parseTextAttr AcctSessionIdAttribute "session_id" fields | |
> , parseTextAttr CallingStationIdAttribute "src_addr" fields | |
> , parseTextAttr CalledStationIdAttribute "dst_addr" fields | |
> ] | |
С этой функцией есть иная проблема если ничего не найдено, то мы просто посылаем пустой список вот функция, | |
которая считает все поля обязательными. Hold me beer! | |
Надо поменять catMaybes на secuence https://hackage.haskell.org/package/base-4.14.0.0/docs/Control-Monad.html#v:sequence | |
> parseRADUISSeq :: Fields -> Maybe [PacketAttribute] | |
> parseRADUISSeq = sequence | |
> [ parseTextAttr AcctSessionIdAttribute "session_id" fields | |
> , parseTextAttr CallingStationIdAttribute "src_addr" fields | |
> , parseTextAttr CalledStationIdAttribute "dst_addr" fields | |
> ] | |
В этом случае если нет какого-то поля, то нет и списка. Но если это ошибка ввода, то надо сказать в лог чего не хватает. | |
Нам нужна информация об ошибке. | |
Вместо Maybe возьмем Either, который тоже монада | |
> type ErrMonad a = Either String a | |
> lookupE :: :: key -> Map key val -> ErrMonad val | |
> lookupE k m = case lookup k m | |
> Just v -> Right v | |
> Nothing -> Left (show k ++ " field is not found") | |
> | |
> parseTextAttrE :: (ByteString -> PacketAttribute) -> Name -> Fields -> ErrMonad PacketAttribute | |
> parseTextAttrE mkAttr n fields = do | |
> lookupE n fields >>= return . mkAttr . toLBS | |
> | |
Как видно выше я просто поменял тип Maybe на ErrMonad, а lookup на lookupE. | |
> parseRADUISE :: Fields -> [PacketAttribute] | |
> parseRADUISE = rights | |
> [ parseTextAttrE AcctSessionIdAttribute "session_id" fields | |
> , parseTextAttrE CallingStationIdAttribute "src_addr" fields | |
> , parseTextAttrE CalledStationIdAttribute "dst_addr" fields | |
> ] | |
В этой функции я заменил parseTextAttr на parseTextAttrE, а catMaybes на | |
rights http://hackage.haskell.org/package/base-4.14.0.0/docs/Data-Either.html#v:rights | |
> parseRADUISSeqE :: Fields -> ErrMonad [PacketAttribute] | |
> parseRADUISSeqE = sequence | |
> [ parseTextAttrE AcctSessionIdAttribute "session_id" fields | |
> , parseTextAttrE CallingStationIdAttribute "src_addr" fields | |
> , parseTextAttrE CalledStationIdAttribute "dst_addr" fields | |
> ] | |
И в этой функции меняется ещё меньше. parseTextAttr -> parseTextAttrE . Благодаря монадам, код почти не меняется! | |
И на закуску. У нас один из аттрибутов теперь хочет не ByteString а IP. У нас появляется разбор и возможна ошибка | |
разбора. Для простоты я приведу только сигнатуру. | |
> toIP :: Text -> ErrMonad IP | |
Функция разбора аттрибута с IP. Изменения тоже минимальны. | |
> parseIPAttrE :: (IP -> PacketAttribute) -> Name -> Fields -> ErrMonad PacketAttribute | |
> parseIPAttrE mkAttr n fields = do | |
> lookupE n fields >>= return . mkAttr . toIP | |
А в эти функции просто добавилась строка в список | |
> parseRADUISE :: Fields -> [PacketAttribute] | |
> parseRADUISE = rights | |
> [ parseTextAttrE AcctSessionIdAttribute "session_id" fields | |
> , parseTextAttrE CallingStationIdAttribute "src_addr" fields | |
> , parseTextAttrE CalledStationIdAttribute "dst_addr" fields | |
> , parseIPAttrE NASIPAddress "src_addr" fields | |
> ] | |
> | |
> parseRADUISSeqE :: Fields -> ErrMonad [PacketAttribute] | |
> parseRADUISSeqE = sequence | |
> [ parseTextAttrE AcctSessionIdAttribute "session_id" fields | |
> , parseTextAttrE CallingStationIdAttribute "src_addr" fields | |
> , parseTextAttrE CalledStationIdAttribute "dst_addr" fields | |
> , parseIPAttrE NASIPAddress "src_addr" fields | |
> ] | |
Что я хотел показать? Я хотел показать, что монады помогают значительно лучше комбинировать куски кода между собой. | |
В примере выше монадические функции практически не меняются и хорошо видно что мы можем получить разные свойства | |
слегка меняя код. Самое важное тут что изменение стратегии обработки ошибок получается практически ортогонально | |
коду. Это хорошо видно для чистых фунций, но все точно так же может работать и для IO. Например, у нас несколько IP и | |
надо сделать запрос к одному из них и пробовать дальше только в случае ошибки. | |
> tryAll :: (IP -> IO (Maybe a)) -> [IP] -> IO (Maybe a) | |
> tryAll req ips = runMaybeT $ msum $ MaybeT . req <$> ips | |
Этот однострочник взрывает мозг, потому что благодаря ленивости выполняется только все запросы до первого успешного. Я | |
его привожу просто для примера. | |
P.S. Код специально написан просто хотя тут есть много путей для обобщения я намерено этого избегал. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment