Стандартный комбинатор для получения первого «непустого» значения из набора действий monadi c - PullRequest
1 голос
/ 18 января 2020

Я уверен, что упускаю что-то очень очевидное здесь. Вот что я пытаюсь достичь на концептуальном уровне:

action1 :: (MonadIO m) => m [a]
action1 = pure []

action2 :: (MonadIO m) => m [a]
action2 = pure [1, 2, 3]

action3 :: (MonadIO m) => m [a]
action3 = error "should not get evaluated"

someCombinator [action1, action2, action3] == m [1, 2, 3]

Существует ли это гипотетическое someCombinator? Я пытался играть с <|> и msum, но не смог получить то, что хочу.

Я думаю, это можно обобщить двумя способами:

-- Will return the first monadic value that is NOT an mempty 
-- (should NOT blindly execute all monadic actions)
-- This is something like the msum function

someCombinator :: (Monoid a, Monad m, Traversable t, Eq a) => t m a -> m a

-- OR

-- this is something like the <|> operator

someCombinator :: (Monad m, Alternative f) => m f a -> m f a -> m f a

Ответы [ 2 ]

3 голосов
/ 18 января 2020

Мне не известна библиотека, которая обеспечивает это, но это не сложно реализовать:

someCombinator :: (Monoid a, Monad m, Foldable t, Eq a) => t (m a) -> m a 
someCombinator = foldr f (pure mempty)
    where
        f item next = do
            a <- item
            if a == mempty then next else pure a

Обратите внимание, что вам даже не нужно Traversable: Foldable достаточно.

2 голосов
/ 18 января 2020

На абстрактном уровне, первое непустое значение - это Monoid, называемое First. Однако оказывается, что если вы просто наивно поднимите значения IO в First, у вас возникнет проблема с action3, , поскольку операция моноидального добавления по умолчанию строго соответствует IO* 1010. *.

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

Помимо вышеупомянутой оболочки FirstIO, вы можете найти эту функцию полезной :

guarded :: Alternative f => (a -> Bool) -> a -> f a
guarded p x = if p x then pure x else empty

Я просто скопировал его из Protolude , поскольку в base я не смог найти нужную функциональность. Вы можете использовать его, чтобы обернуть свои списки в Maybe, чтобы они соответствовали FirstIO:

> guarded (not . null) [] :: Maybe [Int]
Nothing
> guarded (not . null) [1, 2, 3] :: Maybe [Int]
Just [1,2,3]

Сделайте это для каждого действия в вашем списке действий и оберните их в FirstIO.

> :t (firstIO . fmap (guarded (not . null))) <$> [action1, action2, action3]
(firstIO . fmap (guarded (not . null))) <$> [action1, action2, action3]
  :: Num a => [FirstIO [a]]

В приведенном выше фрагменте GHCi я показываю только тип с :t. Я не могу показать значение, так как FirstIO не имеет экземпляра Show. Суть, однако, в том, что теперь у вас есть список FirstIO значений, из которых mconcat выберет первое непустое значение:

> getFirstIO $ mconcat $ (firstIO . fmap (guarded (not . null))) <$> [action1, action2, action3]
Just [1,2,3]

Если вы хотите распаковать Maybe, Вы можете использовать fromMaybe из Data.Maybe:

answer :: IO [Integer]
answer =
  fromMaybe [] <$>
  (getFirstIO $ mconcat $ (firstIO . fmap (guarded (not . null))) <$> [action1, action2, action3])

Это явно сложнее, чем ответ Федора Сойкина, но я очарован тем, как Haskell позволяет вам собирать нужные функции с помощью ' щелкая вместе существующие вещи, почти как кирпичи Le go.

Итак, на вопрос существует ли этот комбинатор? ответ в том, что он вроде как, но какая-то сборка требуется.

...