Как нотация do в монаде означает в Haskell

Рассмотрим следующий сегмент кода

import Control.Monad.State

type Stack = [Int]

pop :: State Stack Int 
pop = state $ \(x:xs) -> (x, xs)

push :: Int -> State Stack () 
push a = state $ \xs -> ((), a:xs)

stackManip :: State Stack Int
stackManip = do 
    push 3 
    a <- pop 
    pop

Как мы знаем, do notation в монаде совпадает с оператором >>=. Мы можем переписать этот сегмент так:

push 3 >>= (\_ ->
pop >>= (\a ->
pop))

окончательное значение этого выражения не связано с push 3 и первым pop, независимо от ввода, оно просто возвращает pop, поэтому сначала не поместит значение 3 в стек и не вытолкнет его, почему это произошло?


Спасибо за ваш ответ. Я добавил недостающий код (реализация для Stack, push и pop) выше и, кажется, понял, как он работает. Ключом к пониманию этого кода является понимание реализации монады State s:

instance Monad (State s) where 
    return x = State $ \s -> (x, s) 
    (State h) >>= f = State $ \s -> let (a, newState) = h s 
                                        (State g) = f a
                                    in g newState

Вся реализация >>= do заключается в передаче состояния функции h, вычислении ее нового состояния и передаче нового состояния функции g, подразумеваемой в f , получая более новое состояние. Так что на самом деле состояние изменилось неявно.


person Tri    schedule 02.03.2020    source источник
comment
Нет времени для подробного ответа, но это в основном из-за того, как определяется >>= монады State. Это гарантирует, что когда состояние изменяется одним выражением, обновленное значение передается следующему.   -  person Robin Zigmond    schedule 02.03.2020
comment
Как определяются Stack, push и pop?   -  person Mark Seemann    schedule 02.03.2020
comment
что значит не будет? конечно будет, вы так написали в своем коде. Сначала вы нажимаете 3, затем извлекаете его (да, отрицая нажатие, но какое дело компилятору? в любом случае это незаметно (кроме изучения кода, созданного компилятором)), затем делаете еще одно извлечение, значение которого возвращается (предположительно, так определяется pop). поэтому эквивалентный фрагмент push 3 >>= (\() -> pop >>= (\3 -> pop)). (возможно, если так определяется push). в чем именно ваш вопрос? Haskell — это YAPL, вы, программист, отвечаете за него.   -  person Will Ness    schedule 02.03.2020
comment
>>= не просто возвращает вторую вещь!   -  person user253751    schedule 02.03.2020
comment
Весь смысл монады State состоит в том, чтобы скрыть явную передачу состояния (в данном случае манипуляции со стеком).   -  person chepner    schedule 02.03.2020
comment
Конечным значением выражения является преобразователь состояния, который берет начальный стек, помещает в него 3, извлекает первое значение, извлекает второе значение, затем возвращает первое значение и результирующий стек. Это абсолютно связано с push 3.   -  person chepner    schedule 02.03.2020
comment
@chepner Думаю, ты ошибаешься. он возвращает значение из второго всплывающего окна AFAICS. он игнорирует значение из первого всплывающего окна (которое равно 3, которое оно только что вставило непосредственно перед первым всплывающим окном).   -  person Will Ness    schedule 02.03.2020
comment
Да, я думал, что a был связан по какой-то причине, но он никогда не используется.   -  person chepner    schedule 02.03.2020
comment
Какие наблюдения вы делаете, которые подтверждают утверждение, что он не сначала поместит значение 3 в стек и не вытолкнет его? К чему это относится, почему это произошло?   -  person Daniel Wagner    schedule 02.03.2020


Ответы (2)


Из-за законов монад ваш код эквивалентен

stackManip :: State Stack Int
stackManip = do 
    push 3 
    a <- pop    -- a == 3
    r <- pop
    return r

Таким образом, вы нажимаете 3, выталкиваете его, игнорируете выдвинутое 3, выталкиваете другое значение и возвращаете его.

Haskell — это просто еще один язык программирования. Вы программист в контроле. Пропускает ли компилятор несущественные инструкции, зависит от него, и в любом случае это незаметно (за исключением проверки кода, созданного компилятором, или измерения тепла ЦП при выполнении вашего кода, но это может быть немного сложно сделать на сервере). ферма за полярным кругом).

person Will Ness    schedule 02.03.2020

Монады в Haskell иногда называют «программируемыми точками с запятой». В целом я не нахожу эту фразу особенно полезной, но она отражает тот факт, что выражения, написанные с помощью нотации Haskell do, имеют что-то вроде императивных программ. И, в частности, то, как комбинируются «операторы» в блоке do, зависит от конкретной используемой монады. Следовательно, «программируемые точки с запятой» - способ, которым последовательные «операторы» (которые во многих императивных языках разделяются точкой с запятой) объединяются вместе, можно изменить («запрограммировать») с помощью другой монады.

И поскольку нотация do на самом деле является просто синтаксическим сахаром для построения выражения из других с помощью оператора >>=, именно реализация >>= для каждой монады определяет ее «особое поведение».

Например, экземпляр Monad для Maybe позволяет, в качестве грубого описания, работать со значениями Maybe, как если бы они на самом деле были значениями базового типа, при этом гарантируя, что если незначение (то есть Nothing) возникнет в любой точке , вычисления завершатся короткими замыканиями, и Nothing будет общим результатом.

Для монады списка каждая строка фактически "выполняется" несколько раз (или ни разу) - один раз для каждого элемента в списке.

А для значений монады State s это, по сути, "функции управления состоянием" типа s -> (a, s) - они принимают начальное состояние и из него вычисляют новое состояние, а также выходное значение некоторого типа a. Реализация >>= — «точка с запятой» — здесь* просто гарантирует, что когда за одной функцией f :: s -> (a, s) следует другая g :: s -> (b, s), результирующая функция применяет f к начальному состоянию, а затем применяет g к состоянию, вычисленному из f. По сути, это просто композиция функций, слегка измененная, чтобы мы также могли получить доступ к «выходному значению», тип которого не обязательно связан с типом состояния. И это позволяет перечислить различные функции манипулирования состоянием одну за другой в блоке do и знать, что состояние на каждом этапе точно такое же, как вычислено предыдущими строками вместе. Это, в свою очередь, позволяет использовать очень естественный стиль программирования, когда вы даете последовательные «команды» для управления состоянием, но при этом фактически не выполняете деструктивные обновления или иным образом не выходите из мира чистых функций и неизменяемых данных.

*строго говоря, это не >>=, а >>, операция, производная от >>=, но игнорирующая выходное значение. Вы могли заметить, что в примере, который я дал, значение a, выводимое f, полностью игнорируется, но >>= позволяет проверить это значение и определить, какое вычисление делать дальше. В нотации do это означает запись a <- f, а затем использование a позже. На самом деле это ключевая вещь, которая отличает монады от их менее мощных, но все же жизненно важных кузенов (особенно аппликативных функторов).

person Robin Zigmond    schedule 02.03.2020