Я думаю, что это хороший вопрос. Вот более простое воспроизведение:
let test n =
[for i in 1 .. n -> Seq.empty]
|> List.fold (Seq.map2 max) Seq.empty
|> Seq.iter ignore
test
создает последовательность пустых последовательностей, вычисляет максимум по строкам и затем повторяет полученную (пустую) последовательность. Вы обнаружите, что при высоком значении n
это вызовет переполнение стека, даже если нет никаких значений для перебора вообще!
Это немного сложно объяснить, почему, но вот удар в этом. Проблема в том, что при сворачивании последовательностей Seq.map2
возвращает новую последовательность, которая откладывает ее работу до ее перечисления. Таким образом, когда вы пытаетесь перебрать полученную последовательность, вы в конечном итоге перезвоните в цепочку вычислений n
слоев глубиной.
Как объясняет Даниил, вы можете избежать этого, с нетерпением оценив результирующую последовательность (например, преобразовав ее в список).
EDIT
Вот попытка объяснить, что происходит не так. Когда вы звоните Seq.map2 max s1 s2
, ни s1
, ни s2
фактически не перечисляются; Вы получите новую последовательность, которая при перечислении перечислит их обоих и сравнит полученные значения. Таким образом, если мы сделаем что-то вроде следующего:
let s0 = Seq.empty
let s1 = Seq.map2 max Seq.emtpy s0
let s2 = Seq.map2 max Seq.emtpy s1
let s3 = Seq.map2 max Seq.emtpy s2
let s4 = Seq.map2 max Seq.emtpy s3
let s5 = Seq.map2 max Seq.emtpy s4
...
Тогда вызов Seq.map2
всегда возвращается немедленно и использует постоянное пространство стека. Однако , перечисление s5 требует перечисления s4, что требует перечисления s3 и т. Д. Это означает, что перечисление s99999 создаст огромный стек вызовов, который выглядит примерно так:
...
(s99996's enumerator).MoveNext()
(s99997's enumerator).MoveNext()
(s99998's enumerator).MoveNext()
(s99999's enumerator).MoveNext()
и мы получим переполнение стека.