Search code examples
f#singletonsequenceseq

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


Update: I created a UserVoice request for this: Expand on the Cardinality functions for Seq.

I need the functionality of Seq.exactlyOne, but with Some/None semantics. In other words, I need either Seq.head, or, if the sequence is empty or contains more than one item, I need nothing. Using Seq.exactlyOne will throw in such cases.

I don't think there's a built-in way of getting this (though it sounds so trivial that one would expect there is a counterpart for Seq.singleton). I came up with this, but it feels convoluted:

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

Gives:

> 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

Is there a simpler, or even a built-in way? I could try/catch on Seq.exactlyOne, but that is building business logic around exceptions, I'd rather not (and it is expensive).

UPDATE: I wasn't aware of the Seq.tryItem function, which would make this simpler:

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

(better, but it still feels rather awkward)


Solution

  • Why not approach the problem by handling the enumerator imperatively?

    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