在 F# 中,如何在较大管道的上下文中使用 Seq.unfold?

In F#, How do I use Seq.unfold in the context of a larger pipeline?

我有一个包含两列的 CSV 文件,text 和 count。目标是从以下文件转换文件:

some text once,1
some text twice,2
some text thrice,3

为此:

some text once,1
some text twice,1
some text twice,1
some text thrice,1
some text thrice,1
some text thrice,1

重复每行数次并将计数分散到那么多行。

在我看来,这很适合 Seq.unfold,在我们读取文件时生成额外的行。我有以下生成器函数:

let expandRows (text:string, number:int32) =
    if number = 0 
    then None
    else
        let element = text                  // "element" will be in the generated sequence
        let nextState = (element, number-1) // threaded state replacing looping 
        Some (element, nextState)

FSI 产生以下函数签名:

val expandRows : text:string * number:int32 -> (string * (string * int32)) option

在 FSI 中执行以下操作:

let expandedRows = Seq.unfold expandRows ("some text thrice", 3)

产生预期的结果:

val it : seq<string> = seq ["some text thrice"; "some text thrice"; "some text thrice"]

问题是:如何将其插入更大的 ETL 管道的上下文中?例如:

File.ReadLines(inFile)                  
    |> Seq.map createTupleWithCount
    |> Seq.unfold expandRows // type mismatch here
    |> Seq.iter outFile.WriteLine

以下错误发生在管道上下文中的 expandRows 上。

Type mismatch. 
Expecting a 'seq<string * int32> -> ('a * seq<string * int32>) option'    
but given a     'string * int32 -> (string * (string * int32)) option' 
The type    'seq<string * int 32>' does not match the type 'string * int32'

我原以为 expandRows 会返回字符串序列,就像在我的独立测试中一样。因为这既不是 "Expecting" 也不是 "given",我很困惑。有人能指出我正确的方向吗?

代码要点在这里: https://gist.github.com/akucheck/e0ff316e516063e6db224ab116501498

听起来你想做的实际上是

File.ReadLines(inFile)                  
|> Seq.map createTupleWithCount
|> Seq.map (Seq.unfold expandRows) // Map each tuple to a seq<string>
|> Seq.concat // Flatten the seq<seq<string>> to seq<string>
|> Seq.iter outFile.WriteLine

因为您似乎想通过 Seq.unfoldexpandRows 将序列中每个带有计数的元组转换为 seq<string>。这是通过映射完成的。

之后,您想将 seq<seq<string>> 展平成一个大的 seq<string>,它位于 Seq.concat.

下方

Seq.map 产生一个序列,但 Seq.unfold 不接受序列,它接受单个值。所以你不能直接将 Seq.map 的输出通过管道传输到 Seq.unfold。您需要逐个元素地进行。

但是,对于每个元素,您的 Seq.unfold 都会产生一个序列,因此最终结果将是一个序列的序列。您可以使用 Seq.collect:

在单个序列中收集所有这些 "subsequences"
File.ReadLines(inFile) 
    |> Seq.map createTupleWithCount 
    |> Seq.collect (Seq.unfold expandRows)
    |> Seq.iter outFile.WriteLine

Seq.collect 接受一个函数和一个输入序列。对于输入序列的每个元素,该函数应该产生另一个序列,并且 Seq.collect 会将所有这些序列连接成一个。您可能会认为 Seq.collectSeq.mapSeq.concat 组合在一个函数中。此外,如果您来自 C#,Seq.collect 在那边称为 SelectMany

在这种情况下,由于您只是想多次重复某个值,因此没有理由使用 Seq.unfold。您可以使用 Seq.replicate 代替:

// 'a * int -> seq<'a>
let expandRows (text, number) = Seq.replicate number text

可以用Seq.collect来合成:

File.ReadLines(inFile)
|> Seq.map createTupleWithCount
|> Seq.collect expandRows
|> Seq.iter outFile.WriteLine

事实上,此版本的 expandRows 执行的唯一工作是 'unpack' 一个元组并将其值组合成柯里化形式。

虽然 F# 的核心库中没有这样的通用函数,但您可以轻松定义它(和 other similarly useful functions):

module Tuple2 =
    let curry f x y = f (x, y)    
    let uncurry f (x, y) = f x y    
    let swap (x, y) = (y, x)

这将使您能够从众所周知的功能构建块组成您的管道:

File.ReadLines(inFile)
|> Seq.map createTupleWithCount
|> Seq.collect (Tuple2.swap >> Tuple2.uncurry Seq.replicate)
|> Seq.iter outFile.WriteLine