diff --git a/src/FSharp.Collections.Immutable/Helper/Buffers.fs b/src/FSharp.Collections.Immutable/Helper/Buffers.fs
index bea7f019..5d0fd533 100644
--- a/src/FSharp.Collections.Immutable/Helper/Buffers.fs
+++ b/src/FSharp.Collections.Immutable/Helper/Buffers.fs
@@ -123,3 +123,57 @@ type ChosenBuffer<'T> (bound : int) =
this.Release ()
result
| spilled -> ImmutableList.Create<'T>(Span<'T>.op_Implicit(CollectionsMarshal.AsSpan spilled))
+
+///
+/// Builds the indexed list of the elements that a function writes into an array of a known length, and returns what the
+/// function returns as well.
+///
+/// The type of the elements.
+/// The type of what the function returns.
+/// The number of elements.
+///
+/// Writes the elements at the indexes 0 to - 1 of the array that it gets, which can be longer.
+///
+/// The indexed list, and the value that returned.
+///
+///
+/// The array is a new one below elements, and one rented
+/// from the pool above, which is cleared as far as it was written when it holds references and returned.
+///
+///
+/// A function that throws does not return the rented array. The pool allocates another one, and nothing leaks.
+///
+///
+let ofFilledWith (length : int) (fill : 'T array -> 'R) : struct (ImmutableList<'T> * 'R) =
+ if length < PooledFrom then
+ let items : 'T array =
+ if length = 0 then
+ Array.Empty<'T>()
+ else
+ Array.zeroCreate length
+ let extra = fill items
+ struct (ImmutableList.Create<'T>(ReadOnlySpan<'T>(items)), extra)
+ else
+ let pool = ArrayPool<'T>.Shared
+ let items = pool.Rent length
+ let extra = fill items
+ let result = ImmutableList.Create<'T>(ReadOnlySpan<'T>(items, 0, length))
+
+ if RuntimeHelpers.IsReferenceOrContainsReferences<'T>() then
+ Array.Clear (items, 0, length)
+
+ pool.Return (items, false)
+ struct (result, extra)
+
+///
+/// Builds the indexed list of the elements that a function writes into an array of a known length.
+///
+/// The type of the elements.
+/// The number of elements.
+///
+/// Writes the elements at the indexes 0 to - 1 of the array that it gets, which can be longer.
+///
+/// The indexed list.
+let ofFilled (length : int) (fill : 'T array -> unit) : ImmutableList<'T> =
+ let struct (result, ()) = ofFilledWith length fill
+ result
diff --git a/src/FSharp.Collections.Immutable/IndexedList/IIndexedListTuple.fs b/src/FSharp.Collections.Immutable/IndexedList/IIndexedListTuple.fs
index d05f05fa..2eea1473 100644
--- a/src/FSharp.Collections.Immutable/IndexedList/IIndexedListTuple.fs
+++ b/src/FSharp.Collections.Immutable/IndexedList/IIndexedListTuple.fs
@@ -1,7 +1,6 @@
[]
module FSharp.Collections.Immutable.Tuple.IIndexedList
-open System.Collections.Immutable
open FSharp.Collections.Immutable
// The functions that build a tuple per element share their algorithm with the default module and only pass another
@@ -29,31 +28,31 @@ let (|Cons|_|) (list : IIndexedList<'T>) = tryUncons list
[]
let allPairs (list1 : IIndexedList<'T1>) (list2 : IIndexedList<'T2>) =
Arrays.allPairs referencePair (Snapshot.ofSeq (nameof list1) list1) (Snapshot.ofSeq (nameof list2) list2)
- |> ImmutableList.CreateRange
+ |> IndexedList.ofArray
[]
let indexed (list : IIndexedList<'T>) =
Arrays.indexed referencePair (Snapshot.ofSeq (nameof list) list)
- |> ImmutableList.CreateRange
+ |> IndexedList.ofArray
[]
let pairwise (list : IIndexedList<'T>) =
Arrays.pairwise referencePair (Snapshot.ofSeq (nameof list) list)
- |> ImmutableList.CreateRange
+ |> IndexedList.ofArray
[]
let unzip (list : IIndexedList<'T1 * 'T2>) =
let struct (first, second) =
Arrays.unzip (fun (item1, item2) -> struct (item1, item2)) (Snapshot.ofSeq (nameof list) list)
- ImmutableList.CreateRange first, ImmutableList.CreateRange second
+ IndexedList.ofArray first, IndexedList.ofArray second
[]
let unzip3 (list : IIndexedList<'T1 * 'T2 * 'T3>) =
let struct (first, second, third) =
Arrays.unzip3 (fun (item1, item2, item3) -> struct (item1, item2, item3)) (Snapshot.ofSeq (nameof list) list)
- ImmutableList.CreateRange first, ImmutableList.CreateRange second, ImmutableList.CreateRange third
+ IndexedList.ofArray first, IndexedList.ofArray second, IndexedList.ofArray third
[]
let zip (list1 : IIndexedList<'T1>) (list2 : IIndexedList<'T2>) =
@@ -62,7 +61,7 @@ let zip (list1 : IIndexedList<'T1>) (list2 : IIndexedList<'T2>) =
ListChecks.sameLengths (nameof list1) list1.Count (nameof list2) list2.Count
Arrays.zip referencePair (Snapshot.ofSeq (nameof list1) list1) (Snapshot.ofSeq (nameof list2) list2)
- |> ImmutableList.CreateRange
+ |> IndexedList.ofArray
[]
let zip3 (list1 : IIndexedList<'T1>) (list2 : IIndexedList<'T2>) (list3 : IIndexedList<'T3>) =
@@ -76,7 +75,7 @@ let zip3 (list1 : IIndexedList<'T1>) (list2 : IIndexedList<'T2>) (list3 : IIndex
(Snapshot.ofSeq (nameof list1) list1)
(Snapshot.ofSeq (nameof list2) list2)
(Snapshot.ofSeq (nameof list3) list3)
- |> ImmutableList.CreateRange
+ |> IndexedList.ofArray
// ---- Folding ----
@@ -103,12 +102,12 @@ let mapFoldBack (mapping : 'T -> 'State -> 'Result * 'State) (list : IIndexedLis
[]
let countBy (projection : 'T -> 'Key) (list : IIndexedList<'T>) =
Arrays.countBy referencePair projection (Snapshot.ofSeq (nameof list) list)
- |> ImmutableList.CreateRange
+ |> IndexedList.ofArray
[]
let groupBy (projection : 'T -> 'Key) (list : IIndexedList<'T>) =
- Arrays.groupBy (fun key items -> key, ImmutableList.CreateRange items) projection (Snapshot.ofSeq (nameof list) list)
- |> ImmutableList.CreateRange
+ Arrays.groupBy (fun key items -> key, IndexedList.ofArray items) projection (Snapshot.ofSeq (nameof list) list)
+ |> IndexedList.ofArray
[]
let partition predicate (list : IIndexedList<'T>) =
diff --git a/src/FSharp.Collections.Immutable/IndexedList/IndexedList.fs b/src/FSharp.Collections.Immutable/IndexedList/IndexedList.fs
index 5c09ce2a..22f59aef 100644
--- a/src/FSharp.Collections.Immutable/IndexedList/IndexedList.fs
+++ b/src/FSharp.Collections.Immutable/IndexedList/IndexedList.fs
@@ -10,8 +10,10 @@ open System.Linq
// snapshot of the elements in an array, apply an Array function or a shared algorithm and build a new tree from the fresh
// result (ADR 0001, D5). The checks of ListChecks throw what the List functions throw (D6).
-/// An indexed list of the elements of a fresh array that nobody changes any more; the tree copies the elements.
-let inline private ofFreshArray (array : 'T array) = ImmutableList.CreateRange array
+/// An indexed list of the elements of a fresh array that nobody changes any more; the tree copies the elements. The tree is
+/// built from a span of the array: CreateRange reads the array through IReadOnlyList and checks the range at every node,
+/// and the build from a span took 12 to 22 % less time for arrays of 100 to a million strings, and the same time for integers.
+let inline private ofFreshArray (array : 'T array) = ImmutableList.Create<'T>(ReadOnlySpan<'T>(array))
/// A range of an indexed list: the empty list or the list itself without a new tree, otherwise a new tree. The range is
/// copied into an array and the tree is built from the array: GetRange reads the elements of the range one by one from
@@ -23,9 +25,7 @@ let private range (list : IndexedList<'T>) (start : int) (count : int) =
elif count = list.Count then
list
else
- let items = Array.zeroCreate count
- list.CopyTo (start, items, 0, count)
- ofFreshArray items
+ Buffers.ofFilled count (fun items -> list.CopyTo (start, items, 0, count))
/// The first value that the chooser returns for the elements, in their order.
let private firstChoice (chooser : 'T -> 'U voption) (list : IndexedList<'T>) = Walk.tryPick chooser list
@@ -57,12 +57,17 @@ let empty<'T> : IndexedList<'T> = ImmutableList<'T>.Empty
let init length (initializer : int -> 'T) =
// List.init reports the length under the name "count"
ListChecks.nonNegative "count" length
- Array.init length initializer |> ofFreshArray
+ Buffers.ofFilled
+ length
+ (fun items ->
+ for index in 0 .. length - 1 do
+ items[index] <- initializer index
+ )
[]
let ofArray (array : 'T array) =
checkNotNull (nameof array) array
- ImmutableList.CreateRange array
+ ofFreshArray array
[]
let ofList (list : 'T list) =
@@ -90,7 +95,9 @@ let ofSpan (span : Span<'T>) = ImmutableList.Create<'T>(Span<'T>.op_Implicit spa
[]
let replicate count (initial : 'T) =
ListChecks.replicate (nameof count) count
- Array.create count initial |> ofFreshArray
+ // Array.Fill is vectorized, where Array.create stores the elements one by one: 0.8 to 0.9 times as long for 100 to a
+ // million strings, and the same time for integers
+ Buffers.ofFilled count (fun items -> Array.Fill (items, initial, 0, count))
[]
let singleton (value : 'T) = ImmutableList.Create<'T> value
@@ -618,9 +625,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
- Walk.iteri (fun index item -> results[index] <- mapping index item) list
- ofFreshArray results
+ Buffers.ofFilled list.Count (fun results -> Walk.iteri (fun index item -> results[index] <- mapping index item) list)
[]
let mapi2 (mapping : int -> 'T1 -> 'T2 -> 'U) (list1 : IndexedList<'T1>) (list2 : IndexedList<'T2>) =
@@ -698,9 +703,15 @@ let rev (list : IndexedList<'T>) =
// A copy reversed in place and a new balanced tree are about 8 times faster than ImmutableList.Reverse at 100 000
// integers (about 3 ms against 23 ms), and the copy is the only extra allocation (ADR 0001, D5); the copy is fresh,
// so nobody else sees it change
- let array = Snapshot.ofSeq (nameof list) list
- Array.Reverse array
- ofFreshArray array
+ checkNotNull (nameof list) list
+ let count = list.Count
+
+ Buffers.ofFilled
+ count
+ (fun items ->
+ list.CopyTo items
+ Array.Reverse (items, 0, count)
+ )
[]
let revRange index count (list : IndexedList<'T>) =
@@ -902,21 +913,23 @@ 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
-
- for item in list do
- let struct (result, next) = mapping state item
- results[index] <- result
- state <- next
- index <- index + 1
-
- struct (ofFreshArray results, state)
+ Buffers.ofFilledWith
+ list.Count
+ (fun results ->
+ let mutable state = state
+ let mutable index = 0
+
+ for item in list do
+ let struct (result, next) = mapping state item
+ results[index] <- result
+ state <- next
+ index <- index + 1
+
+ state
+ )
[]
let mapFoldBack (mapping : 'T -> 'State -> struct ('Result * 'State)) (list : IndexedList<'T>) (state : 'State) =
@@ -1043,10 +1056,12 @@ let reduceBack (reduction : 'T -> 'T -> 'T) (list : IndexedList<'T>) =
[]
let scan (folder : 'State -> 'T -> 'State) (state : 'State) (list : IndexedList<'T>) =
checkNotNull (nameof list) list
- let states = Array.zeroCreate (list.Count + 1)
- states[0] <- state
- Walk.iteri (fun index item -> states[index + 1] <- folder states[index] item) list
- ofFreshArray states
+ Buffers.ofFilled
+ (list.Count + 1)
+ (fun states ->
+ states[0] <- state
+ Walk.iteri (fun index item -> states[index + 1] <- folder states[index] item) list
+ )
[]
let scanBack (folder : 'T -> 'State -> 'State) (list : IndexedList<'T>) (state : 'State) =
diff --git a/src/FSharp.Collections.Immutable/IndexedList/IndexedListTuple.fs b/src/FSharp.Collections.Immutable/IndexedList/IndexedListTuple.fs
index 0a1e79ae..1a402377 100644
--- a/src/FSharp.Collections.Immutable/IndexedList/IndexedListTuple.fs
+++ b/src/FSharp.Collections.Immutable/IndexedList/IndexedListTuple.fs
@@ -1,7 +1,6 @@
[]
module FSharp.Collections.Immutable.Tuple.IndexedList
-open System.Collections.Immutable
open FSharp.Collections.Immutable
// The functions that build a tuple per element share their algorithm with the default module and only pass another
@@ -40,31 +39,31 @@ let (|Cons|_|) (list : IndexedList<'T>) = tryUncons list
[]
let allPairs (list1 : IndexedList<'T1>) (list2 : IndexedList<'T2>) =
Arrays.allPairs referencePair (Snapshot.ofSeq (nameof list1) list1) (Snapshot.ofSeq (nameof list2) list2)
- |> ImmutableList.CreateRange
+ |> IndexedList.ofArray
[]
let indexed (list : IndexedList<'T>) =
Arrays.indexed referencePair (Snapshot.ofSeq (nameof list) list)
- |> ImmutableList.CreateRange
+ |> IndexedList.ofArray
[]
let pairwise (list : IndexedList<'T>) =
Arrays.pairwise referencePair (Snapshot.ofSeq (nameof list) list)
- |> ImmutableList.CreateRange
+ |> IndexedList.ofArray
[]
let unzip (list : IndexedList<'T1 * 'T2>) =
let struct (first, second) =
Arrays.unzip (fun (item1, item2) -> struct (item1, item2)) (Snapshot.ofSeq (nameof list) list)
- ImmutableList.CreateRange first, ImmutableList.CreateRange second
+ IndexedList.ofArray first, IndexedList.ofArray second
[]
let unzip3 (list : IndexedList<'T1 * 'T2 * 'T3>) =
let struct (first, second, third) =
Arrays.unzip3 (fun (item1, item2, item3) -> struct (item1, item2, item3)) (Snapshot.ofSeq (nameof list) list)
- ImmutableList.CreateRange first, ImmutableList.CreateRange second, ImmutableList.CreateRange third
+ IndexedList.ofArray first, IndexedList.ofArray second, IndexedList.ofArray third
[]
let zip (list1 : IndexedList<'T1>) (list2 : IndexedList<'T2>) =
@@ -73,7 +72,7 @@ let zip (list1 : IndexedList<'T1>) (list2 : IndexedList<'T2>) =
ListChecks.sameLengths (nameof list1) list1.Count (nameof list2) list2.Count
Arrays.zip referencePair (Snapshot.ofSeq (nameof list1) list1) (Snapshot.ofSeq (nameof list2) list2)
- |> ImmutableList.CreateRange
+ |> IndexedList.ofArray
[]
let zip3 (list1 : IndexedList<'T1>) (list2 : IndexedList<'T2>) (list3 : IndexedList<'T3>) =
@@ -87,7 +86,7 @@ let zip3 (list1 : IndexedList<'T1>) (list2 : IndexedList<'T2>) (list3 : IndexedL
(Snapshot.ofSeq (nameof list1) list1)
(Snapshot.ofSeq (nameof list2) list2)
(Snapshot.ofSeq (nameof list3) list3)
- |> ImmutableList.CreateRange
+ |> IndexedList.ofArray
// ---- Folding ----
@@ -114,12 +113,12 @@ let mapFoldBack (mapping : 'T -> 'State -> 'Result * 'State) (list : IndexedList
[]
let countBy (projection : 'T -> 'Key) (list : IndexedList<'T>) =
Arrays.countBy referencePair projection (Snapshot.ofSeq (nameof list) list)
- |> ImmutableList.CreateRange
+ |> IndexedList.ofArray
[]
let groupBy (projection : 'T -> 'Key) (list : IndexedList<'T>) =
- Arrays.groupBy (fun key items -> key, ImmutableList.CreateRange items) projection (Snapshot.ofSeq (nameof list) list)
- |> ImmutableList.CreateRange
+ Arrays.groupBy (fun key items -> key, IndexedList.ofArray items) projection (Snapshot.ofSeq (nameof list) list)
+ |> IndexedList.ofArray
[]
let partition predicate (list : IndexedList<'T>) =
diff --git a/tests/FSharp.Collections.Immutable.Tests/IndexedList/IndexedListTests.fs b/tests/FSharp.Collections.Immutable.Tests/IndexedList/IndexedListTests.fs
index dd070ade..8405f0ce 100644
--- a/tests/FSharp.Collections.Immutable.Tests/IndexedList/IndexedListTests.fs
+++ b/tests/FSharp.Collections.Immutable.Tests/IndexedList/IndexedListTests.fs
@@ -1021,3 +1021,96 @@ type IndexedListTests () =
|> IndexedList.toArray,
$"IndexedList.choose must keep the chosen elements of a long list in order (%d{percent} %% chosen)"
)
+
+ /// The tree of a long array is built from a span: every index must find its element, which a tree that is not balanced
+ /// as the indexer expects would not.
+ []
+ member _.``ofArray of a long array keeps the order and the indices`` () =
+ let items = Array.init 100_000 (fun index -> index * 7)
+ let list = IndexedList.ofArray items
+
+ Assert.HasCount (items.Length, list, "IndexedList.ofArray must keep all elements of a long array")
+ CollectionAssert.AreEqual (items, IndexedList.toArray list, "IndexedList.ofArray must keep the order of a long array")
+
+ let wrong = [|
+ for index in 0 .. items.Length - 1 do
+ if list[index] <> items[index] then
+ index
+ |]
+ Assert.IsEmpty (wrong, "Every index of IndexedList.ofArray must give the element at that index")
+
+ []
+ member _.``replicate repeats the value for strings and integers`` (count : int) =
+ CollectionAssert.AreEqual (
+ List.replicate count "x" |> List.toArray,
+ IndexedList.replicate count "x" |> IndexedList.toArray,
+ "IndexedList.replicate must repeat the string"
+ )
+
+ CollectionAssert.AreEqual (
+ List.replicate count 7 |> List.toArray,
+ IndexedList.replicate count 7 |> IndexedList.toArray,
+ "IndexedList.replicate must repeat the integer"
+ )
+
+ /// The temporary array of these functions is a new array below `Buffers.PooledFrom` elements and a rented one above, so
+ /// the lengths around it take both ways. The strings show that a rented array is cleared as far as it was written.
+ []
+ []
+ []
+ []
+ []
+ []
+ []
+ member _.``the functions that fill an array give the results of List around the length of the rented array`` (length : int) =
+ let items = Array.init length (fun index -> $"item %d{index}")
+ let list = IndexedList.ofArray items
+ let expected = List.ofArray items
+ let tag (index : int) (item : string) = $"%d{index}:%s{item}"
+ let step (total : int) (item : string) = total * 31 + item.Length
+
+ CollectionAssert.AreEqual (
+ List.init length (fun index -> $"%d{index}!")
+ |> List.toArray,
+ IndexedList.init length (fun index -> $"%d{index}!")
+ |> IndexedList.toArray,
+ "IndexedList.init must give the strings in order"
+ )
+
+ CollectionAssert.AreEqual (
+ List.rev expected |> List.toArray,
+ IndexedList.rev list |> IndexedList.toArray,
+ "IndexedList.rev must reverse the strings"
+ )
+
+ CollectionAssert.AreEqual (
+ List.mapi tag expected |> List.toArray,
+ IndexedList.mapi tag list |> IndexedList.toArray,
+ "IndexedList.mapi must map the strings in order"
+ )
+
+ CollectionAssert.AreEqual (
+ List.scan step 7 expected |> List.toArray,
+ IndexedList.scan step 7 list |> IndexedList.toArray,
+ "IndexedList.scan must give the states"
+ )
+
+ let struct (mapped, final) =
+ IndexedList.mapFold (fun total item -> struct (tag total item, step total item)) 7 list
+ let expectedMapped, expectedFinal =
+ List.mapFold (fun total item -> tag total item, step total item) 7 expected
+
+ CollectionAssert.AreEqual (
+ expectedMapped |> List.toArray,
+ IndexedList.toArray mapped,
+ "IndexedList.mapFold must map the strings with the state"
+ )
+
+ Assert.AreEqual(expectedFinal, final, "IndexedList.mapFold must return the last state")
+
+ if length >= 2 then
+ CollectionAssert.AreEqual (
+ Array.sub items 1 (length - 2),
+ IndexedList.sub list 1 (length - 2) |> IndexedList.toArray,
+ "IndexedList.sub must give the strings of the range"
+ )