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

Использование return vs. не использование return в списке monad

Я начал свой Великий Крестовый поход Хаскелла (GHC:)), и я немного смущен монадами и функциями IO. Может ли кто-нибудь объяснить просто, в чем разница между этими двумя функциями?

f1 = do x <- [1,2]
        [x, x+1] -- this is monad, right?

f2 = do x <- [1,2]
        return [x, x+1]

Результаты:

*Main> f1
[1,2,2,3]

*Main> f2
[[1,2],[2,3]]
4b9b3361

Ответ 1

Другие ответы здесь правильные, но мне интересно, не совсем ли они вам нужны... Я постараюсь сделать это как можно проще, всего две точки:


Точка 1. return не является особой вещью на языке Haskell. Это не ключевое слово, а это не синтаксический сахар для чего-то другого. Это просто функция, которая является частью класса Monad. Его подпись проста:

return :: a -> m a

где m - какая-то монада, о которой мы говорим в то время. Он берет "чистое" значение и застревает его в вашу монаду. (Кстати, есть еще одна функция под названием pure, которая в основном является синонимом return... Мне это нравится, потому что имя более очевидно!) В любом случае, если m является монадой списка, тогда return имеет это Тип:

return :: a -> [a]

Если это поможет, вы можете подумать о синониме типа type List a = [a], который может сделать несколько более очевидным, что List - это то, что мы заменяем на m. В любом случае, если вы собираетесь реализовать return самостоятельно, единственный разумный способ реализовать его - взять какое-то значение (любого типа a) и вставить его в список сам по себе:

return a = [a]

Итак, я могу сказать return 1 в монаде списка, и я получу [1]. Я также могу сказать return [1, 2, 3], и я получу [[1, 2, 3]].


Точка 2. IO является монадой, но не все монады IO.. Многие учебники Haskell, похоже, объединяют две темы в основном по историческим причинам (кстати, те же самые запутанные исторические причины что привело к тому, что return был так плохо назван). Похоже, у вас может быть некоторая (понятная) путаница вокруг этого.

В вашем коде вы находитесь в списке монады, потому что вы написали do x <- [1, 2]. Если вместо этого вы написали do x <- getLine, например, вы были бы в монаде IO (потому что getLine возвращает IO String)). Во всяком случае, вы находитесь в списке монады, так что вы получаете определение списка return, описанное выше. Вы также получаете определение списка >>=, которое является просто (перевернутая версия) concatMap, определяемая как:

concatMap :: (a -> [b]) -> [a] -> [b]
concatMap f xs = concat (map f xs)

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

Ответ 2

Чтобы понять, почему вы получаете конкретные ответы, эти объяснения очень полезны. Позвольте мне дополнить их небольшим общим советом по разработке восприятия кода на Haskell.

Система типов Хаскелла не делает различий между двумя разделяемыми "моральными" целями:

  • [x] тип значений, представляющих собой списки с элементами, взятыми из x
  • [x] тип вычислений элементов x которые допускают приоритетный выбор

Тот факт, что эти два понятия имеют одинаковое представление, не означает, что они играют одинаковые роли. В f1 [x, x+1] играет роль вычисления, поэтому генерируемые им возможности объединяются в выбор, генерируемый всем вычислением: то, что делает >>= монады списка. В f2, однако, [x, x+1] играет роль значения, так что все вычисления генерируют приоритетный выбор между двумя значениями (которые оказываются списочными значениями).

Хаскелл не использует типы, чтобы сделать это различие [и вы уже могли догадаться, что я думаю, что это следует, но это другая история]. Вместо этого он использует синтаксис. Поэтому вам нужно тренировать свою голову, чтобы воспринимать значение и вычислительные роли при чтении кода. Нотация do - это специальный синтаксис для построения вычислений. То, что входит в do, построено из следующего набора шаблонов:

jigsaw pieces for computations

Три синие фигуры делают do -computations. Я отметил дыры в вычислениях синим цветом, а дыры в значениях - красным. Это не полный синтаксис, а всего лишь руководство к тому, как воспринимать фрагменты кода в вашем уме.

В самом деле, вы можете написать любое старое выражение в синих местах при условии, что оно имеет соответственно монадический тип, и сгенерированные таким образом вычисления будут объединены с общим вычислением, используя >>= мере необходимости. В вашем примере f1 ваш список находится в синем месте и рассматривается как приоритетный выбор.

Точно так же вы можете писать выражения в красных местах, которые вполне могут иметь монадические типы (например, списки в этом случае), но они все равно будут рассматриваться как значения. То, что происходит в f2: внешние скобки результата как бы синие, а внутренние - красные.

Тренируйте свой мозг, чтобы разделить значение и вычисление, когда вы читаете код, чтобы вы инстинктивно знали, какие части текста выполняют какую работу. Как только вы перепрограммируете свою голову, различие между f1 и f2 будет казаться совершенно нормальным!

Ответ 3

Легко видеть, когда переписывать код со связыванием и возвратом:

[1,2] >>= (\x->        [x,x+1]) === concatMap (\x-> [  x,x+1  ]) [1,2]

[1,2] >>= (\x-> return [x,x+1]) === concatMap (\x-> [ [x,x+1] ]) [1,2]

Ваш первый код равносилен вызову join по результатам второго, удаляя один монадический "слой", введенный return :: a -> m a, conflating "список" используемой монады, с "списком" вашего значения. Если вы возвращали пару, скажем, было бы бессмысленно опускать return:

                                     -- WRONG: type mismatch
[1,2] >>= (\x->        (x,x+1)) === concatMap (\x-> (  x,x+1  )) [1,2]
                                     -- OK:
[1,2] >>= (\x-> return (x,x+1)) === concatMap (\x-> [ (x,x+1) ]) [1,2]

Или мы можем использовать перезапись join/fmap:

ma >>= famb === join (fmap famb ma)   -- famb :: a -> m b, m ~ []

join (fmap (\x->        [x,x+1]) [1,2]) = concat [ [  x,x+1  ] | x<-[1,2]]
join (fmap (\x->        (x,x+1)) [1,2]) = concat [ (  x,x+1  ) | x<-[1,2]]  -- WRONG
join (fmap (\x-> return [x,x+1]) [1,2]) = concat [ [ [x,x+1] ] | x<-[1,2]]

                                                         =  [y | x<-[1,2], y<-[ x,x+1 ]]
                                            {- WRONG -}  =  [y | x<-[1,2], y<-( x,x+1 )]
                                                         =  [y | x<-[1,2], y<-[[x,x+1]]]

Ответ 4

Тип списка ([]) является монадой, да.

Теперь помните, что делает return. Это легко увидеть по сигнатуре типа: return :: Monad m => a -> m a. Позвольте подставить тип списка в: return :: a -> [a]. Поэтому эта функция принимает некоторое значение и возвращает только список этого значения. Это эквивалентно \ x -> [x].

Итак, в первом примере кода у вас есть один список в самом конце: [x, x+1]. Во втором примере у вас есть вложенный список: один список происходит от [x, x + 1], а другой список - от return. В этом случае строка return [x, x + 1] может быть переписана на [[x, x + 1]].

В итоге результат - список всех возможных результатов. То есть, мы объединяем результат x как 1 и результат x как 2 (благодаря строке x <- [1,2]). Итак, в первом случае мы объединяем два списка; во втором случае мы объединяем два списка списков, поскольку дополнительный return завернул результат в дополнительный список.

Ответ 5

Desugaring синтаксис do эквивалентному

f1 = [1,2] >>= \x -> [x, x+1]
f2 = [1,2] >>= \x -> return [x, x+1]

Теперь >>= происходит из класса Monad,

class Monad m where
    (>>=) :: m a -> (a -> m b) -> m b
    return :: a -> m a

и LHS >>= в f1 и f2 составляет [a] (где a по умолчанию имеет значение Integer), поэтому мы действительно рассматриваем

instance Monad [] where
    (>>=) :: [a] -> (a -> [b]) -> [b]
    ...

Это подчиняется тем же законам монады, но является другой монадой,

instance Monad IO where ...

и >>= для других монад, так что не просто слепо применять то, что вы знаете об этом, хорошо?:)

instance Monad [] определяется таким образом в GHC

instance Monad [] where
    m >>= k = foldr ((++) . k) [] m
    return x = [x]
    ...

но, вероятно, легче понять [] >>= как

instance Monad [] where
    m >>= k = concatMap k m

Если вы возьмете это и примените его к оригиналу, вы получите

f1 = concatMap (\x -> [x, x+1]) [1,2]
f2 = concatMap (\x -> [[x, x+1]]) [1,2]

и ясно, почему значения f1 и f2 являются тем, чем они являются.

Ответ 6

Я понимаю, что делать

return [1,2], когда в Монаде списка это то же самое, что и при выполнении

func :: Maybe (Maybe Int)
func = return $ Just 1

поэтому вы заканчиваете с завернутыми списками, так как [] - это просто синтаксический сахар?

Когда он действительно хотел сделать

func :: Maybe Int
func = return 5

или

func = Just 5

Его немного легче понять, что происходит с монашкой Maybe, которую я думаю.

Итак, когда вы делаете

return [1,2]

Вы делаете то же самое, что и

[ [1,2] ]