Ленивая оценка и беспорядок побочных эффектов IO - PullRequest
6 голосов
/ 22 ноября 2011

Этот код (взят из Learn You A Haskell ):

main = do   putStr "Hey, "  
            putStr "I'm "  
            putStrLn "Andy!"  

очевидно, десугары до

main =        putStr "Hey, " >>=  
       (\_ -> putStr "I'm "  >>= 
       (\_ -> putStrLn "Andy!"))

Который, как я понимаю, может быть истолкован как "Энди! Для того, чтобы положить StrLn"! Мне сначала нужно положить "Я", а для этого мне сначала нужно положить "Эй";

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

Также, конечно, привязка возвращает действие IO, и когда это действие IO попадает в main, оно выполняется. Но что мешает ему напечатать "Эй, Энди! Я"? Я подозреваю, что это то, что делает привязка.

Кроме того, как действие ввода-вывода типа "IO ()" несет достаточно информации, чтобы система выполнения могла вывести "Эй, я, Энди!"? Чем этот IO () отличается от IO (), чем печатает «Hello World!» или пишет в файл?

Рассмотрим другой, со страницы Википедии для монады:

Версия с сахаром:

do
  putStrLn "What is your name?"
  name <- getLine
  putStrLn ("Nice to meet you, " ++ name ++ "!")

Версия Desugared:

putStrLn "What is your name?" >>= 
   (\_ ->
      getLine >>=
         (\name ->
            putStrLn ("Nice to meet you, " ++ name ++ "!")))

Подобная история здесь.

Я думаю, мне просто нужно увидеть определение bind для IO, и тогда все будет ясно. Еще кое-что, что могло бы помочь, - это если бы кто-то мог помочь мне пройти через то, как программа действительно оценивается, и определить точные моменты, когда возникают побочные эффекты.

Ответы [ 4 ]

10 голосов
/ 22 ноября 2011

Прочитайте статью Саймона Пейтона Джонса " Tackling the unkward squad ".

По связанным вопросам смотрите

Возьмите любое такое объяснение, в том числе и мое, с крошкой соли - никакие помахивания рукой не могут заменить строгую рецензируемую бумагу, и объяснения обязательно являются чрезмерно упрощенными.1035 * можно рассматривать как конструктор списка:

data IO = [Primitive] 

и IOПодсистема восстанавливает значение main и использует этот список.То есть `` main is just a list. So you may want to take a look at the definition of Haskell entry point above main , bind` довольно неинтересна.

Вы также можете прочитать статьи по истории haskell и взглянуть на более ранние версии подсистемы ввода-вывода, чтобы понять, что происходит.

Также посмотрите на Язык С является чисто функциональным сатирическим постом Конала Эллиота.

Определение функциональной чистоты нетривиально, и я помню статью, в которой уточняется это определение, ноЯ не помню название.

7 голосов
/ 22 ноября 2011

Глядя на IO в реальной реализации на Haskell, возможно, запутает больше, чем просветит. Но представьте, что IO определено так (предполагается, что вы знаете GADT):

data IO a where
    Return a :: IO a
    Bind :: IO a -> (a -> IO b) -> IO b
    PutStr :: String -> IO ()
    GetLine :: IO String

instance Monad IO where
    return = Return
    (>>=) = Bind

putStr :: String -> IO ()
putStr = PutStr

getLine :: IO String
getLine = GetLine

Поэтому, когда вы оцениваете программу (типа IO ()), все, что она делает, - это строит структуру данных типа IO (), которая описывает, как будет происходить взаимодействие с миром после его выполнения. Затем вы можете представить себе движок выполнения, написанный на, например, C, и там, где происходят все эффекты.

So

main = do   putStr "Hey, "  
            putStr "I'm "  
            putStrLn "Andy!"  

совпадает с

main = Bind (PutStr "Hey, ") (\ _ -> Bind (PutStr "I'm ") (\ _ -> PutStr "Andy!"))

И их последовательность основана на том, как работает механизм исполнения.

Тем не менее, я не знаю ни одной реализации на Haskell, которая на самом деле делает это таким образом. Реальные реализации имеют тенденцию реализовывать IO как монаду состояния с токеном, представляющим реальный мир, который передается (это то, что гарантирует последовательность), а примитивы, такие как putStr, являются просто вызовами функций на Си.

3 голосов
/ 22 ноября 2011

Я думаю, мне просто нужно увидеть определение привязки для IO, и тогда все будет ясно.

Да, вы должны это сделать.На самом деле это довольно просто, и если я правильно помню, это выглядит как

newtype IO = IO (RealWorld -> (a, RealWorld))

(IO f) >>= g = ioBind f g
    where
       ioBind :: (RealWorld -> (a, RealWorld)) -> (a -> IO b) -> RealWorld -> (b, RealWorld)
       ioBind f g rw = case f rw of
            (a, rw@RealWorld) -> case g a of
                IO b -> b rw

«Хитрость» заключается в том, что каждое значение IO на самом деле является функцией, но для его оценки вам понадобится токен типа RealWorld.Существует только один экземпляр, который может предоставить такое значение - система времени выполнения, в которой работает main (и, конечно, функция, имя которой не должно называться).

1 голос
/ 22 ноября 2011

Я думаю, что это более понятно, если вы снова думаете о действиях как о функциях. Ваш пример привязки (do { foo <- getLine ; putStrLn foo ; }) интуитивно похож на следующую функцию:

apply arg func = func (arg)

За исключением того, что функция является транзакцией. Таким образом, наш вызов func(arg) оценивается, если он есть, только если (arg) завершается успешно. В противном случае мы fail в наших действиях.

Это отличается от обычных функций, потому что тогда Хаскеллу действительно все равно, будет ли (arg) вычисляться полностью или вообще, до тех пор, пока для продолжения программы не понадобится немного func(arg).

...