Монады Трансформаторов


Я столкнулся с такой же сценарий с участием монады трансформаторов в несколько раз теперь, и я ищу некоторые "лучшие практики" советы.

Медведь с введения, самого вопроса очень проста:


Допустим, вы дали список входов, а вы хотите, чтобы преобразовать их в обобщенной форме при обращении с возможностью недопустимых входных данных. В код, мы могли бы что-то вроде:

type Input = String
type ProcessedInput = String
type Output = S.Set ProcessedInput
data Error = InvalidFormat Input
           | DuplicateInput ProcessedInput

processInput :: Input -> Maybe ProcessedInput

-- Useful later, not vital to the problem
ifM :: Monad m => m Bool -> m a -> m a -> m a
ifM cond t f = cond >>= \c -> if c then t else f

и мы хотим, что-то вроде:

buildInputs :: [Input] -> Either Error Output

Я обнаружил, что это обычно лучше, чтобы вычислить это внутри маленькой изолированной монады трансформатор стек (StateT вокруг ExceptТак мы можем создать наш результат и обрабатывать ошибки, когда это необходимо), а не делать кучу карт и any проверки и агрегатов.

Так вот вопрос: это "лучше", чтобы выразить это вычисление в качестве исполняющего обязанности на один вклад, или действуя на весь входной? Принимая во внимание, что это полностью автономные вычисления, не должны использоваться в качестве subcomputation в другом месте.


С помощью "единого ввода" действие требует немного нелогично для упаковки в buildInputs:

buildInputs :: [Input] -> Either Error Output
buildInputs is = runExcept $ execStateT builder S.empty
        where builder = sequence (map buildInputs' is)

buildInputs' :: Input -> StateT Output (Except Error) ()
buildInputs' i = case processInput i of
                    Nothing -> throwError (InvalidFormat i)
                    Just p  -> ifM (gets $ S.member p)
                                       (throwError $ DuplicateInput p)
                                       (modify $ S.insert p)

С помощью "единого ввода" действий дает чище фантик на стоимость грязнее тела:

buildInputs :: [Input] -> Either Error Output
buildInputs is = runExcept $ execStateT (buildInputs2' is) S.empty

buildInputs' :: [Input] -> StateT Output (Except Error) ()
buildInputs' [] = return ()
buildInputs' (i:is) = case processInput i of
                        Nothing -> throwError (InvalidFormat i)
                        Just p  -> do
                                    ifM (gets $ S.member p)
                                            (throwError $ DuplicateInput p)
                                            (modify $ S.insert p)
                                    buildInputs' is

Есть ли лучшие практики конвенции для такого рода вещи? Есть совершенно лучший способ сделать это, что я пропустил?

Вот полный код



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

Петли, второй-именно то, что комбинаторы нравится sequence и map предназначены для захвата и фактор ушел. Таким образом, я считаю первую версию более идиоматические.

Всегда можно включить в программу на один вклад в одном на все входы, обернув его в цикле, как ваш второй фрагмент, но не наоборот в общем, потому что программа, по спискам могут принимать нетривиальные использовать оставшийся хвост (is). В этом смысле, ваш первый вариант содержит больше информации, которая теряется при преобразовании его во вторую, и держать его таким образом может улучшить читаемость: мы знаем больше о том, что функция не может сделать.


В меньших масштабах, sequence и map вместе также называются traverseи так как в этом случае мы собираем результаты в отдельную структуру, нам нужно только сложить (traverse_), а не в обход, который ставит индивида () выводит в список. Вот опять же, с помощью более общей функцию, то сигнал о том, что "обычный выход" вычислений (что бы [()]) не имеет никакого отношения.

Когда тело цикла (buildInputs') невелика и вызывается только в одном месте, может быть стоит встраивание его, и for_ иногда лучше, чем traverse_ для этого (for_ = flip traverse_) и делает код выглядеть красиво необходимо, когда он предназначается, чтобы быть.

Рисунок-матч результат process также может быть переработан, чтобы быть спокойнее.

buildInputs :: [Input] -> Either Error Output
buildInputs is = runExcept (execStateT go S.empty) where
go = for_ is $ \i -> do
p <- processInput i `ifFail` throwError (InvalidFormat i)
collides <- gets $ S.member p -- The variable name tells what this is checking
when collides $ throwError (DuplicateInput p)
modify $ S.insert p

ifFail :: Applicative m => Maybe a -> m a -> m a
ifFail (Just a) _ = pure a
ifFail Nothing oops = oops

3
ответ дан 15 марта 2018 в 01:03 Источник Поделиться