Подтвердить что ты не робот

Соответствующее использование Monad `fail` vs. MonadPlus` mzero`

Это вопрос, который возник несколько раз для меня в коде проекта, особенно в библиотеках. Кажется, в нем есть некоторый интерес, поэтому я подумал, что это может сделать хорошую вики сообщества.

Метод fail в Монаде рассматривается некоторыми как бородавка; несколько произвольное дополнение к классу, которое не исходит из исходной теории категорий. Но, конечно, в текущем состоянии вещей многие типы Monad имеют логические и полезные экземпляры fail.

Класс MonadPlus - это подкласс Monad, который предоставляет метод mzero, который логически инкапсулирует идею отказа в монаде.

Итак, дизайнер библиотеки, который хочет написать некоторый монадический код, который выполняет какую-то обработку отказа, может выбрать, чтобы его код использовал метод fail в Монаде или ограничил его код классом MonadPlus, чтобы он мог чувствовать хорошо использовать mzero, хотя он не заботится о моноидальном объединении операции mplus вообще.

Некоторые дискуссии по этому вопросу находятся на этой странице wiki о предложения по реформированию класса MonadPlus.


Итак, у меня есть один конкретный вопрос:

Какие экземпляры монады, если они есть, имеют естественный метод fail, но не могут быть экземплярами MonadPlus, потому что у них нет логической реализации для mplus?

Но меня в основном интересует обсуждение этой темы. Спасибо!


РЕДАКТИРОВАТЬ. Последняя мысль произошла со мной. Недавно я узнал (хотя он и есть в документах для fail), что монадическая нотация "делать" снимается таким образом, что ошибки совпадения шаблонов, как и в (x:xs) <- return [], вызывают монаду fail.

Похоже, что на разработчиков языков должно было сильно повлиять перспектива некоторой автоматической обработки отказа, встроенной в синтаксис haskell при включении fail в Monad.

4b9b3361

Ответ 1

Подумайте о Either. Монадический экземпляр выглядит так:

{-# LANGUAGE FlexibleInstances #-}
instance Monad (Either String) where
  (Left x) >>= _   = Left x
  (Right a)  >>= f = f a
  return           = Right
  fail             = Left

(Нам нужно FlexibleInstances, чтобы разрешить экземпляр типа Either String)
Поэтому в основном это как Maybe с необязательным сообщением об ошибке, если что-то происходит. Вы не можете воссоздать это с помощью mzero, так как вы не можете добавить сообщение об ошибке к ошибке. Он несколько отличается от fail.

Каждый экземпляр mplus должен удовлетворять этим двум законам:

mzero `mplus` a -> a
a `mplus` mzero -> a

Прост, не так ли? Но эти законы делают особенным. С их помощью можно написать для него разумный экземпляр MonadPlus:

instance MonadPlus (Either a) where
  mzero = Left undefined
  mplus (Left _) b = b
  mplus a _        = a

Что это? Он представляет собой выбор. Если первое вычисление будет успешным, оно будет возвращено. Else, mplus возвращает второе вычисление. Обратите внимание, что он отличается от (>>), который не удовлетворяет законам:

Left a   >>    Right b -> Left a
Left a `mplus` Right b -> Right b

(>>) остановится при первом вычислении, тогда как mplus попробует второй. [] также ведет себя следующим образом:

[] >> [1..4] -> []
[] `mplus` [1..4] -> [1,2,3,4]

Это просто обсуждение аспектов MonadPlus и особенно аспект mplus в отличие от (>>).

Ответ 2

В этом ответе я хочу обсудить тему, почему fail является членом Monad. Я не хочу добавлять это в свой другой ответ, поскольку он охватывает другую тему.


Хотя математическое определение монады не содержит fail, создатели Haskell 98 помещают его в класс Monad. Почему?

Чтобы облегчить использование монадов и облегчить абстрактное использование монадов, они представили ноту do - очень полезный кусок сахара. Например, этот код:

do putStr "What your full name? "
   [name,surname] <- getLine >>= return . words
   putStr "How old are you? "
   age <- getLine >>= return . read
   if age >= 18
      then putStrLn $ "Hello Mr / Ms " ++ surname
      else putStrLn $ "Hello " ++ name

переводит на:

putStr "What your full name? " >>
getLine >>= return . words >>= \[name,surname] ->
putSr "How old are you? " >>
getLine >>= return . read >>= \age ->
if age >= 18
   then putStrLn $ "Hello Mr / Ms " ++ surname
   else putStrLn $ "Hello " ++ name

В чем проблема? Представьте, что у вас есть имя с промежутком между ними, например, Jon M. Doe. В этом случае вся конструкция будет _|_. Конечно, вы можете обойти это, добавив некоторую функцию ad-hoc с помощью let, но это чистый шаблон. В то время, когда был создан Haskell 98, не было такой системы исключений, как сегодня, где вы могли бы просто поймать несоответствующее соответствие шаблонов. Кроме того, неполные шаблоны считаются плохим стилем кодирования.

Какое решение? Создатели Haskell 98 добавили специальную функцию fail, вызываемую в случае несоответствия. Desugaring выглядит примерно так:

putStr "What your full name? " >> let
  helper1 [name,surname] =
    putSr "How old are you? " >> let
      helper2 age =
        if age >= 18
           then putStrLn $ "Hello Mr / Ms " ++ surname
           else putStrLn $ "Hello " ++ name
      helper2 _ = fail "..."
    in getLine >>= return . read >>= helper2
  helper1 _ = fail "..."
in getLine >>= return . words >>= helper1

(Я не уверен, действительно ли есть helper2, но я так думаю)

Если вы посмотрите дважды на это, вы узнаете, насколько он умен. Во-первых, никогда не будет неполного соответствия шаблону, а во-вторых, вы можете настроить fail configurizable. Чтобы достичь этого, они просто помещают fail в определение монады. Например, для Maybe fail просто Nothing, а для экземпляра Either String - Left. Таким образом, легко написать монад-независимый монадический код. Например, в течение долгого времени lookup определялся как (Eq a,Monad b) => a -> [(a, b)] -> m b, а lookup возвращал fail, если не было соответствия.

Теперь в сообществе Haskell по-прежнему остается большой вопрос: не стоит ли добавлять что-то совершенно независимое в класс Monad вроде fail? Я не могу ответить на этот вопрос, но ИМХО это решение было правильным, поскольку другие места не так удобны для fail.