用每个元素的(简单)计算状态装饰 F# 序列

Decorate an F# sequence with (simple) computed state for each element

我有一个解决方案,还有几个可行但不令人满意的解决方案,但它花费了大量的工作,而且似乎不必要地复杂。

我在 F# 中遗漏了什么吗?

问题

我有一个数字序列

let nums = seq { 9; 12; 4; 17; 9; 7; 13; }

我想给每个数字都加上一个“索引”,所以结果是

seq [(9, 0); (12, 1); (4, 2); (17, 3); ...]

看起来很简单!

在实践中,输入可能非常大且大小不确定。在我的应用程序中,它来自 REST 服务。

进一步

让我们调用 (seq<'a> -> seq<'a * int>) 类型的函数 decorate,它会按如下方式工作:

nums
|> decorate
|> Seq.iter (fun (n,index) -> printfn "%d: %d" index n)

制作中:

0: 9
1: 12
2: 4
...
6: 13

对于列表来说这是一个微不足道的问题(除了惰性求值),但对于序列来说就很棘手了。

我的解决方案是使用Seq.unfold,如下:

let decorate numSeq = 
    (0,numSeq) 
    |> Seq.unfold 
        (fun (count,(nums : int seq)) -> 
            if Seq.isEmpty nums then
                None
            else
                let result = ((Seq.head nums),count)
                let remaining = Seq.tail nums
                Some( result, (count+1,remaining)))

这符合所有要求,是我想到的最好的。

这是完整的解决方案,带有显示延迟计算的诊断:

let nums = 
    seq {
        // With diagnostic
        let getN n =
            printfn "get: %d" n
            n

        getN 9; 
        getN 12; 
        getN 4; 
        getN 17; 
        getN 9; 
        getN 7; 
        getN 13 
    }
    
let decorate numSeq = 
    (0,numSeq) 
    |> Seq.unfold 
        (fun (count,(nums : int seq)) -> 
            if Seq.isEmpty nums then
                None
            else
                let result = ((Seq.head nums),count)
                let remaining = Seq.tail nums
                printfn "unfold: %A" result
                Some( result, (count+1,remaining)))

nums
|> Seq.cache 
    // To prevent re-computation of the sequence.
    // Will be necessary for any solution. This solution required only one.
|> decorate
|> Seq.iter (fun (n,index) -> printfn "ITEM %d: %d" index n)

问题: 这需要很多工作才能实现。与(显然)简单的要求相比,它看起来很复杂。

问题:是否有更简单的解决方案?


一些备选方案的讨论。

所有工作,但由于给出的原因不令人满意

// Most likely: Seq.mapFold
// Fails lazy evalation. The final state must be evaluated, even if not used
let decorate numSeq = 
    numSeq
    |> Seq.mapFold 
        (fun count num -> 
            let result = (num,count)
            printfn "yield: %A" result
            (result,(count + 1)))
        0
    |> fun (nums,finalState) -> nums // And, no, using "_" doesn't fix it!


// 'for' loop, with MUTABLE
// Lazy evaluation works
// Not extensible, as the state 'count' is specific to this problem
let decorate numSeq =
    let mutable count = 0
    seq { 
        for num in numSeq do 
            let result = num,count
            printfn "yield: %A" result
            yield result; 
            count <- count+1
    }

// 'for' loop, without mutable
// Fails lazy evaluation, and is ugly 
let decorate numSeq =
    seq {
        for index in 0..((Seq.length numSeq) - 1) do
            let result = ((Seq.item index numSeq), // Ugly!
                            index) 
            printfn "yield: %A" result
            yield result
    }

// "List" like recursive descent, 
// Fails lazy evaluation. Ugly, because we are not meant to use recursion on Sequences
// 
let rec decorate' count (nums : int seq) =
    if Seq.isEmpty nums then
        Seq.empty
    else
        let hd = Seq.head nums
        let tl = Seq.tail nums
        let result = (hd,count)
        let tl' = decorate' (count+1) tl
        printfn "yield: %A" result
        seq { yield result; yield! tl'}

let decorate : (seq<'a> -> seq<'a * int>) = decorate' 0 

您可以使用 Seq.mapi 来做您需要的事情。

let nums = seq { 9; 12; 4; 17; 9; 7; 13; }
nums |> Seq.mapi (fun i num  -> (num, i)) 

这给出 (9, 0); (12, 1); etc...

Seq 与 C# 中的 IEnumerable 意义相同。

您可以在此处阅读有关 Seq.mapi 的信息:

https://fsharp.github.io/fsharp-core-docs/reference/fsharp-collections-seqmodule.html#mapi

在此处详细了解 map 的使用:

https://fsharpforfunandprofit.com/posts/elevated-world/#map

除了Sean的回答中提到的Seq.mapi函数,F#还有一个内置的Seq.indexed函数,用索引修饰一个序列。这并不完全符合您的要求,因为索引成为元组的第一个元素,但根据您的用例,它可能会起作用:

> let nums = seq { 9; 12; 4; 17; 9; 7; 13; };;
val nums : seq<int>

> Seq.indexed nums;;
val it : seq<int * int> = seq [(0, 9); (1, 12); (2, 4); (3, 17); ...]

如果我尝试使用更原始的函数自行实现它,可以使用 Seq.scan 来完成,这有点像折叠,但会产生一个惰性状态序列。唯一棘手的事情是你必须构造初始状态,然后处理序列的其余部分:

Seq.tail nums 
|> Seq.scan (fun (prevIndex, _) v -> (prevIndex+1, v)) (0, Seq.head nums)

这不适用于空列表,即使函数在逻辑上应该能够处理这个问题。

  1. for也不错,还是错的。 foryieldseq {} 中是你编写新的 seq 函数的方式,如果 Seq 模块中提供的函数中的 none 是最好的-合身。使用这种特殊结构既没有错也没有坏处。它与 C# foreachyield 语法相同。

  2. 在有限范围内使用可变变量,也没有错。可变变量是一个坏主意,如果它们超出了范围。例如,您 return 来自函数的可变值。

  3. 将可变变量放在 seq 内部而不是外部很重要。你的版本不对。

让我们假设这个

let xs = decorate [3;6;7;12;9]

for x in xs do
    printfn "%A" x

for x in xs do
    printfn "%A" x

现在你有两个版本的装饰。第一版

let decorate numSeq = 
    let mutable count = 0
    seq { 
        for num in numSeq do 
            yield (num,count)
            count <- count + 1
    }

将打印:

(3, 0)
(6, 1)
(7, 2)
(12, 3)
(9, 4)
(3, 5)
(6, 6)
(7, 7)
(12, 8)
(9, 9)

或者换句话说。每当您遍历序列时,可变变量就会在所有调用中共享。作为一般提示。如果你想 return 一个 seq 然后把你所有的代码放到 seq 中。并将 seq {} 放在 = 符号之后。如果您改为这样做。

let decorate numSeq = seq { 
    let mutable count = 0
    for num in numSeq do 
        yield (num,count)
        count <- count + 1
}

你得到正确的输出:

(3, 0)
(6, 1)
(7, 2)
(12, 3)
(9, 4)
(3, 0)
(6, 1)
(7, 2)
(12, 3)
(9, 4)

请您解释一下,这个版本不是“可扩展的”。但是 mapi 你 select 的版本是“正确的”。有同样的问题,它只提供一个索引,仅此而已。

如果你想要一个更通用的版本,你总是可以创建一个函数,它期望它的值作为函数参数。例如,您可以将上述功能更改为此代码。

let decorate2 f (state:'State) (xs:'T seq) = seq {
    let mutable state = state
    for x in xs do
        yield state, x
        let newState = f state x
        state <- newState
}

现在decorate2需要一个你可以自由通过的状态,以及一个改变状态的函数。使用此功能,您可以编写:

decorate2 (fun state _ -> state+1) 0 [3;6;7;12;9]

函数签名与Seq.scan几乎相同,但还是有一点不同。但是如果你想创建一个 indexed 函数,你可以像这样使用 scan

let indexed xs =
    Seq.scan (fun (count,_) x -> (count+1,x)) (0,Seq.head xs) (Seq.skip 1 xs)

仅在我看来。与 decoratedecorate2.

相比,这个版本更难阅读、理解,而且非常糟糕

还有一张纸条。标准库中已经有一个 Seq.indexed 函数,可以满足您的需求。

for x in Seq.indexed [3;6;7;12;9] do
    printfn "%A" x

将打印

(0, 3)
(1, 6)
(2, 7)
(3, 12)
(4, 9)