Skip to content

Instantly share code, notes, and snippets.

@drchaos
Last active May 11, 2020 13:39
Show Gist options
  • Save drchaos/0a9d98a9058da47ffcf6758902303e7e to your computer and use it in GitHub Desktop.
Save drchaos/0a9d98a9058da47ffcf6758902303e7e to your computer and use it in GitHub Desktop.
Возьмем для примера 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