Обозначение для монады в функции, возвращающей другой тип

Есть ли способ записать do нотацию для монады в функции, возвращаемый тип которой не принадлежит указанной монаде?

У меня есть основная функция, выполняющая большую часть логики кода, дополненная другой функцией, которая выполняет некоторые вычисления для нее посередине. Дополнительная функция может дать сбой, поэтому она возвращает значение Maybe. Я хочу использовать обозначение do для возвращаемых значений в основной функции. Приведем общий пример:

-- does some computation to two Ints which might fail
compute :: Int -> Int -> Maybe Int

-- actual logic 
main :: Int -> Int -> Int
main x y = do
  first <- compute x y
  second <- compute (x+2) (y+2)
  third <- compute (x+4) (y+4)
  -- does some Int calculation to first, second and third

Я намерен сделать так, чтобы first, second и third имели фактические значения Int, вынутые из контекста Maybe, но выполнение описанного выше способа заставляет Haskell жаловаться на невозможность сопоставления типов Maybe Int с Int.

Есть ли способ сделать это? Или я иду не в том направлении?

Простите меня, если некоторая терминология используется неправильно, я новичок в Haskell и все еще пытаюсь осмыслить все вокруг.

ИЗМЕНИТЬ

main должен возвращать Int без упаковки в Maybe, поскольку есть другая часть кода, использующая результат mainas Int. Результаты одного compute могут не сработать, но они должны вместе пройти (т.е. пройти хотя бы один) в main, и я ищу способ использовать do нотацию, чтобы вывести их из Maybe, сделать несколько простых Int вычисления для них (например, возможно, обработка любого Nothing, возвращенного как 0), и возвращение окончательного значения как просто Int.


person lookarpthestars    schedule 13.10.2018    source источник
comment
Дело в том, что эти вычисления также должны привести к Maybe, а если нет, вы можете использовать return.   -  person Willem Van Onsem    schedule 13.10.2018
comment
Не уверены, о чем вы говорите: compute или main? В любом случае compute уже возвращает значение Maybe, а main должен определенно возвращать Int, следовательно, Maybe не будет. Другими словами, одно из вычислений в main может завершиться неудачно, но по крайней мере одно должно пройти, что гарантирует, что main имеет возвращаемое значение Int.   -  person lookarpthestars    schedule 13.10.2018
comment
но идея Monad Maybe состоит в том, чтобы рассматривать Maybe как потенциально неудачные вычисления. Итак, здесь вы составляете цепочку потенциально неудачных вычислений и возвращаете Just result в случае успеха и Nothing в противном случае. Если вы уверены, что compute всегда будет возвращать Just _, тогда почему у него есть подпись Int -> Int -> Maybe Int?   -  person Willem Van Onsem    schedule 13.10.2018


Ответы (3)


Ну подпись принципиально неправильная. Результатом должно быть Maybe Int:

main :: Int -> Int -> Maybe Int
main x y = do
  first <- compute x y
  second <- compute (x+2) (y+2)
  third <- compute (x+4) (y+4)
  return (first + second + third)

Например, здесь мы return (first + second + third), а return заключит их в Just конструктор данных.

Это потому, что ваш блок do неявно использует >>= из Monad Maybe, который определяется как:

instance Monad Maybe where
    Nothing >>=_ = Nothing
    (Just x) >>= f = f x
    return = Just

Это означает, что он действительно будет «распаковывать» значения из конструктора данных Just, но если из него выйдет Nothing, то это означает, что результатом всего блока do будет Nothing.

Это более или менее удобство, которое предлагает Monad Maybe: вы можете выполнять вычисления как цепочку успешных действий, и в случае сбоя одного из них результатом будет Nothing, в противном случае - Just result.

Таким образом, вы не можете в конце вернуть Int вместо Maybe Int, поскольку определенно возможно - с точки зрения типов - что одно или несколько вычислений могут вернуть Nothing.

Однако вы можете «опубликовать» результат блока do, если вы, например, добавите значение «по умолчанию», которое будет использоваться в случае, если одно из вычислений будет Nothing, например:

import Data.Maybe(fromMaybe)

main :: Int -> Int -> Int
main x y = fromMaybe 0 $ do
  first <- compute x y
  second <- compute (x+2) (y+2)
  third <- compute (x+4) (y+4)
  return (first + second + third)

Здесь, если блок do возвращает Nothing, мы заменяем его на 0 (конечно, вы можете добавить другое значение в fromMaybe :: a -> Maybe a -> a в качестве значения в случае" сбоя "вычисления).

Если вы хотите вернуть первый элемент в списке Maybe, то есть Just, вы можете использовать asum :: (Foldable t, Alternative f) => t (f a) -> f a, чтобы вы могли написать свой main как:

-- first non-failing computation

import Data.Foldable(asum)
import Data.Maybe(fromMaybe)

main :: Int -> Int -> Int
main x y = fromMaybe 0 $ asum [
    compute x y
    compute (x+2) (y+2)
    compute (x+4) (y+4)
]

Обратите внимание, что asum по-прежнему может содержать только Nothing, поэтому вам все равно потребуется выполнить некоторую пост-обработку.

person Willem Van Onsem    schedule 13.10.2018
comment
Спасибо, ваш комментарий, который называет это цепочкой успешных действий, прояснил мне задачу, и я понимаю, что на самом деле иду в неправильном направлении. Я намерен сделать это не цепочкой действий, а серией вычислений (которые могут потерпеть неудачу, но одно обязательно пройдет) и возвратом первого из прошедшей серии. По сути, то, что делает main, - это абстрагируется от возможных сбоев и выбирает первый из них, который завершится успешно, так что определенное значение Int может использоваться в другом месте при вызове main. Есть ли способ сделать это? - person lookarpthestars; 13.10.2018
comment
@lookarpthestars: вы можете использовать asum для этого, например asum [compute x y, compute (x+2) (y+2)], который вернет первый Just в списке, но это все равно Maybe, поскольку возможно, что все вычисления завершатся ошибкой. - person Willem Van Onsem; 13.10.2018
comment
См .: hackage.haskell.org/ пакет / base-4.12.0.0 / docs / - person Willem Van Onsem; 13.10.2018
comment
Милый, этот комментарий вместе с частью, которую вы добавили после редактирования, дает мне возможность поработать. Оцените краткий ответ. - person lookarpthestars; 13.10.2018
comment
@lookarpthestars к вашему первому комментарию, no, main not выбирает первый, который удастся. только если все три вычисления завершились успешно, он возвращает Just (с суммой трех результатов). в противном случае он ничего не возвращает. поэтому он пытается запустить их все (один за другим, но все), а затем объединить все результаты. - person Will Ness; 14.10.2018

Ответ Виллема в основном идеален, но, чтобы действительно понять суть, давайте подумаем о том, что бы произошло, если бы вы могли написать что-то, что позволяет вам возвращать int.

Итак, у вас есть функция main с типом Int -> Int -> Int, предположим, что реализация вашей функции compute выглядит следующим образом:

compute :: Int -> Int -> Maybe Int
compute a 0 = Nothing
compute a b = Just (a `div` b)

Теперь это в основном безопасная версия функции целочисленного деления div :: Int -> Int -> Int, которая возвращает Nothing, если делитель равен 0.

Если бы вы могли написать основную функцию, которая возвращает Int, вы могли бы написать следующее:

unsafe :: Int
unsafe = main 10 (-2)

Это приведет к сбою second <- compute ... и возврату Nothing, но теперь вы должны интерпретировать Nothing как число, что нехорошо. Это противоречит цели использования Maybe монады, которая безопасно фиксирует ошибки. Вы, конечно, можете указать значение по умолчанию для Nothing, как описал Виллем, но это не всегда подходит.

В более общем смысле, когда вы находитесь внутри do блока, вы должны просто думать о «коробке», которая является монадой, и не пытаться сбежать. В некоторых случаях, таких как Maybe, вы можете выполнять unMaybe что-то вроде fromMaybe или maybe функций, но не в целом.

person madgen    schedule 13.10.2018

У меня есть две интерпретации вашего вопроса, поэтому отвечу на обе:

Суммируйте Maybe Int значения, которые равны Just n, чтобы получить Int

Чтобы суммировать Maybe Ints при отбрасывании значений Nothing, вы можете использовать sum с Data.Maybe.catMaybes :: [Maybe a] -> [a], чтобы выбросить Nothing значений из списка:

sum . catMaybes $ [compute x y, compute (x+2) (y+2), compute (x+4) (y+4)]

Получите первое Maybe Int значение, которое Just n как Int

Чтобы получить первое значение, отличное от Nothing, вы можете использовать catMaybes в сочетании с _ 15_, чтобы получить Just первое значение, если оно есть, или Nothing, если его нет, и _ 18_ для преобразования Nothing в значение по умолчанию:

fromMaybe 0 . listToMaybe . catMaybes $ [compute x y, compute (x+2) (y+2), compute (x+4) (y+4)]

Если вам гарантирован хотя бы один успех, используйте вместо этого head:

head . catMaybes $ [compute x y, compute (x+2) (y+2), compute (x+4) (y+4)]
person Chai T. Rex    schedule 13.10.2018