Я делаю свои первые шаги с F#, портируя интерпретатор, который я написал на другом языке. Я сделал следующий код действительно общим, чтобы получить минимальный воспроизводимый пример (поэтому он может выглядеть бессмысленным). У меня есть метод типа this.Process
, который вызывает приватную scan
функцию перед вызовом приватной cleanupProcess1
функции (которая запускает различные задачи «очистки»). По какой-то причине функции очистки вызываются, пока scan
все еще выполняется.
type SomeClass() =
let mutable charSeq = Seq.empty
let mutable chars = Array.empty
let mutable currentIndex = 0
let cleanupProcess2 () = printfn "called `cleanupProcess2`"
let cleanupProcess1 () =
printfn "called `cleanupProcess1`"
cleanupProcess2 ()
let next () =
currentIndex <- currentIndex + 1
try
Some(chars[currentIndex - 1])
with :? System.IndexOutOfRangeException ->
None
let scan () =
printfn "called `scan`"
let rec advance c =
seq {
printf "called `advance`... "
match c with
| Some(c) ->
printfn $"{c}"
yield c
yield! advance (next ())
| None ->
printfn "(`advance` complete)"
()
}
printfn "(before calling `advance`)"
charSeq <- advance (next ())
member this.Process(s: string) =
printfn "called `this.Process`"
chars <- s.ToCharArray()
scan () // this contains `advance`...
cleanupProcess1 () // ...and this executes before the above `scan`/`advance` completes?
printfn $"{charSeq |> Array.ofSeq |> System.String}"
let someClass = SomeClass()
someClass.Process("foobarbaz")
Выполнение этого кода должно привести к следующему выводу:
called `this.Process`
called `scan`
(before calling `advance`)
called `cleanupProcess1` # <--- why are these two being called early here?
called `cleanupProcess2` # <---
called `advance`... f
called `advance`... o
called `advance`... o
called `advance`... b
called `advance`... a
called `advance`... r
called `advance`... b
called `advance`... a
called `advance`... z
called `advance`... (`advance` complete)
# <--- instead of being called after `advance` completes here?
# <---
foobarbaz
Все компилируется без всяких предупреждений. Последовательность внутри scan
/advance
в конце концов строится, но не раньше, чем будут вызваны функции очистки. Я ожидал, что они будут вызваны после, так как вызов cleanupProcess1
появляется после вызова scan
/advance
внутри this.Process
. Это похоже на какую-то асинхронную проблему, но я нигде не использую async {}
, поэтому я не знаю, нужно ли что-то где-то «ждать»?
Последовательности F# (seq
) являются ленивыми, что означает, что advance
возвращается немедленно, а содержимое последовательности вычисляется позже, по запросу. В вашем случае это вызов Array.ofSeq
, который вызывает оценку, но это происходит только после запуска вашего кода очистки. Если вы знакомы с C#, это то же самое поведение, что и IEnumerable<T>, и, по сути, seq
реализовано через IEnumerable<T>
под одеялом. Обратите внимание, что ленивая оценка — это не то же самое, что асинхронная оценка. В вашем коде нет ничего асинхронного.
Чтобы быстро вычислить последовательность, вы можете вместо этого использовать массив:
let rec advance c =
[|
printf "called `advance`... "
match c with
| Some(c) ->
printfn $"{c}"
yield c
yield! advance (next ())
| None ->
printfn "(`advance` complete)"
()
|]
Результирующий вывод:
calling `this.Process`
calling `scan`
(before calling `advance`)
called `advance`... f
called `advance`... o
called `advance`... o
called `advance`... b
called `advance`... a
called `advance`... r
called `advance`... b
called `advance`... a
called `advance`... z
called `advance`... (`advance` complete)
calling `cleanupProcess1`
calling `cleanupProcess2`
foobarbaz
На самом деле seq<T>
реализуется не только через IEnumerable<T>
; это просто псевдоним для IEnumerable<T>
.
Не знал, что вы уже опубликовали ответ, я удалю свой.