Построение минимального примера Haskell для обработки ошибок в монаде State

Я скручиваю свой мозг в узлы, пытаясь понять, как соединить монаду State с Maybe.

Давайте начнем с конкретного (и намеренно тривиального/ненужного) примера, в котором мы используем монаду State для нахождения суммы списка чисел:

import Control.Monad.State

list :: [Int]
list = [1,4,5,6,7,0,3,2,1]

adder :: Int
adder = evalState addState list

addState :: State [Int] Int
addState = do
  ms <- get
  case ms of
    []     -> return 0
    (x:xs) -> put xs >> fmap (+x) addState

Прохладно.

Теперь давайте изменим его так, чтобы он возвращал Nothing, если список содержит число 0. Другими словами, evalState addState' list должно возвращать Nothing (поскольку list содержит 0). Я думал, что это может выглядеть примерно так...

addState' :: State [Int] (Maybe Int)
addState' = do
  ms <- get
  case ms of
    [] -> return (Just 0)
    (0:xs) -> return Nothing
    (x:xs) -> put xs >> fmap (fmap (+x)) addState'

... это работает, но я предполагаю, что есть лучший способ сделать это...

Я играл с StateT и MaybeT и не могу заставить их работать. Я просмотрел пару вступлений к трансформерам Monad, но они либо не затрагивали эту конкретную комбинацию (т. е. State + Maybe), либо примеры были слишком сложными для моего понимания.

TL;DR: Буду признателен, если кто-нибудь покажет, как написать этот (по общему признанию тривиальный) фрагмент кода, используя StateT и MaybeT (два примера). (Я предполагаю, что невозможно написать этот код без использования трансформаторов — это неверно?)

P.S. Насколько я понимаю, StateT, вероятно, лучше подходит для этого примера, но концептуально было бы полезно увидеть оба примера, если не слишком много проблем.

Обновление: Как указал @Brenton Alker, моя первая версия приведенного выше кода не работает из-за простой опечатки (мне не хватало апострофа). Чтобы сфокусировать вопрос на использовании StateT/MaybeT, я исправляю сообщение выше. Просто хотел включить эту заметку, чтобы придать контекст своему сообщению.


person iceman    schedule 24.11.2014    source источник


Ответы (3)


Тип, который я бы рекомендовал использовать, это:

StateT [Int] Maybe Int

Действительно простой способ использования Maybe/MaybeT состоит в том, чтобы просто вызывать mzero всякий раз, когда вы хотите потерпеть неудачу, и mplus всякий раз, когда вы хотите восстановиться после неудачного вычисления. Это работает, даже если они находятся внутри других преобразователей монад.

Вот пример:

addState' :: StateT [Int] Maybe Int
addState' = do
  ms <- get
  case ms of
    []     -> return 0
    (0:xs) -> mzero
    (x:xs) -> put xs >> fmap (fmap (+x)) addState

-- This requires generalizing the type of `addState` to:
addState :: Monad m => StateT [Int] m Int

Обратите внимание, что я написал это таким образом, что не использовал никаких операций, специфичных для Maybe. На самом деле, если вы позволите компилятору вывести сигнатуру типа, вместо этого он выведет этот более общий тип:

addState' :: MonadPlus m => StateT [Int] m Int

Это работает, потому что StateT имеет следующий экземпляр MonadPlus:

instance MonadPlus m => MonadPlus (StateT s m) where ...

И Maybe будет проверять тип как экземпляр MonadPlus, поэтому приведенный выше код работает, когда мы специализируем m на Maybe.

person Gabriel Gonzalez    schedule 24.11.2014
comment
Вы знаете, как мы будем запускать эту функцию StateT? Ваш код компилируется (при условии, что мы исправим апостроф в моем исходном сообщении, как указано @Brenton Alker), но я не могу запустить его, используя следующее (в GHCi): evalState addState' list. Какие-либо предложения? - person iceman; 24.11.2014
comment
@DipakC, evalStateT - person luqui; 24.11.2014

Я считаю, что ваше решение в основном правильное, у вас просто есть несколько незначительных проблем.

  1. В вашем рекурсивном вызове addState отсутствует штрих, т.е. это должно быть addState' (я подозреваю, что это просто проблема при вставке вопроса, учитывая сообщение об ошибке)
  2. Вы утверждаете adder :: Int, но в новой версии должно быть adder :: Maybe Int - я думаю, это ошибка типа, которую вы получаете.

К сожалению, сейчас у меня нет ресурсов, чтобы попробовать версию трансформеров.

person Brenton Alker    schedule 24.11.2014
comment
Да, вы действительно правы насчет апострофа... Красное лицо - person iceman; 24.11.2014

В этом примере реализован простой стек с использованием MaybeT (State [Int]) Int.

У нас есть монада State, которая содержит s -> (a, s) как s :: [Int] (стек) и a :: Maybe Int (просто элемент, который вы получаете/помещаете в стек или ничего, когда пытаетесь извлечь что-то из пустого стека).

-- -----------------------------------------
-- -----------------------------------------
import Control.Monad.Trans.State
import Control.Monad.Trans.Maybe

-- -----------------------------------------
-- -----------------------------------------
pop2 :: MaybeT (State [Int]) Int
pop2 = MaybeT . state $ \xx -> case xx of
  [] -> (Nothing, [])
  x:xs -> (Just x, xs)

push2 :: Int -> MaybeT (State [Int]) Int
push2 x =  MaybeT . state $ \xs -> (Just x, x:xs)

-- -----------------------------------------
-- -----------------------------------------
dup2 = do
  x <- pop2
  push2 x
  push2 x

-- -----------------------------------------
-- -----------------------------------------
main = do
  print $ runState (runMaybeT dup2) [12, 34, 56]
  print $ runState (runMaybeT dup2) []

При запуске программы получаем:

(Just 12,[12,12,34,56]) 
(Nothing,[])

Давайте рассмотрим типы:

Первоначально,

MaybeT :: m (Maybe a) -> MaybeT m a (m монада, заключающая в себе Maybe a)

Мы используем m == State [Int] и a == Int

Поэтому

MaybeT :: (State [Int]) (Maybe Int) -> MaybeT (State [Int]) Int

и

runMaybeT :: MaybeT m a -> m (Maybe a) == MaybeT (State [Int]) Int -> (State [Int]) (Maybe Int) (runMaybeT вытаскивает то, что вкладывает MaybeT).

и

runState of (State [Int]) (Maybe Int) == [Int] -> ((Maybe Int), [Int]) (runState вытаскивает то, что (State [Int]) (Maybe Int) закрывает).

person cibercitizen1    schedule 04.10.2018