Skip to content

Commit b9b63a5

Browse files
committed
Cached List.GetHashCode
1 parent 601026c commit b9b63a5

File tree

3 files changed

+74
-56
lines changed

3 files changed

+74
-56
lines changed

src/Fable.Transforms/Replacements.fs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -717,6 +717,8 @@ let identityHash r (arg: Expr) =
717717
Helper.CoreCall("Util", "structuralHash", Number Int32, [arg], ?loc=r)
718718
| DeclaredType(ent,_) when ent.IsFSharpUnion || ent.IsFSharpRecord || ent.IsValueType ->
719719
Helper.CoreCall("Util", "structuralHash", Number Int32, [arg], ?loc=r)
720+
| DeclaredType(ent,_) ->
721+
Helper.InstanceCall(arg, "GetHashCode", Number Int32, [], ?loc=r)
720722
| _ ->
721723
Helper.CoreCall("Util", "identityHash", Number Int32, [arg], ?loc=r)
722724

src/fable-library/List.fs

Lines changed: 68 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -8,58 +8,72 @@ open Fable.Core
88
let msgListWasEmpty = "List was empty"
99
let msgListNoMatch = "List did not contain any matching elements"
1010

11-
[<CustomEquality; CustomComparison>]
12-
type List<'T> when 'T : comparison =
13-
{ Count: int; Values: ResizeArray<'T> }
11+
type List<'T when 'T: comparison>(Count: int, Values: ResizeArray<'T>) =
12+
let mutable hashCode = None
1413

15-
static member Empty =
16-
{ Count = 0; Values = ResizeArray<'T>() }
17-
static member Cons (x: 'T, xs: 'T list) =
14+
static member Empty = new List<'T>(0, ResizeArray<'T>())
15+
static member Cons (x: 'T, xs: 'T list) = xs.Add(x)
16+
17+
member _.Add(x: 'T) =
1818
let values =
19-
if xs.Count = xs.Values.Count
20-
then xs.Values
21-
else xs.Values.GetRange(0, xs.Count)
19+
if Count = Values.Count
20+
then Values
21+
else Values.GetRange(0, Count)
2222
values.Add(x)
23-
{ Count = values.Count; Values = values }
23+
new List<'T>(values.Count, values)
24+
25+
member _.IsEmpty = Count <= 0
26+
member _.Length = Count
27+
28+
member _.Head =
29+
if Count > 0
30+
then Values.[Count - 1]
31+
else failwith msgListWasEmpty
32+
33+
member _.Tail =
34+
if Count > 0
35+
then new List<'T>(Count - 1, Values)
36+
else failwith msgListWasEmpty
37+
38+
member _.Item with get(index) =
39+
Values.[Count - 1 - index]
2440

2541
override xs.ToString() =
2642
"[" + System.String.Join("; ", xs) + "]"
2743

28-
override xs.GetHashCode() =
29-
let inline combineHash x y = (x <<< 1) + y + 631
30-
let mutable res = 0
31-
for i = xs.Count - 1 downto 0 do
32-
res <- combineHash res (hash xs.Values.[i])
33-
res
44+
override xs.Equals(other: obj) =
45+
let ys = other :?> 'T list
46+
if xs.Length <> ys.Length then false
47+
elif xs.GetHashCode() <> ys.GetHashCode() then false
48+
else Seq.forall2 (Unchecked.equals) xs ys
49+
// (xs :> System.IComparable).CompareTo(other) = 0
3450

35-
override xs.Equals(that: obj) =
36-
(xs :> System.IComparable).CompareTo(that) = 0
51+
override xs.GetHashCode() =
52+
match hashCode with
53+
| Some h -> h
54+
| None ->
55+
let inline combineHash i x y = (x <<< 1) + y + 631 * i
56+
let len = min (xs.Length - 1) 18 // limit the hash count
57+
let mutable h = 0
58+
for i = 0 to len do
59+
h <- combineHash i h (hash xs.[i])
60+
hashCode <- Some h
61+
h
3762

3863
interface System.IComparable with
39-
member xs.CompareTo(that: obj) =
40-
List.CompareWith compare xs (that :?> 'T list)
64+
member xs.CompareTo(other: obj) =
65+
List.CompareWith compare xs (other :?> 'T list)
4166

4267
interface System.Collections.Generic.IEnumerable<'T> with
4368
member xs.GetEnumerator(): System.Collections.Generic.IEnumerator<'T> =
44-
let elems = seq { for i = xs.Count - 1 downto 0 do yield xs.Values.[i] }
69+
let len = xs.Length - 1
70+
let elems = seq { for i = 0 to len do yield xs.[i] }
4571
elems.GetEnumerator()
72+
4673
interface System.Collections.IEnumerable with
4774
member this.GetEnumerator(): System.Collections.IEnumerator =
4875
((this :> System.Collections.Generic.IEnumerable<'T>).GetEnumerator() :> System.Collections.IEnumerator)
4976

50-
member xs.Length = xs.Count
51-
member xs.Head =
52-
if xs.Count > 0
53-
then xs.Values.[xs.Count - 1]
54-
else failwith msgListWasEmpty
55-
member xs.Tail =
56-
if xs.Count > 0
57-
then { Count = xs.Count - 1; Values = xs.Values }
58-
else failwith msgListWasEmpty
59-
member xs.IsEmpty = xs.Count <= 0
60-
member xs.Item with get(index) =
61-
xs.Values.[xs.Count - 1 - index]
62-
6377
static member CompareWith (comparer: 'T -> 'T -> int) (xs: 'T list) (ys: 'T list): int =
6478
if obj.ReferenceEquals(xs, ys)
6579
then 0
@@ -80,9 +94,7 @@ type List<'T> when 'T : comparison =
8094

8195
and 'T list when 'T: comparison = List<'T>
8296

83-
open System.Collections.Generic
84-
85-
let newList (values: ResizeArray<'T>) = { Count = values.Count; Values = values }
97+
let newList (values: ResizeArray<'T>) = new List<'T>(values.Count, values)
8698

8799
let empty () = List.Empty
88100

@@ -213,7 +225,7 @@ let iterateIndexed f xs =
213225
let iterateIndexed2 f xs ys =
214226
fold2 (fun i x y -> f i x y; i + 1) 0 xs ys |> ignore
215227

216-
let ofArray (xs: IList<'T>) =
228+
let ofArray (xs: System.Collections.Generic.IList<'T>) =
217229
let mutable res = List.Empty
218230
for i = xs.Count - 1 downto 0 do
219231
res <- cons xs.[i] res
@@ -315,13 +327,13 @@ let choose f xs =
315327
| Some y -> cons y acc
316328
| None -> acc) List.Empty xs |> reverse
317329

318-
let contains (value: 'T) (xs: 'T list) ([<Inject>] eq: IEqualityComparer<'T>) =
330+
let contains (value: 'T) (xs: 'T list) ([<Inject>] eq: System.Collections.Generic.IEqualityComparer<'T>) =
319331
tryFindIndex (fun v -> eq.Equals (value, v)) xs |> Option.isSome
320332

321-
let except (itemsToExclude: seq<'t>) (xs: 't list) ([<Inject>] eq: IEqualityComparer<'t>): 't list =
333+
let except (itemsToExclude: seq<'t>) (xs: 't list) ([<Inject>] eq: System.Collections.Generic.IEqualityComparer<'t>): 't list =
322334
if isEmpty xs then xs
323335
else
324-
let cached = HashSet(itemsToExclude, eq)
336+
let cached = System.Collections.Generic.HashSet(itemsToExclude, eq)
325337
xs |> filter cached.Add
326338

327339
let initialize n f =
@@ -373,16 +385,16 @@ let sortWith (comparison: 'T -> 'T -> int) (xs: 'T list): 'T list =
373385
values.Sort(System.Comparison<_>(comparison))
374386
values |> ofSeq
375387

376-
let sort (xs: 'T list) ([<Inject>] comparer: IComparer<'T>): 'T list =
388+
let sort (xs: 'T list) ([<Inject>] comparer: System.Collections.Generic.IComparer<'T>): 'T list =
377389
sortWith (fun x y -> comparer.Compare(x, y)) xs
378390

379-
let sortBy (projection: 'a -> 'b) (xs: 'a list) ([<Inject>] comparer: IComparer<'b>): 'a list =
391+
let sortBy (projection: 'a -> 'b) (xs: 'a list) ([<Inject>] comparer: System.Collections.Generic.IComparer<'b>): 'a list =
380392
sortWith (fun x y -> comparer.Compare(projection x, projection y)) xs
381393

382-
let sortDescending (xs: 'T list) ([<Inject>] comparer: IComparer<'T>): 'T list =
394+
let sortDescending (xs: 'T list) ([<Inject>] comparer: System.Collections.Generic.IComparer<'T>): 'T list =
383395
sortWith (fun x y -> comparer.Compare(x, y) * -1) xs
384396

385-
let sortByDescending (projection: 'a -> 'b) (xs: 'a list) ([<Inject>] comparer: IComparer<'b>): 'a list =
397+
let sortByDescending (projection: 'a -> 'b) (xs: 'a list) ([<Inject>] comparer: System.Collections.Generic.IComparer<'b>): 'a list =
386398
sortWith (fun x y -> comparer.Compare(projection x, projection y) * -1) xs
387399

388400
let sum (xs: 'T list) ([<Inject>] adder: IGenericAdder<'T>): 'T =
@@ -391,16 +403,16 @@ let sum (xs: 'T list) ([<Inject>] adder: IGenericAdder<'T>): 'T =
391403
let sumBy (f: 'T -> 'T2) (xs: 'T list) ([<Inject>] adder: IGenericAdder<'T2>): 'T2 =
392404
fold (fun acc x -> adder.Add(acc, f x)) (adder.GetZero()) xs
393405

394-
let maxBy (projection: 'a -> 'b) (xs: 'a list) ([<Inject>] comparer: IComparer<'b>): 'a =
406+
let maxBy (projection: 'a -> 'b) (xs: 'a list) ([<Inject>] comparer: System.Collections.Generic.IComparer<'b>): 'a =
395407
reduce (fun x y -> if comparer.Compare(projection y, projection x) > 0 then y else x) xs
396408

397-
let max (li:'a list) ([<Inject>] comparer: IComparer<'a>): 'a =
409+
let max (li:'a list) ([<Inject>] comparer: System.Collections.Generic.IComparer<'a>): 'a =
398410
reduce (fun x y -> if comparer.Compare(y, x) > 0 then y else x) li
399411

400-
let minBy (projection: 'a -> 'b) (xs: 'a list) ([<Inject>] comparer: IComparer<'b>): 'a =
412+
let minBy (projection: 'a -> 'b) (xs: 'a list) ([<Inject>] comparer: System.Collections.Generic.IComparer<'b>): 'a =
401413
reduce (fun x y -> if comparer.Compare(projection y, projection x) > 0 then x else y) xs
402414

403-
let min (xs: 'a list) ([<Inject>] comparer: IComparer<'a>): 'a =
415+
let min (xs: 'a list) ([<Inject>] comparer: System.Collections.Generic.IComparer<'a>): 'a =
404416
reduce (fun x y -> if comparer.Compare(y, x) > 0 then x else y) xs
405417

406418
let average (xs: 'T list) ([<Inject>] averager: IGenericAverager<'T>): 'T =
@@ -452,11 +464,11 @@ let splitAt i (xs: 'T list) =
452464
if i > xs.Length then invalidArg "index" "The input sequence has an insufficient number of elements."
453465
take i xs, skip i xs
454466

455-
let distinctBy (projection: 'T -> 'Key) (xs: 'T list) ([<Inject>] eq: IEqualityComparer<'Key>) =
456-
let hashSet = HashSet<'Key>(eq)
467+
let distinctBy (projection: 'T -> 'Key) (xs: 'T list) ([<Inject>] eq: System.Collections.Generic.IEqualityComparer<'Key>) =
468+
let hashSet = System.Collections.Generic.HashSet<'Key>(eq)
457469
xs |> filter (projection >> hashSet.Add)
458470

459-
let distinct (xs: 'T list) ([<Inject>] eq: IEqualityComparer<'T>) =
471+
let distinct (xs: 'T list) ([<Inject>] eq: System.Collections.Generic.IEqualityComparer<'T>) =
460472
distinctBy id xs eq
461473

462474
let exactlyOne (xs: 'T list) =
@@ -465,8 +477,8 @@ let exactlyOne (xs: 'T list) =
465477
| 0 -> invalidArg "list" LanguagePrimitives.ErrorStrings.InputSequenceEmptyString
466478
| _ -> invalidArg "list" "Input list too long"
467479

468-
let groupBy (projection: 'T -> 'Key) (xs: 'T list)([<Inject>] eq: IEqualityComparer<'Key>): ('Key * 'T list) list =
469-
let dict = Dictionary<'Key, 'T list>(eq)
480+
let groupBy (projection: 'T -> 'Key) (xs: 'T list)([<Inject>] eq: System.Collections.Generic.IEqualityComparer<'Key>): ('Key * 'T list) list =
481+
let dict = System.Collections.Generic.Dictionary<'Key, 'T list>(eq)
470482
let mutable keys = List.Empty
471483
xs |> iterate (fun v ->
472484
let key = projection v
@@ -480,8 +492,8 @@ let groupBy (projection: 'T -> 'Key) (xs: 'T list)([<Inject>] eq: IEqualityCompa
480492
keys |> iterate (fun key -> result <- cons (key, reverse dict.[key]) result)
481493
result
482494

483-
let countBy (projection: 'T -> 'Key) (xs: 'T list)([<Inject>] eq: IEqualityComparer<'Key>) =
484-
let dict = Dictionary<'Key, int>(eq)
495+
let countBy (projection: 'T -> 'Key) (xs: 'T list)([<Inject>] eq: System.Collections.Generic.IEqualityComparer<'Key>) =
496+
let dict = System.Collections.Generic.Dictionary<'Key, int>(eq)
485497
let mutable keys = List.Empty
486498
xs |> iterate (fun v ->
487499
let key = projection v

tests/Main/ComparisonTests.fs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,10 @@ let tests =
434434
(OTest(1).GetHashCode(), OTest(1).GetHashCode()) ||> notEqual
435435
(OTest(2).GetHashCode(), OTest(1).GetHashCode()) ||> notEqual
436436

437+
testCase "GetHashCode with objects that overwrite it works" <| fun () ->
438+
(Test(1).GetHashCode(), Test(1).GetHashCode()) ||> equal
439+
(Test(2).GetHashCode(), Test(1).GetHashCode()) ||> notEqual
440+
437441
testCase "GetHashCode with same object works" <| fun () ->
438442
let o = OTest(1)
439443
let h1 = o.GetHashCode()

0 commit comments

Comments
 (0)