Seq.exactlyOne 的非抛出版本来测试单例序列

Non-throwing version of Seq.exactlyOne to test for a singleton sequence

更新:我为此创建了一个 UserVoice 请求:Expand on the Cardinality functions for Seq

我需要 Seq.exactlyOne 的功能,但具有 Some/None 语义。换句话说,我需要 Seq.head,或者,如果序列为空或包含多个项目,我什么都不需要。使用 Seq.exactlyOne 会在这种情况下抛出。

我不认为有一种内置的方式来获得它(尽管它听起来很微不足道,以至于人们会期望 Seq.singleton 有一个对应的方式)。我想到了这个,但感觉很费解:

let trySingleton sq = 
    match Seq.isEmpty sq with 
    | true -> None 
    | false -> 
        match sq |> Seq.indexed |> Seq.tryFind (fst >> ((=) 1)) with
        | Some _ -> None
        | None -> Seq.exactlyOne sq |> Some

给出:

> trySingleton [|1;2;3|];;
val it : int option = None
> trySingleton Seq.empty<int>;;
val it : int option = None
> trySingleton [|1|];;
val it : int option = Some 1

是否有更简单甚至内置的方法?我可以在 Seq.exactlyOne 上 try/catch,但这是围绕异常构建业务逻辑,我宁愿不这样做(而且它很昂贵)。

更新: 我不知道 Seq.tryItem 函数,这会使事情变得更简单:

let trySingleton sq =
    match sq |> Seq.tryItem 1 with
    | Some _ -> None
    | None -> Seq.tryHead sq

(好多了,但还是感觉比较别扭)

为什么不通过命令式处理枚举数来解决问题?

let trySingleton' (xs : seq<_>) =
    use en = xs.GetEnumerator()
    if en.MoveNext() then
        let res = en.Current
        if en.MoveNext() then None
        else Some res
    else None

trySingleton' Seq.empty<int>    // None
trySingleton' [1]               // Some 1
trySingleton' [1;2]             // None

我不知道这个有内置函数,但这里有另一种实现它的方法:

let tryExactlyOne xs =
    match xs |> Seq.truncate 2 |> Seq.toList with
    | [x] -> Some x
    | _   -> None

FSI 演示:

> [42] |> List.toSeq |> tryExactlyOne;;
val it : int option = Some 42

> [42; 1337] |> List.toSeq |> tryExactlyOne;;
val it : int option = None

> Seq.empty<int> |> tryExactlyOne;;
val it : int option = None