具有历史记录的困难序列处理

Difficult sequence handling with history

我想我忘记了如何使用 F#:(

我正在实现一个控制台界面。因为它们可以持续很长时间,所以我使用一个序列来生成输入的行。在遇到 quit 命令时,yield 停止(也是在 EOF 时,对管道很重要)。

open System

type Return =
    | Code of int
    | Assert of int

let rec consoleLines () = seq {
    printf ">>> "
    match Console.ReadLine() with
    | null -> ()
    | line ->
        match line.Trim().ToLowerInvariant() with
        | "quit" -> printfn "Bye!"
        | line ->
            yield line
            yield! consoleLines()
}

let handle (str:string) = async {
    if str.StartsWith "noop" then return Code 0
    elif str.StartsWith "assert" then
        let num = str.Substring(7) |> int
        return Assert num
    elif str.StartsWith "sing" then
        printfn "La la la"
        return Code 0
    elif str.StartsWith "cry" then
        printfn "Boo hoo"
        return Code 2
    //elif other commands...
    else
        printfn "Unknown command: %s" str
        return Code -1
}

let results =
    consoleLines()
    |> Seq.map handle
    |> Seq.map Async.RunSynchronously

let firstBad =
    results
    // Want to execute all lines, not just up to the first not-ok.
    |> Seq.toList
    |> List.tryFind ((<>)(Code 0))

match firstBad with
| Some (Code error) ->
    eprintfn "Exiting with error code %i." error
    exit error
| _ ->
    printfn "Exiting normally."
    exit 0

我想 return 程序退出时的第一个 not-ok 代码(即 > 0),但应处理所有行。

例外是 assert 命令,它可能会或可能不会阻止后续命令的执行,具体取决于它的参数和之前的命令。但是当它退出时,程序 return 应该是崩溃命令 之前的最后一个值 。所以说我愿意

cry
assert 2
sing

程序哭了,但这是预料之中的,所以它也应该执行 sing 命令并以 0 退出。断言任何其他数字应该停止执行,return 2 并且不会让程序唱歌。

我尝试了很多东西,比如

  • Seq.pairwise 似乎是个好主意,但重复了(而且不起作用)
  • Seq.map 后跟 Seq.concat
  • Seq.unfold但不知道
  • 各种let rec loop () = seq { … loop() }
  • 几个seq { for (l, r) in … }

但我无法让它发挥任何作用。如果我有一个列表,我想这会很简单:

let matchExceptions =
    let rec matchExceptions acc = function
        | [] -> acc |> List.rev
        | Code code :: Assert ass :: rest when code = ass ->
            matchExceptions (0 :: acc) rest
        | Code code :: Assert _ :: _ ->
            [code]
        | Assert _ :: rest ->
            // Unmatched assert is useless.
            matchExceptions acc rest
        | Code code :: rest ->
            matchExceptions (code :: acc) rest
    matchExceptions []

但我无法将其转换为列表,因为它可能是无限的。我也不会在每次输入后得到输出。

我知道我不应该以 recursive/looping 的方式将 Seq.itemSeq.headSeq.tail 一起使用,因为那样可能会迅速呈二次方爆炸。

可能我可以获得 seq 可变变量的表达式,但是没有 nicer/more 函数式解决方案吗?我觉得我错过了什么。

您可以创建一个 Seq 辅助函数,它使用 Seq.scan 保存先前值的历史记录(感谢您的建议):

module Seq =
    let withHistory xs =
        ([], xs)
        ||> Seq.scan (fun history x -> x :: history )
        |> Seq.skip 1

Seq.initInfinite id |> Seq.withHistory
// val it : seq<int list> = seq [[0]; [1; 0]; [2; 1; 0]; [3; 2; 1; 0]; ...]

只有一个列表可以有效地将值附加到前面,并且只有一个额外的 seq 层。

嗯,这就是我现在拥有的:

let matchExceptions (source:seq<_>) = seq {
    let mutable prev = None
    let mutable stop = false
    use enm = source.GetEnumerator()
    while not stop && enm.MoveNext() do
        match prev, enm.Current with
        | Some (Code code), Assert exp ->
            if code = exp then
                printfn "Expectation matched, this calls for celebration."
                yield 0
            else
                printf "Expected %i but got %i." exp code
                stop <- true
                yield code
        | Some (Code code), _ ->
            yield code
        | _ -> ()
        prev <- Some enm.Current

    // Don't forget the last one.
    match prev, stop with
    | Some (Code code), false -> yield code
    | _ -> ()
}

它有效,但我仍然希望有一种更“功能上令人愉悦”的方式——即使它只是为了学习。