diff --git a/src/FSharp.Collections.Immutable/FSharp.Collections.Immutable.fsproj b/src/FSharp.Collections.Immutable/FSharp.Collections.Immutable.fsproj
index f66316ea..5fe67a27 100644
--- a/src/FSharp.Collections.Immutable/FSharp.Collections.Immutable.fsproj
+++ b/src/FSharp.Collections.Immutable/FSharp.Collections.Immutable.fsproj
@@ -37,6 +37,8 @@
+
+
diff --git a/src/FSharp.Collections.Immutable/Helper/Buffers.fs b/src/FSharp.Collections.Immutable/Helper/Buffers.fs
new file mode 100644
index 00000000..bea7f019
--- /dev/null
+++ b/src/FSharp.Collections.Immutable/Helper/Buffers.fs
@@ -0,0 +1,125 @@
+///
+/// The buffer in which a function collects the elements that it chooses one by one, until it builds the tree of the result
+/// (ADR 0001, D5).
+///
+///
+///
+/// A result of unknown length waits in a that doubles its array as it
+/// fills, and the tree is built once, from its elements. The growth costs the copies, and for a result of some 20 000
+/// integers or 10 000 references or more the arrays of the growth are allocated in the large object heap, which a collection
+/// of the second generation reclaims; for a million elements the list allocated 18 % (integers) to 35 % (strings) more than
+/// the tree needs.
+///
+///
+/// The number of elements to choose from is known, and it bounds the result. The buffer takes an array of that length: a
+/// new one for a short result, where the array is cheap and the pool would only add its own work, and one rented from
+/// for a long one, which allocates nothing in the steady state. An array
+/// of the length of the source is more than the result needs when few of the elements are chosen, and it would be the
+/// array of a hundred million elements for a list of that length. The rented array is therefore at most
+/// elements long; a longer result moves into a list.
+///
+///
+/// A buffer that is not turned into a list, because the chooser threw, is not returned to the pool. The pool allocates
+/// another array, and nothing leaks.
+///
+///
+module internal FSharp.Collections.Immutable.Buffers
+
+open System
+open System.Buffers
+open System.Collections.Generic
+open System.Collections.Immutable
+open System.Runtime.CompilerServices
+open System.Runtime.InteropServices
+
+///
+/// The least length of the source for which the buffer is rented from the pool.
+///
+///
+/// For 4 to 2048 elements, an array of its own and a rented one took the same time, 0 to 20 % less than the list when all
+/// of the elements were chosen; the first call that rents an array of a length allocates it, and the first call took 6 to
+/// 81 µs for 4 to 2048 elements.
+/// An array below the limit is a small allocation that the next collection of the youngest generation reclaims.
+///
+[]
+let PooledFrom = 1024
+
+///
+/// The most elements in the array that the buffer rents; a result that is longer moves into a list.
+///
+[]
+let PooledUpTo = 1048576
+
+///
+/// Collects the elements that a function chooses from a source of a known length, and builds the indexed list of them.
+///
+/// The type of the elements.
+///
+/// A buffer is used once: ends it.
+///
+[]
+type ChosenBuffer<'T> (bound : int) =
+
+ let pooled = bound >= PooledFrom
+
+ let mutable buffer : 'T array =
+ if bound = 0 then
+ Array.Empty<'T>()
+ elif pooled then
+ ArrayPool<'T>.Shared.Rent(min bound PooledUpTo)
+ else
+ Array.zeroCreate bound
+
+ let mutable count = 0
+ let mutable overflow : ResizeArray<'T> | null = null
+
+ /// Returns the rented array to the pool, cleared as far as it was written when it holds references. The buffer is
+ /// left with an empty array, which is full at once, so that an element goes to the slow path of Add.
+ member private _.Release () =
+ if pooled && buffer.Length > 0 then
+ if RuntimeHelpers.IsReferenceOrContainsReferences<'T>() then
+ Array.Clear (buffer, 0, count)
+
+ ArrayPool<'T>.Shared.Return(buffer, false)
+
+ buffer <- Array.Empty<'T>()
+ count <- 0
+
+ /// Moves the elements of the full array into a list that grows, and gives the array back.
+ member private this.Spill () : ResizeArray<'T> =
+ let spilled = ResizeArray<'T>(buffer.Length * 2)
+ CollectionsMarshal.SetCount (spilled, count)
+ ReadOnlySpan<'T>(buffer, 0, count).CopyTo(CollectionsMarshal.AsSpan spilled)
+ this.Release ()
+ overflow <- spilled
+ spilled
+
+ []
+ member private this.AddSlow (value : 'T) =
+ match overflow with
+ | null -> this.Spill().Add value
+ | spilled -> spilled.Add value
+
+ ///
+ /// Appends an element.
+ ///
+ /// The element.
+ []
+ member this.Add (value : 'T) =
+ if count = buffer.Length then
+ this.AddSlow value
+ else
+ buffer[count] <- value
+ count <- count + 1
+
+ ///
+ /// The indexed list of the elements that were added, in their order.
+ ///
+ /// The indexed list, which is empty when nothing was added.
+ member this.ToImmutable () : ImmutableList<'T> =
+ match overflow with
+ | null ->
+ let result = ImmutableList.Create<'T>(ReadOnlySpan<'T>(buffer, 0, count))
+ this.Release ()
+ result
+ | spilled -> ImmutableList.Create<'T>(Span<'T>.op_Implicit(CollectionsMarshal.AsSpan spilled))
diff --git a/src/FSharp.Collections.Immutable/Helper/Walk.fs b/src/FSharp.Collections.Immutable/Helper/Walk.fs
new file mode 100644
index 00000000..8ea26c3b
--- /dev/null
+++ b/src/FSharp.Collections.Immutable/Helper/Walk.fs
@@ -0,0 +1,100 @@
+///
+/// The loops over the elements of an , which walk it with the
+/// enumerator that is the faster one for the type of the elements (ADR 0001, D5).
+///
+///
+///
+/// ImmutableList<T>.GetEnumerator returns a struct. For a value type that is the fastest way to walk the list: the
+/// compiler knows the struct, and its calls are direct and inlined. For a reference type the code of a generic function is
+/// shared by all reference types, and each call of a member of a generic struct in it needs a lookup of the exact type
+/// at run time. Walking the list through instead, with a boxed
+/// enumerator and calls through the interface, took 0.56 to 0.70 times as long for strings (6 against 10 ns for an
+/// element) and 1.1 to 1.5 times as long for integers. As functions of the library, iter and fold took 0.6 to 0.7 times
+/// as long for strings of 100 to 100 000 elements, tryPick 0.6, mapi and scan 0.7 to 0.9, and none of them changed for
+/// integers. The loop of mapFold stays as it was, because the same loop took 1.1 to 1.4 times as long there.
+///
+///
+/// The test typeof<'T>.IsValueType is a constant for the compiler of the run time, so each instantiation of a
+/// function keeps one of the two loops. The functions are inline, and the body of a loop is passed as a lambda that the
+/// compiler inlines into both loops, so that a body is written once. A body cannot capture a mutable local, which is why
+/// and
+/// count and carry the state themselves.
+///
+///
+module internal FSharp.Collections.Immutable.Walk
+
+open System.Collections.Immutable
+
+///
+/// Calls an action for each element of a list, in order.
+///
+/// The action.
+/// The list, which is not .
+let inline iter ([] action : 'T -> unit) (list : ImmutableList<'T>) =
+ if typeof<'T>.IsValueType then
+ for item in list do
+ action item
+ else
+ for item in (list :> seq<'T>) do
+ action item
+
+///
+/// Calls an action for each element of a list, in order, with the index of the element.
+///
+/// The action.
+/// The list, which is not .
+let inline iteri ([] action : int -> 'T -> unit) (list : ImmutableList<'T>) =
+ let mutable index = 0
+
+ if typeof<'T>.IsValueType then
+ for item in list do
+ action index item
+ index <- index + 1
+ else
+ for item in (list :> seq<'T>) do
+ action index item
+ index <- index + 1
+
+///
+/// Threads a state through the elements of a list, in order.
+///
+/// The function that returns the next state for the state and the element.
+/// The first state.
+/// The list, which is not .
+/// The last state.
+let inline fold ([] folder : 'State -> 'T -> 'State) (state : 'State) (list : ImmutableList<'T>) =
+ let mutable state = state
+
+ if typeof<'T>.IsValueType then
+ for item in list do
+ state <- folder state item
+ else
+ for item in (list :> seq<'T>) do
+ state <- folder state item
+
+ state
+
+///
+/// The first value that a chooser returns for the elements of a list, in order, without a call for the elements after it.
+///
+/// The chooser.
+/// The list, which is not .
+/// The first value, or .
+let inline tryPick ([] chooser : 'T -> 'U voption) (list : ImmutableList<'T>) =
+ let mutable result = ValueNone
+
+ if typeof<'T>.IsValueType then
+ let mutable enumerator = list.GetEnumerator ()
+
+ try
+ while result.IsNone && enumerator.MoveNext () do
+ result <- chooser enumerator.Current
+ finally
+ enumerator.Dispose ()
+ else
+ use enumerator = (list :> seq<'T>).GetEnumerator()
+
+ while result.IsNone && enumerator.MoveNext () do
+ result <- chooser enumerator.Current
+
+ result
diff --git a/src/FSharp.Collections.Immutable/IndexedList/IIndexedList.fs b/src/FSharp.Collections.Immutable/IndexedList/IIndexedList.fs
index 5e3356ff..ec7aecff 100644
--- a/src/FSharp.Collections.Immutable/IndexedList/IIndexedList.fs
+++ b/src/FSharp.Collections.Immutable/IndexedList/IIndexedList.fs
@@ -370,17 +370,15 @@ let append (list1 : IIndexedList<'T>) (list2 : IIndexedList<'T>) =
[]
let choose (chooser : 'T -> 'U voption) (list : IIndexedList<'T>) =
checkNotNull (nameof list) list
- // The number of chosen elements is not known in advance, so they wait in a ResizeArray and CreateRange builds the
- // balanced tree once. A builder rebalances its tree after every Add and took 2.3 times as long for 100 000
- // elements; there is no tree of an input to join to its tree here, as collect and concat have (see Collectors).
- let chosen = ResizeArray<'U>()
+ // As IndexedList.choose: the chosen elements wait in an array as long as the list (see Buffers)
+ let chosen = Buffers.ChosenBuffer<'U> list.Count
for item in list do
match chooser item with
| ValueSome value -> chosen.Add value
| ValueNone -> ()
- ImmutableList.CreateRange chosen
+ chosen.ToImmutable ()
[]
let chunkBySize chunkSize (list : IIndexedList<'T>) =
diff --git a/src/FSharp.Collections.Immutable/IndexedList/IndexedList.fs b/src/FSharp.Collections.Immutable/IndexedList/IndexedList.fs
index dd8044f5..5c09ce2a 100644
--- a/src/FSharp.Collections.Immutable/IndexedList/IndexedList.fs
+++ b/src/FSharp.Collections.Immutable/IndexedList/IndexedList.fs
@@ -28,18 +28,7 @@ let private range (list : IndexedList<'T>) (start : int) (count : int) =
ofFreshArray items
/// The first value that the chooser returns for the elements, in their order.
-let private firstChoice (chooser : 'T -> 'U voption) (list : IndexedList<'T>) =
- let mutable enumerator = list.GetEnumerator ()
-
- try
- let mutable result = ValueNone
-
- while result.IsNone && enumerator.MoveNext () do
- result <- chooser enumerator.Current
-
- result
- finally
- enumerator.Dispose ()
+let private firstChoice (chooser : 'T -> 'U voption) (list : IndexedList<'T>) = Walk.tryPick chooser list
/// A range of an indexed list sorted stably: the range is sorted in a copy, with LINQ, which sorts stably, and put back
/// in place of the range. ImmutableList.Sort(Int32, Int32, IComparer) is not stable (ADR 0001, D5).
@@ -76,7 +65,12 @@ let ofArray (array : 'T array) =
ImmutableList.CreateRange array
[]
-let ofList (list : 'T list) = ImmutableList.CreateRange list
+let ofList (list : 'T list) =
+ // An F# list is an IReadOnlyList whose indexer walks the list from its head, and the BCL builds a tree by indexing a
+ // list that implements IReadOnlyList: ImmutableList.CreateRange of a list took 0.43 ms for 1000 elements, 41 ms for
+ // 10 000 and 5.9 s for 100 000, a time that grows with the square of the length. The copy in an array takes 8 µs, 90 µs
+ // and 1.4 ms for the same lists, and the tree is built from the array.
+ ofFreshArray (List.toArray list)
[]
let ofReadOnlySpan (span : ReadOnlySpan<'T>) = ImmutableList.Create<'T> span
@@ -85,7 +79,10 @@ let ofReadOnlySpan (span : ReadOnlySpan<'T>) = ImmutableList.Create<'T> span
let ofSeq (source : seq<'T>) =
// A default flat list given as a sequence is a null input; the BCL would fail while it enumerates it (ADR 0001, D6)
checkNotNullOrDefaultSeq (nameof source) source
- ImmutableList.CreateRange source
+
+ match source with
+ | :? ('T list) as list -> ofList list
+ | _ -> ImmutableList.CreateRange source
[]
let ofSpan (span : Span<'T>) = ImmutableList.Create<'T>(Span<'T>.op_Implicit span)
@@ -500,17 +497,26 @@ let append (list1 : IndexedList<'T>) (list2 : IndexedList<'T>) =
[]
let choose (chooser : 'T -> 'U voption) (list : IndexedList<'T>) =
checkNotNull (nameof list) list
- // The number of chosen elements is not known in advance, so they wait in a ResizeArray and CreateRange builds the
- // balanced tree once. A builder rebalances its tree after every Add and took 2.3 times as long for 100 000
+ // The chosen elements wait in an array as long as the list, the most that the result can have: a new one below 1024
+ // elements, and one rented from the pool above (see Buffers). The ResizeArray that was here doubles its array as it
+ // fills, and allocated 18 % more than the tree needs for a million integers and 35 % more for a million strings; the
+ // arrays of its growth in the large object heap made the collections of the second generation more frequent. The
+ // array took the same time for integers of 10 to 10 000 elements and up to 10 % less above; a buffer of chunks that
+ // are joined to the result as subtrees left a tree whose indexer was 6 to 10 % slower (the average of two runs, 100 to
+ // 100 000 elements), and it took no less time than the array. A builder rebalances its tree
+ // after every Add and took 3.3 to 3.6 times as long for integers and 2.5 times for strings of 100 000 to a million
// elements; there is no tree of an input to join to its tree here, as collect and concat have (see Collectors).
- let chosen = ResizeArray<'U>()
-
- for item in list do
- match chooser item with
- | ValueSome value -> chosen.Add value
- | ValueNone -> ()
+ let chosen = Buffers.ChosenBuffer<'U> list.Count
+
+ Walk.iter
+ (fun item ->
+ match chooser item with
+ | ValueSome value -> chosen.Add value
+ | ValueNone -> ()
+ )
+ list
- ImmutableList.CreateRange chosen
+ chosen.ToImmutable ()
[]
let chunkBySize chunkSize (list : IndexedList<'T>) =
@@ -537,9 +543,7 @@ let collect (mapping : 'T -> IndexedList<'U>) (list : IndexedList<'T>) =
// The collector copies the short results and joins the long ones to its tree as they are (see Collectors)
let collector = Collectors.ListCollector<'U>()
- for item in list do
- collector.Add (mapping item)
-
+ Walk.iter (fun item -> collector.Add (mapping item)) list
collector.ToImmutable ()
[]
@@ -615,12 +619,7 @@ let map3 (mapping : 'T1 -> 'T2 -> 'T3 -> 'U) (list1 : IndexedList<'T1>) (list2 :
let mapi (mapping : int -> 'T -> 'U) (list : IndexedList<'T>) =
checkNotNull (nameof list) list
let results = Array.zeroCreate list.Count
- let mutable index = 0
-
- for item in list do
- results[index] <- mapping index item
- index <- index + 1
-
+ Walk.iteri (fun index item -> results[index] <- mapping index item) list
ofFreshArray results
[]
@@ -846,12 +845,7 @@ let compareWith (comparer : 'T -> 'T -> int) (list1 : IndexedList<'T>) (list2 :
[]
let fold (folder : 'State -> 'T -> 'State) (state : 'State) (list : IndexedList<'T>) =
checkNotNull (nameof list) list
- let mutable state = state
-
- for item in list do
- state <- folder state item
-
- state
+ Walk.fold folder state list
[]
let fold2 (folder : 'State -> 'T1 -> 'T2 -> 'State) (state : 'State) (list1 : IndexedList<'T1>) (list2 : IndexedList<'T2>) =
@@ -880,8 +874,7 @@ let iter (action : 'T -> unit) (list : IndexedList<'T>) =
checkNotNull (nameof list) list
// ForEach calls a delegate for every element; the loop calls the function directly
- for item in list do
- action item
+ Walk.iter action list
[]
let iter2 (action : 'T1 -> 'T2 -> unit) (list1 : IndexedList<'T1>) (list2 : IndexedList<'T2>) =
@@ -895,11 +888,7 @@ let iter2 (action : 'T1 -> 'T2 -> unit) (list1 : IndexedList<'T1>) (list2 : Inde
[]
let iteri (action : int -> 'T -> unit) (list : IndexedList<'T>) =
checkNotNull (nameof list) list
- let mutable index = 0
-
- for item in list do
- action index item
- index <- index + 1
+ Walk.iteri action list
[]
let iteri2 (action : int -> 'T1 -> 'T2 -> unit) (list1 : IndexedList<'T1>) (list2 : IndexedList<'T2>) =
@@ -914,6 +903,10 @@ let iteri2 (action : int -> 'T1 -> 'T2 -> unit) (list1 : IndexedList<'T1>) (list
let mapFold (mapping : 'State -> 'T -> struct ('Result * 'State)) (state : 'State) (list : IndexedList<'T>) =
checkNotNull (nameof list) list
let results = Array.zeroCreate list.Count
+
+ // Not a Walk loop: for strings, the loop of Walk took 1.1 to 1.4 times as long as this one once it was compiled into the
+ // library, although its machine code differs only in the order of a copy of the struct that the mapping returns, and
+ // the same source in a script ran 0.8 times as long. The loops of iter, fold, mapi and scan took 0.6 to 0.9 times as long.
let mutable state = state
let mutable index = 0
@@ -1052,12 +1045,7 @@ let scan (folder : 'State -> 'T -> 'State) (state : 'State) (list : IndexedList<
checkNotNull (nameof list) list
let states = Array.zeroCreate (list.Count + 1)
states[0] <- state
- let mutable index = 0
-
- for item in list do
- states[index + 1] <- folder states[index] item
- index <- index + 1
-
+ Walk.iteri (fun index item -> states[index + 1] <- folder states[index] item) list
ofFreshArray states
[]
diff --git a/src/FSharp.Collections.Immutable/IndexedList/IndexedList.fsi b/src/FSharp.Collections.Immutable/IndexedList/IndexedList.fsi
index 6d2fbbce..132c1c92 100644
--- a/src/FSharp.Collections.Immutable/IndexedList/IndexedList.fsi
+++ b/src/FSharp.Collections.Immutable/IndexedList/IndexedList.fsi
@@ -65,7 +65,10 @@ val ofArray : array : 'T array -> IndexedList<'T>
/// Creates an indexed list from a list.
/// The input list.
/// The indexed list of the elements of the list.
-/// O(n).
+///
+/// O(n). The elements are copied into an array first, because the BCL builds the tree by indexing and the indexer of an
+/// F# list walks the list from its head, so that building the tree from the list itself takes O(n²).
+///
[]
val ofList : list : 'T list -> IndexedList<'T>
@@ -89,6 +92,7 @@ val ofReadOnlySpan : span : ReadOnlySpan<'T> -> IndexedList<'T>
///
/// O(n). The function wraps ,
/// which returns an indexed list itself, without a copy.
+/// An F# list is copied into an array first, as does.
///
[]
val ofSeq : source : seq<'T> -> IndexedList<'T>
diff --git a/tests/FSharp.Collections.Immutable.Tests/FSharp.Collections.Immutable.Tests.fsproj b/tests/FSharp.Collections.Immutable.Tests/FSharp.Collections.Immutable.Tests.fsproj
index 12eeec14..b1950bfc 100644
--- a/tests/FSharp.Collections.Immutable.Tests/FSharp.Collections.Immutable.Tests.fsproj
+++ b/tests/FSharp.Collections.Immutable.Tests/FSharp.Collections.Immutable.Tests.fsproj
@@ -73,6 +73,7 @@
+
diff --git a/tests/FSharp.Collections.Immutable.Tests/IndexedList/ChosenBufferTests.fs b/tests/FSharp.Collections.Immutable.Tests/IndexedList/ChosenBufferTests.fs
new file mode 100644
index 00000000..fcdebf08
--- /dev/null
+++ b/tests/FSharp.Collections.Immutable.Tests/IndexedList/ChosenBufferTests.fs
@@ -0,0 +1,71 @@
+namespace FSharp.Collections.Immutable.Tests
+
+open System.Buffers
+open FSharp.Collections.Immutable
+open Microsoft.VisualStudio.TestTools.UnitTesting
+
+/// The buffer in which `choose` collects the elements: an array of its own below `Buffers.PooledFrom` elements, one rented
+/// from the pool above, and a list for a result that does not fit the rented array.
+[]
+type ChosenBufferTests () =
+
+ []
+ []
+ []
+ []
+ []
+ []
+ []
+ []
+ []
+ member _.``the buffer builds the list of the elements that were added`` (bound : int, count : int) =
+ let buffer = Buffers.ChosenBuffer bound
+
+ for value in 0 .. count - 1 do
+ buffer.Add value
+
+ CollectionAssert.AreEqual (
+ Array.init count id,
+ buffer.ToImmutable () |> IndexedList.toArray,
+ "The buffer must give the elements that were added, in order"
+ )
+
+ []
+ member _.``a result longer than the rented array moves into a list`` () =
+ let count = Buffers.PooledUpTo + 10
+ let buffer = Buffers.ChosenBuffer count
+
+ for value in 0 .. count - 1 do
+ buffer.Add value
+
+ let list = buffer.ToImmutable ()
+ Assert.HasCount (count, list, "The buffer must keep all elements of a result longer than the rented array")
+ Assert.AreEqual(
+ Buffers.PooledUpTo - 1,
+ list[Buffers.PooledUpTo - 1],
+ "The last element of the rented array must stay"
+ )
+ Assert.AreEqual(
+ Buffers.PooledUpTo,
+ list[Buffers.PooledUpTo],
+ "The first element after the rented array must follow it"
+ )
+ Assert.AreEqual(count - 1, list[count - 1], "The last element must stay")
+
+ []
+ member _.``the array that a buffer gives back holds no references`` (count : int) =
+ let buffer = Buffers.ChosenBuffer count
+
+ for _ in 1..count do
+ buffer.Add "x"
+
+ buffer.ToImmutable () |> ignore
+ let rented = ArrayPool.Shared.Rent count
+
+ try
+ let leftovers =
+ rented
+ |> Array.filter (fun item -> not (obj.ReferenceEquals (item, null)))
+ Assert.IsEmpty (leftovers, "The pool must not keep the elements that a buffer held")
+ finally
+ ArrayPool.Shared.Return rented
diff --git a/tests/FSharp.Collections.Immutable.Tests/IndexedList/IIndexedListTests.fs b/tests/FSharp.Collections.Immutable.Tests/IndexedList/IIndexedListTests.fs
index 464efd97..2a714632 100644
--- a/tests/FSharp.Collections.Immutable.Tests/IndexedList/IIndexedListTests.fs
+++ b/tests/FSharp.Collections.Immutable.Tests/IndexedList/IIndexedListTests.fs
@@ -674,6 +674,21 @@ type IIndexedListTests () =
Assert.AreEqual(-1, IIndexedList.indexOf [| 1; 2 |] arrays, "IIndexedList.indexOf must compare arrays by reference")
+ /// The contracts choose from short lists, which a new array holds; a long list goes through the rented array, and the
+ /// selectivities give the results of all lengths, down to a few elements.
+ []
+ member _.``choose of a long list keeps the chosen elements in order`` (percent : int) =
+ let items = Array.init 100_000 id
+ let list : IIndexedList = IndexedList.ofArray items
+
+ CollectionAssert.AreEqual (
+ items
+ |> Array.choose (fun x -> if x % 100 < percent then Some (x * 2) else None),
+ IIndexedList.choose (fun x -> if x % 100 < percent then ValueSome (x * 2) else ValueNone) list
+ |> IIndexedList.toArray,
+ $"IIndexedList.choose must keep the chosen elements of a long list in order (%d{percent} %% chosen)"
+ )
+
/// The sequence-like contract for `IIndexedList` over an implementation that is not an indexed list. The functions that
/// take a shortcut for indexed lists go the general way here, and must give the same results.
[]
diff --git a/tests/FSharp.Collections.Immutable.Tests/IndexedList/IndexedListTests.fs b/tests/FSharp.Collections.Immutable.Tests/IndexedList/IndexedListTests.fs
index 8d3645bf..dd070ade 100644
--- a/tests/FSharp.Collections.Immutable.Tests/IndexedList/IndexedListTests.fs
+++ b/tests/FSharp.Collections.Immutable.Tests/IndexedList/IndexedListTests.fs
@@ -909,3 +909,115 @@ type IndexedListTests () =
$"IndexedList.%s{name} must throw the type of exception of List"
)
Assert.AreEqual(expected.Message, ex.Message, $"IndexedList.%s{name} must throw the message of List")
+
+ /// An F# list is an `IReadOnlyList` whose indexer walks the list from its head, and the BCL builds a tree by indexing,
+ /// so `ImmutableList.CreateRange` of a list takes time that grows with the square of the length: about 6 s for 100 000
+ /// elements and four times as much for 200 000, where the copy that `ofList` makes takes a few milliseconds. The timeout is
+ /// far above the time of the copy and a fraction of the time of the quadratic build.
+ []
+ member _.``ofList and ofSeq copy a long list before they build the tree`` () =
+ let expected = Array.init 200_000 id
+ let list = List.ofArray expected
+
+ CollectionAssert.AreEqual (
+ expected,
+ IndexedList.toArray (IndexedList.ofList list),
+ "IndexedList.ofList must keep the elements of a long list in order"
+ )
+
+ CollectionAssert.AreEqual (
+ expected,
+ IndexedList.toArray (IndexedList.ofSeq list),
+ "IndexedList.ofSeq must keep the elements of a long list in order"
+ )
+
+ /// The functions that walk the list use the enumerator of the interface for reference types (see Walk), a path that
+ /// the contracts, over integers, do not reach.
+ []
+ member _.``the functions that walk a list give the results of List for strings`` (length : int) =
+ let items = Array.init length (fun index -> $"item %d{index}")
+ let list = IndexedList.ofArray items
+ let expected = List.ofArray items
+ let visited = ResizeArray()
+ IndexedList.iter visited.Add list
+ CollectionAssert.AreEqual (items, visited.ToArray (), "IndexedList.iter must visit the strings in order")
+
+ let visitedAt = ResizeArray()
+ IndexedList.iteri (fun index item -> visitedAt.Add $"%d{index}:%s{item}") list
+
+ CollectionAssert.AreEqual (
+ expected
+ |> List.mapi (fun index item -> $"%d{index}:%s{item}")
+ |> List.toArray,
+ visitedAt.ToArray (),
+ "IndexedList.iteri must pass the index and the string in order"
+ )
+
+ let fold (total : int) (item : string) = total * 31 + item.Length
+
+ Assert.AreEqual(
+ List.fold fold 7 expected,
+ IndexedList.fold fold 7 list,
+ "IndexedList.fold must thread the state through the strings in order"
+ )
+
+ CollectionAssert.AreEqual (
+ expected
+ |> List.mapi (fun index item -> $"%d{index}:%s{item}")
+ |> List.toArray,
+ IndexedList.mapi (fun index item -> $"%d{index}:%s{item}") list
+ |> IndexedList.toArray,
+ "IndexedList.mapi must map the strings in order"
+ )
+
+ CollectionAssert.AreEqual (
+ expected |> List.scan fold 7 |> List.toArray,
+ IndexedList.scan fold 7 list |> IndexedList.toArray,
+ "IndexedList.scan must give the states for the strings"
+ )
+
+ let pick (item : string) =
+ if item.EndsWith ('7') then
+ ValueSome item.Length
+ else
+ ValueNone
+
+ Assert.AreEqual(
+ expected
+ |> List.tryPick (fun item -> pick item |> ValueOption.toOption)
+ |> ValueOption.ofOption,
+ IndexedList.tryPick pick list,
+ "IndexedList.tryPick must return the first value for the strings"
+ )
+
+ CollectionAssert.AreEqual (
+ expected
+ |> List.choose (fun item -> pick item |> ValueOption.toOption)
+ |> List.toArray,
+ IndexedList.choose pick list |> IndexedList.toArray,
+ "IndexedList.choose must keep the chosen values for the strings"
+ )
+
+ CollectionAssert.AreEqual (
+ expected
+ |> List.collect (fun item -> [ item; item ])
+ |> List.toArray,
+ IndexedList.collect (fun item -> IndexedList.ofArray [| item; item |]) list
+ |> IndexedList.toArray,
+ "IndexedList.collect must concatenate the results for the strings"
+ )
+
+ /// The contracts choose from short lists, which a new array holds; a long list goes through the rented array, and the
+ /// selectivities give the results of all lengths, down to a few elements.
+ []
+ member _.``choose of a long list keeps the chosen elements in order`` (percent : int) =
+ let items = Array.init 100_000 id
+
+ CollectionAssert.AreEqual (
+ items
+ |> Array.choose (fun x -> if x % 100 < percent then Some (x * 2) else None),
+ IndexedList.ofArray items
+ |> IndexedList.choose (fun x -> if x % 100 < percent then ValueSome (x * 2) else ValueNone)
+ |> IndexedList.toArray,
+ $"IndexedList.choose must keep the chosen elements of a long list in order (%d{percent} %% chosen)"
+ )