views:

278

answers:

3

I want to extract a single item from a sequence in F#, or give an error if there is none or more than one. What is the best way to do this?

I currently have

let element = data |> (Seq.filter (function | RawXml.Property (x) -> false | _ -> true))
                   |> List.of_seq
                   |> (function head :: [] -> head | head :: tail -> failwith("Too many elements.") | [] -> failwith("Empty sequence"))
                   |> (fun x -> match x with MyElement (data) -> x | _ -> failwith("Bad element."))

It seems to work, but is it really the best way?

Edit: As I was pointed in the right direction, I came up with the following:

let element = data |> (Seq.filter (function | RawXml.Property (x) -> false | _ -> true))
                   |> (fun s -> if Seq.length s <> 1 then failwith("The sequence must have exactly one item") else s)
                   |> Seq.hd
                   |> (fun x -> match x with MyElement (_) -> x | _ -> failwith("Bad element."))

I guess it's a little nicer.

+3  A: 

Sequence has a find function.

val find : ('a -> bool) -> seq<'a> -> 'a

but if you want to ensure that the seq has only one element, then doing a Seq.filter, then take the length after filter and ensure it equals one, and then take the head. All in Seq, no need to convert to a list.

Edit: On a side note, I was going to suggest checking that the tail of a result is empty (O(1), instead of using the function length (O(n)). Tail isn't a part of seq, but I think you can work out a good way to emulate that functionality.

nlucaroni
On long sequences with many matches this will fail very slowly, on infinite sequences then the correct result is to keep calculating to infinity or terminate early but it won't either (this difference is pretty marginal in utility though)
ShuggyCoUk
Yeah, I think infinite seq are going to get you either way, try to find something that isn't in an infinite list...But, the point of taking the length instead of just checking that the tail is empty is a good one, and what I originally wanted to mention, but seq doesn't have a tail function. I modified my post to reflect that limitation and use a function that's O(1).Thanks
nlucaroni
Seq's skip function will function as an alternate to tail, Seq.skip 1 filter should be empty and seq.hd after that will check it has at least one (hd will throw it's own exception if it's empty which is useful)
ShuggyCoUk
good call... thanks.
nlucaroni
+3  A: 

done in the style of the existing sequence standard functions

#light

let findOneAndOnlyOne f (ie : seq<'a>)  = 
    use e = ie.GetEnumerator()
    let mutable res = None 
    while (e.MoveNext()) do
        if f e.Current then
            match res with
            | None -> res <- Some e.Current
            | _ -> invalid_arg "there is more than one match"          
    done;
    match res with
        | None -> invalid_arg "no match"          
        | _ -> res.Value

You could do a pure implementation but it will end up jumping through hoops to be correct and efficient (terminating quickly on the second match really calls for a flag saying 'I found it already')

ShuggyCoUk
+1  A: 

Use this:

> let only s =
    if not(Seq.isEmpty s) && Seq.isEmpty(Seq.skip 1 s) then
      Seq.hd s
    else
      raise(System.ArgumentException "only");;
val only : seq<'a> -> 'a
Jon Harrop
Don't both skip and hd both compute the head (so if there are side effects, you see them twice)?
wrang-wrang
Yes. If they are slow or have undesirable side effects then you'll want to optimise this.
Jon Harrop