Skip to content

Commit 6ebbce8

Browse files
xperiandriclaude
andcommitted
fixup! feat: FlatList module with the vocabulary of the FSharp.Core Array module
Review of #26: - `ofSeq` rejects a default flat list passed as a sequence with `ArgumentNullException`, as every function rejects a default flat list (D6); a valid flat list is still returned without a copy. - `collect` rejects a default flat list that the mapping returns with `ArgumentNullException`, as `concat` does. - `countBy` keeps one counter per key instead of every element: a dictionary over keys wrapped in a struct, which admits `null` keys, with F# structural equality and the order of first occurrence. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 4b2ba96 commit 6ebbce8

4 files changed

Lines changed: 66 additions & 13 deletions

File tree

‎src/FSharp.Collections.Immutable/FlatList/FlatList.fs‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,10 @@ module FlatList =
3636
[<CompiledName "OfSeq">]
3737
let ofSeq (source : seq<'T>) =
3838
match source with
39-
// An immutable array needs no copy
40-
| :? ImmutableArray<'T> as array when not array.IsDefault -> array
39+
// An immutable array needs no copy; a default one is a default FlatList, which no function accepts (ADR 0001, D6)
40+
| :? ImmutableArray<'T> as array ->
41+
checkNotDefault (nameof source) array
42+
array
4143
| _ -> Array.ofSeq source |> wrapArray
4244

4345
[<CompiledName "OfSpan">]
@@ -351,7 +353,10 @@ module FlatList =
351353
let builder = ImmutableArray.CreateBuilder<'U>()
352354

353355
for item in source do
354-
builder.AddRange (mapping item)
356+
let items = mapping item
357+
// Array.collect dereferences a null result; a default result is a default FlatList, rejected as in concat
358+
checkNotDefault (nameof mapping) items
359+
builder.AddRange items
355360

356361
builder.DrainToImmutable ()
357362

‎src/FSharp.Collections.Immutable/FlatList/FlatList.fsi‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,8 @@ module FlatList =
8484
/// <summary>Creates a flat list from a sequence.</summary>
8585
/// <param name="source">The input sequence.</param>
8686
/// <returns>The flat list of the elements of the sequence.</returns>
87-
/// <exception cref="T:System.ArgumentNullException">Thrown when <paramref name="source"/> is <see langword="null"/>.</exception>
88-
/// <remarks>O(n).</remarks>
87+
/// <exception cref="T:System.ArgumentNullException">Thrown when <paramref name="source"/> is <see langword="null"/> or a default flat list.</exception>
88+
/// <remarks>O(n). A flat list is returned as it is, without a copy.</remarks>
8989
[<CompiledName "OfSeq">]
9090
val ofSeq : source : seq<'T> -> FlatList<'T>
9191

@@ -502,6 +502,7 @@ module FlatList =
502502
/// <param name="mapping">The function that returns a flat list for each element.</param>
503503
/// <param name="array">The input flat list.</param>
504504
/// <returns>The concatenation of the returned flat lists.</returns>
505+
/// <exception cref="T:System.ArgumentNullException">Thrown when the mapping returns a default flat list.</exception>
505506
/// <remarks>O(n + the total length of the returned flat lists).</remarks>
506507
[<CompiledName "Collect">]
507508
val collect : mapping : ('T -> FlatList<'U>) -> array : FlatList<'T> -> FlatList<'U>

‎src/FSharp.Collections.Immutable/Helper.fs‎

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -229,20 +229,46 @@ module internal Arrays =
229229

230230
result
231231

232-
/// The keys of the projection in the order of their first occurrence, each with the number of its elements.
232+
/// A key that may be null, for a dictionary, which rejects null keys.
233+
[<Struct>]
234+
type NullableKey<'Key> = { Key : 'Key }
235+
236+
/// Compares wrapped keys in F# structural equality, which handles null keys, as FSharp.Core does for its keys.
237+
let nullableKeyComparer<'Key when 'Key : equality> () =
238+
let structural = HashIdentity.Structural<'Key>
239+
240+
{
241+
new IEqualityComparer<NullableKey<'Key>> with
242+
member _.Equals (left, right) = structural.Equals (left.Key, right.Key)
243+
member _.GetHashCode key = structural.GetHashCode key.Key
244+
}
245+
246+
/// <summary>The keys of the projection in the order of their first occurrence, each with the number of its elements.</summary>
247+
/// <remarks>The counting keeps one counter per key, not the elements, so it needs memory for the keys only.</remarks>
233248
let inline countBy
234249
([<InlineIfLambda>] tuple : 'Key -> int -> 'R)
235250
(projection : 'T -> 'Key)
236251
(array : 'T array)
237252
: 'R array when 'Key : equality =
238-
// ToLookup keeps the order of the first occurrence of each key and accepts null keys, as FSharp.Core does
239-
let lookup = array.ToLookup ((fun item -> projection item), HashIdentity.Structural<'Key>)
240-
let result = Array.zeroCreate lookup.Count
241-
let mutable index = 0
253+
let indices = Dictionary<NullableKey<'Key>, int>(nullableKeyComparer<'Key>())
254+
let keys = ResizeArray<'Key>()
255+
let counts = ResizeArray<int>()
242256

243-
for grouping in lookup do
244-
result[index] <- tuple grouping.Key (grouping.Count ())
245-
index <- index + 1
257+
for item in array do
258+
let key = projection item
259+
let mutable index = 0
260+
261+
if indices.TryGetValue ({ Key = key }, &index) then
262+
counts[index] <- counts[index] + 1
263+
else
264+
indices.Add ({ Key = key }, keys.Count)
265+
keys.Add key
266+
counts.Add 1
267+
268+
let result = Array.zeroCreate keys.Count
269+
270+
for index = 0 to result.Length - 1 do
271+
result[index] <- tuple keys[index] counts[index]
246272

247273
result
248274

‎tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListTests.fs‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,15 @@ type FlatListTests () =
192192
// Array.concat dereferences a null element array without a check; a default element is a default flat list (D6)
193193
struct (nameof FlatList.concat, "arrays", (fun () -> FlatList.concat [ defaultArray ] |> ignore))
194194
struct (nameof FlatList.transpose, "arrays", (fun () -> FlatList.transpose [ defaultArray ] |> ignore))
195+
// Array.collect dereferences a null result of the mapping; a default result is rejected as in concat
196+
struct (nameof FlatList.collect,
197+
"mapping",
198+
(fun () ->
199+
FlatList.collect (fun _ -> defaultArray) (FlatList.singleton 1)
200+
|> ignore
201+
))
202+
// A default flat list passed as a sequence is still a default flat list
203+
struct (nameof FlatList.ofSeq, "source", (fun () -> FlatList.ofSeq (defaultArray :> seq<int>) |> ignore))
195204
struct (nameof FlatList.indexOf, "array", (fun () -> FlatList.indexOf 1 defaultArray |> ignore))
196205
struct (nameof FlatList.indexOfWith,
197206
"array",
@@ -230,6 +239,18 @@ type FlatListTests () =
230239
$"FlatList.%s{name} must name the parameter of the default flat list"
231240
)
232241

242+
[<TestMethod; TestCategory(nameof FlatList.countBy)>]
243+
member _.``countBy counts null keys like any other key`` () =
244+
let array = FlatList.ofList Inputs.several
245+
let keyOf (item : int) = if item > 0 then Some (item % 2) else None
246+
247+
Assert.AreEqual<struct (int option * int) list>(
248+
List.countBy keyOf Inputs.several
249+
|> List.map (fun (key, count) -> struct (key, count)),
250+
FlatList.countBy keyOf array |> FlatList.toList,
251+
"FlatList.countBy must count the key None, which is null, as List.countBy does"
252+
)
253+
233254
[<TestMethod; TestCategory(nameof FlatList.asSpan)>]
234255
member _.``asSpan views the elements`` () =
235256
let array = FlatList.ofList Inputs.several

0 commit comments

Comments
 (0)