Почему последовательность [getLine, getLine, getLine] не оценивается лениво? - PullRequest
8 голосов
/ 20 апреля 2019
main = do
  input <- sequence [getLine, getLine, getLine]
  mapM_ print input

Давайте посмотрим на эту программу в действии:

m@m-X555LJ:~$ runhaskell wtf.hs
asdf
jkl
powe
"asdf"
"jkl"
"powe"

Удивительно для меня, но здесь, похоже, нет лени.Вместо этого все 3 getLine с оцениваются с нетерпением, считанные значения сохраняются в памяти, а затем, не раньше, все печатаются.

Сравните с этим:

main = do
  input <- fmap lines getContents
  mapM_ print input

Давайте посмотримэто в действии:

m@m-X555LJ:~$ runhaskell wtf.hs
asdf
"asdf"
lkj
"lkj"
power
"power"

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

Из LearnYouAHaskell:

При использовании с действиями ввода / вывода sequenceA являетсятоже самое что и sequence!Он принимает список действий ввода-вывода и возвращает действие ввода-вывода, которое будет выполнять каждое из этих действий и в результате будет иметь список результатов этих действий ввода-вывода.Это связано с тем, что для преобразования значения [IO a] в значение IO [a] для выполнения действия ввода-вывода, которое при выполнении выдает список результатов, все эти действия ввода-вывода должны быть упорядочены таким образом, чтобы они затем выполнялись однимпосле другого, когда оценка является принудительной.Вы не можете получить результат действия ввода-вывода, не выполнив его.

Я в замешательстве.Мне не нужно выполнять ВСЕ действия ввода-вывода, чтобы получить результаты только одного.

Несколькими параграфами ранее книга показывает определение sequence:

sequenceA :: (Applicative f) => [f a] -> f [a]  
sequenceA [] = pure []  
sequenceA (x:xs) = (:) <$> x <*> sequenceA xs

Хорошая рекурсия;ничто здесь не намекает мне на то, что эта рекурсия не должна быть ленивой, как и в любой другой рекурсии, чтобы получить заголовок возвращаемого списка, Haskell не должен проходить через ВСЕ шаги рекурсии!

Сравните:

rec :: Int -> [Int]
rec n = n:(rec (n+1))

main = print (head (rec 5))

В действии:

m@m-X555LJ:~$ runhaskell wtf.hs
5
m@m-X555LJ:~$

Очевидно, что рекурсия здесь выполняется лениво, а не жадно.

Тогда почему рекурсия в примере sequence [getLine, getLine, getLine] выполняется


Что касается , почему важно, чтобы действия ввода-вывода выполнялись по порядку независимо от результатов: представьте себе действие createFile :: IO () и writeToFile :: IO ().Когда я делаю sequence [createFile, writeToFile], я надеюсь, что они и выполнены и в порядке, хотя мне все равно, какие у них реальные результаты (оба очень скучные значения ())вообще!

Я не уверен, как это относится к этому вопросу.

Может быть, я скажу свой вопрос таким образом ...

В моемимейте в виду:

do
    input <- sequence [getLine, getLine, getLine]
    mapM_ print input

должно уменьшать до чего-то вроде этого:

do
    input <- do
       input <- concat ( map (fmap (:[])) [getLine, getLine, getLine] )
       return input
    mapM_ print input

Что, в свою очередь, должно уменьшать до чего-то вроде этого (псевдокод, извините):

do
    [ perform print on the result of getLine,
      perform print on the result of getLine,
      perform print on the result of getLine
    ] and discard the results of those prints since print was applied with mapM_ which discards the results unlike mapM

Ответы [ 2 ]

6 голосов
/ 20 апреля 2019

getContents ленив, getLine нет.Ленивый ввод-вывод - это не особенность Haskell, а особенность некоторых конкретных действий ввода-вывода.

Я в замешательстве.Мне не нужно выполнять ВСЕ операции ввода-вывода, чтобы получить результаты только одного.

Да, вы делаете!Это одна из наиболее важных особенностей IO, которая заключается в том, что если вы напишите a >> b или, что эквивалентно,

do a
   b

, то вы можете быть уверены, что a определенно «запустится» до b(см. сноску).getContents на самом деле то же самое, он «запускается» до того, что последует за ним ... но результат , который он возвращает, является хитрым результатом, который незаметно делает больше IO, когда вы пытаетесьоцените это. То, что на самом деле удивительный бит, и это может привести к некоторым интересным результатам на практике (например, файл, который вы читаете, удаляется или изменяется во время обработкирезультаты getContents), поэтому в практических программах вы, вероятно, не должны использовать его, он в основном существует для удобства в программах, где вас не волнуют такие вещи (например, Code Golf, одноразовые сценарии или обучение).


Что касается , почему важно, чтобы действия ввода-вывода выполнялись по порядку независимо от результатов: представьте себе действие createFile :: IO () и writeToFile :: IO ().Когда я делаю sequence [createFile, writeToFile], я надеюсь, что они и выполнены и в порядке, хотя меня не волнуют их фактические результаты (которые очень скучны ())вообще!


Обращаясь к редактированию:

должно сместить что-то вроде этого:

do
    input <- do
       input <- concat ( map (fmap (:[])) [getLine, getLine, getLine] )
       return input
    mapM_ print input

Нет, это фактически превращается впримерно так:

do 
  input <- do
    x <- getLine
    y <- getLine
    z <- getLine
    return [x,y,z]
  mapM_ print input

(фактическое определение sequence более или менее таково:

sequence [] = return []
sequence (a:as) = do
  x <- a
  fmap (x:) $ sequence as
4 голосов
/ 20 апреля 2019

Технически, в

sequenceA (x:xs) = (:) <$> x <*> sequenceA xs

мы находим <*>, который сначала запускает действие слева, затем действие справа и, наконец, применяет их результат вместе. Это то, что делает первый эффект в списке первым, и так далее.

Действительно, на монадах f <*> x эквивалентно

do theF <- f
   theX <- x
   return (theF theX)

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

do let aX = print "x" >> return 4
       aY = print "y" >> return 10
   x <- aX
   y <- aY
   print (x+y)

Haskell гарантирует, что на выходе будет x y 14, в этом порядке. Если бы у нас был полностью ленивый ввод-вывод, мы могли бы также получить y x 14, в зависимости от того, какой аргумент сначала вызывается +. В таком случае нам нужно было бы точно знать порядок, в котором ленивые громкие звуки требуются каждой операцией, о чем программист определенно не хочет заботиться. При такой детальной семантике x + y больше не эквивалентен y + x, что во многих случаях нарушает эквалайзер.

Теперь, если мы хотим заставить IO быть ленивым, мы можем использовать одну из запрещенных функций, например,

do let aX = unsafeInterleaveIO (print "x" >> return 4)
       aY = unsafeInterleaveIO (print "y" >> return 10)
   x <- aX
   y <- aY
   print (x+y)

Приведенный выше код выполняет ленивые действия ввода-вывода aX и aY, и теперь порядок вывода зависит от компилятора и реализации библиотеки +. Это вообще опасно, поэтому unsafe ленивый IO.

Теперь об исключениях. Некоторые действия ввода-вывода, которые только читают из среды, например getContents, были реализованы с помощью отложенного ввода-вывода (unsafeInterleaveIO). Разработчики посчитали, что для таких операций чтения ленивый ввод-вывод может быть приемлемым, и что точное время чтения не так важно во многих случаях.

В настоящее время, это противоречивый . Хотя это может быть удобно, ленивый ввод-вывод может быть слишком непредсказуемым во многих случаях. Например, мы не можем знать, где файл будет закрыт, и это может иметь значение, если мы читаем из сокета. Мы также должны быть очень осторожны, чтобы не начинать чтение слишком рано: это часто приводит к тупику при чтении из канала. Сегодня обычно предпочитают избегать ленивых операций ввода-вывода и прибегать к некоторым библиотекам, таким как pipes или conduit, для операций, подобных потоковым, в которых нет двусмысленности.

...