diff --git a/src/FSharp.Collections.Immutable/SortedSet/SortedSet.fs b/src/FSharp.Collections.Immutable/SortedSet/SortedSet.fs index a4f57a9e..8c58270e 100644 --- a/src/FSharp.Collections.Immutable/SortedSet/SortedSet.fs +++ b/src/FSharp.Collections.Immutable/SortedSet/SortedSet.fs @@ -146,40 +146,55 @@ let contains (element : 'T) (set : SortedSet<'T>) = checkNotNull (nameof set) set set.Contains element -// exists and forall loop over the struct enumerator of the sorted set, which allocates nothing. LINQ Any and All allocated -// 152 bytes per call, a delegate and the boxed enumerator, and Seq.exists and Seq.forall 64 bytes, the boxed enumerator -// (measured in Release; a measured faster path replaces a higher priority, D5). Dispose returns the pooled stack of the -// enumerator. +// exists and forall loop over the struct enumerator of the sorted set for a value type, which allocates nothing. LINQ Any and +// All allocated 152 bytes per call, a delegate and the boxed enumerator, and Seq.exists and Seq.forall 64 bytes, the boxed +// enumerator (measured in Release; a measured faster path replaces a higher priority, D5). A set of a reference type is walked +// through the interface instead, at the price of that boxed enumerator: the code that all reference types share needs a +// lookup of the exact type at run time for each call of a member of the generic struct enumerator, and for strings of 100 to +// 100 000 elements exists and forall took 0.47 to 0.57 times as long, where for integers the interface took 1.3 to 1.7 times +// as long. Dispose returns the pooled stack of the enumerator. [] let exists (predicate : 'T -> bool) (set : SortedSet<'T>) = checkNotNull (nameof set) set - let mutable enumerator = set.GetEnumerator () + let mutable found = false - try - let mutable found = false + if typeof<'T>.IsValueType then + let mutable enumerator = set.GetEnumerator () + + try + while not found && enumerator.MoveNext () do + found <- predicate enumerator.Current + finally + enumerator.Dispose () + else + use enumerator = (set :> seq<'T>).GetEnumerator() while not found && enumerator.MoveNext () do found <- predicate enumerator.Current - found - finally - enumerator.Dispose () + found [] let forall (predicate : 'T -> bool) (set : SortedSet<'T>) = checkNotNull (nameof set) set - let mutable enumerator = set.GetEnumerator () + let mutable satisfied = true - try - let mutable satisfied = true + if typeof<'T>.IsValueType then + let mutable enumerator = set.GetEnumerator () + + try + while satisfied && enumerator.MoveNext () do + satisfied <- predicate enumerator.Current + finally + enumerator.Dispose () + else + use enumerator = (set :> seq<'T>).GetEnumerator() while satisfied && enumerator.MoveNext () do satisfied <- predicate enumerator.Current - satisfied - finally - enumerator.Dispose () + satisfied [] let isProperSubset (set1 : SortedSet<'T>) (set2 : SortedSet<'T>) = diff --git a/tests/FSharp.Collections.Immutable.Tests/SortedSet/SortedSetTests.fs b/tests/FSharp.Collections.Immutable.Tests/SortedSet/SortedSetTests.fs index f7752e2c..c9e23a08 100644 --- a/tests/FSharp.Collections.Immutable.Tests/SortedSet/SortedSetTests.fs +++ b/tests/FSharp.Collections.Immutable.Tests/SortedSet/SortedSetTests.fs @@ -1071,6 +1071,87 @@ type SortedSetTests () = "SortedSet.maxElement of an empty sorted set must throw" ) + // ---- Search ---- + + // exists and forall walk a set of a reference type through the interface and a set of a value type through the struct + // enumerator, and the contracts run on integers, so strings have tests of their own + + [] + member _.``exists and forall of strings give the results of Set`` (count : int, position : string) = + let items = Array.init count (fun index -> "x" + index.ToString ()) + + let target = + match position with + | "first" -> "x0" + | "middle" -> "x" + (count / 2).ToString() + | "last" -> "x" + (count - 1).ToString() + | _ -> "absent" + + let set = SortedSet.ofArray items + let expected = Set.ofArray items + + Assert.AreEqual ( + Set.exists ((=) target) expected, + SortedSet.exists ((=) target) set, + "SortedSet.exists must find the element as Set.exists does" + ) + + Assert.AreEqual ( + Set.forall ((<>) target) expected, + SortedSet.forall ((<>) target) set, + "SortedSet.forall must reject the element as Set.forall does" + ) + + Assert.AreEqual ( + Set.exists (fun (text : string) -> text.Length > 0) expected, + SortedSet.exists (fun (text : string) -> text.Length > 0) set, + "SortedSet.exists must answer for a predicate that fits some elements as Set.exists does" + ) + + Assert.AreEqual ( + Set.forall (fun (text : string) -> text.Length > 0) expected, + SortedSet.forall (fun (text : string) -> text.Length > 0) set, + "SortedSet.forall must answer for a predicate that fits all elements as Set.forall does" + ) + + [] + member _.``exists of strings stops at the first match`` () = + let calls = ref 0 + + let found = + SortedSet.exists + (fun word -> + calls.Value <- calls.Value + 1 + word = "A" + ) + (SortedSet.ofArray words) + + Assert.IsTrue (found, "SortedSet.exists must find the first string in the order") + Assert.AreEqual (1, calls.Value, "SortedSet.exists must not call the predicate after the first match") + + [] + member _.``forall of strings stops at the first element that fails`` () = + let calls = ref 0 + + let holds = + SortedSet.forall + (fun word -> + calls.Value <- calls.Value + 1 + word <> "A" + ) + (SortedSet.ofArray words) + + Assert.IsFalse (holds, "SortedSet.forall must fail on the first string in the order") + Assert.AreEqual (1, calls.Value, "SortedSet.forall must not call the predicate after the first failure") + // ---- Null inputs ---- []