Вы найдете это "шокирующим", потому что вы этого не ожидаете. Как только вы привыкнете к этому ... ОК, на самом деле он все еще путешествует. Но через некоторое время вы в конце концов обманетесь вокруг.
Как работает Haskell: когда вы вызываете функцию, ничего не происходит! Звонок отмечен где-то, и все. Это занимает совсем немного времени. Ваш «результат» на самом деле просто «я должен вам», говоря компьютеру, какой код запускать, чтобы получить результат. Не весь результат, заметьте; просто первый шаг. Для чего-то вроде целого, есть is всего один шаг. Но для списка каждый элемент представляет собой отдельный шаг.
Позвольте мне показать вам простой пример:
print (take 10 ([1..] ++ [0]))
Я говорил с C++ программист, который был «в шоке», что это работает. Несомненно, часть «++[0]
» должна «найти конец списка», прежде чем она сможет добавить к ней нуль? Как этот код может завершиться за конечное время?
Это выглядит как это создает [1..]
(в бесконечном списке), а затем ++[0]
сканирование до конца этого списка и вставляет нуль, а затем take 10
обрезает покинуть только первые 10 элементов, а затем она печатает. То, что будет, конечно, занимает бесконечное время.
Итак, вот что такое на самом деле. Основная функция - take
, так что мы начинаем. (Не ожидали, что ль?) Определение take
что-то вроде этого:
take 0 ( _) = []
take n ( []) = []
take n (x:xs) = x : (take (n-1) xs)
Так ясно 10!= 0, поэтому первая строка не применяется. Таким образом, применяется либо вторая, либо третья строка. Итак, теперь take
смотрит на [1..] ++ [0]
, чтобы узнать, есть ли пустой список или непустой список.
Внешняя функция здесь (++)
. Это определение похоже на
( []) ++ ys = ys
(x:xs) ++ ys = x : (xs ++ ys)
Итак, нам нужно выяснить, какое уравнение применимо. Либо левый аргумент представляет собой пустой список (применяется одна строка), либо нет (применяется строка 2). Ну, поскольку [1..]
- бесконечный список, строка вторая всегда. Таким образом, «результатом» [1..] ++ [0]
является 1 : ([2..] ++ [0])
. Как вы можете видеть, это не полностью выполнено; но он выполнен достаточно далеко, чтобы сказать, что это непустой список. Это все, о чем заботится take
.
take 10 ([1..] ++ [0])
take 10 (1 : ([2..] ++ [0]))
1 : take 9 ([2..] ++ [0])
1 : take 9 (2 : ([3..] ++ [0]))
1 : 2 : take 8 ([3..] ++ [0])
1 : 2 : take 8 (3 : ([4..] ++ [0]))
1 : 2 : 3 : take 7 ([4..] ++ [0])
...
1 : 2 : 3 : 4 : 5 : 6 : 7 : 8 : 9 : 10 : take 0 ([11..] ++ [0])
1 : 2 : 3 : 4 : 5 : 6 : 7 : 8 : 9 : 10 : []
Вы видите, как это расслабляется?
Теперь, возвращаясь к вашему конкретному вопросу: оператор (==)
занимает пару списков и итерацию по обоим из них, сравнивая их поэлементно, чтобы гарантировать, что они равны. Как только разница обнаруживается, он сразу же прекращает и возвращает ложь:
( []) == ( []) = True
(x:xs) == (y:ys) = (x == y) && (xs == ys)
( _) == ( _) = False
Если мы теперь попробуйте, скажем, prime 6
:
prime 6
factors 6 == [1,6]
??? == [1,6]
1 : ??? == [1,6]
??? == [6]
2 : ??? == [6]
False
Подумайте, как реализуются «факторы n == [1, n]», проверяется элемент по элементу, факторы n также будут генерироваться элементом по элементу, если во втором элементе больше не будут равны тесты (и больше элементов из факторов) не требуются. – karakfa
Порядок исполнения не представляет большой проблемы с чистыми функциями; такие понятия, как монады, используются для обеспечения того, чтобы вещи, которые должны произойти в порядке, происходили в правильном порядке. – chepner