Ответ 1
Это действительно не так, как я бы написал это, но его достаточно ясно, что вы делаете. (Кстати, если вы хотите иметь возможность эффективно вставлять произвольный вывод из любой функции в цепочке, не используя монады, вы можете попробовать Data.ByteString.Builder
.)
Ваша первая реализация очень похожа на левую складку, а ваша вторая очень похожа на правую складку или карту. (Вы можете попробовать написать их как таковые!) Второй имеет несколько преимуществ для ввода-вывода. Одним из наиболее важных для обработки ввода и вывода является то, что он может быть интерактивным.
Вы заметите, что первый строит весь список извне: чтобы определить, что такое первый элемент списка, программе необходимо вычислить всю структуру, чтобы добраться до самого внутреннего thunk, который равен return l
, Сначала программа генерирует всю структуру данных, а затем начинает ее обрабатывать. Это полезно, когда вы сокращаете список, потому что функции хвоста-рекурсии и строгие левые складки эффективны.
Во втором, самый внешний thunk содержит голову и хвост списка, поэтому вы можете схватить хвост, а затем вызвать thunk для создания второго списка. Это может работать с бесконечными списками, и оно может создавать и возвращать частичные результаты.
Вот пример: программа, которая читает по одному целю на строку и печатает суммы до сих пор.
main :: IO ()
main = interact( display . compute 0 . parse . lines )
where parse :: [String] -> [Int]
parse [] = []
parse (x:xs) = (read x):(parse xs)
compute :: Int -> [Int] -> [Int]
compute _ [] = []
compute accum (x:xs) = let accum' = accum + x
in accum':(compute accum' xs)
display = unlines . map show
Если вы запустите это в интерактивном режиме, вы получите что-то вроде:
$ 1
1
$ 2
3
$ 3
6
$ 4
10
Но вы также можете написать compute
tail-recursively, с накопительным параметром:
main :: IO ()
main = interact( display . compute [] . parse . lines )
where parse :: [String] -> [Int]
parse = map read
compute :: [Int] -> [Int] -> [Int]
compute xs [] = reverse xs
compute [] (y:ys) = compute [y] ys
compute (x:xs) (y:ys) = compute (x+y:x:xs) ys
display = unlines . map show
Это искусственный пример, но строгие левые складки - общий шаблон. Если, однако, вы пишете compute
или parse
с накопительным параметром, это то, что вы получаете, когда пытаетесь запустить интерактивно, и нажмите EOF (control-D
в Unix, control-Z
в Windows) после номера 4:
$ 1
$ 2
$ 3
$ 4
1
3
6
10
Эта левая версия должна вычислять всю структуру данных, прежде чем она сможет ее прочитать. Это не работает в бесконечном списке (когда вы достигнете базового случая? Как бы вы даже отменили бесконечный список, если бы вы это сделали?) И приложение, которое не может отвечать на ввод пользователя, пока оно не завершится, является нарушителем сделки.
С другой стороны, хвосто-рекурсивная версия может быть строгой в своем накопительном параметре и будет работать более эффективно, особенно когда ее не потребляют немедленно. Он не должен содержать любые трюки или контекст, отличные от его параметров, и он может даже повторно использовать один и тот же стек стека. Строгая функция накопления, такая как Data.List.foldl'
, является отличным выбором, когда вы уменьшаете список до значения, а не стройте список результатов с высокой степенью точности. Такие функции, как sum
, product
или any
, не могут возвращать какое-либо полезное промежуточное значение. Сначала они должны закончить вычисление, а затем вернуть окончательный результат.