memoize fails if an argument to memoized function is illegal from F# compiler perspective
See original GitHub issueDescription
If a memoized function takes an F# type as an argument, any value passed in must be legal in order for memoization to work. Whilst this may sound desirable, we have a situation where we’re trying to memoize union case values. If a union case value takes fields, the caller is not required to pass them in, so we “fill them in” by creating (and memoizing, but that’s irrelevant) a default value. The default value may not be legal from F#'s perspective because there’s only so much we can fill in. We then use that default value when calling another memoized function, and that’s where the problem arises, because memoizing eventually invokes GetHashCode
on the “illegal” object and that crashes with NullReferenceException
.
Repro steps
module Repro =
open FSharpPlus
open Xunit
type SomeRecord = {
Foo: List<string>
}
let getStuff (_: obj) : int * string =
(42, "Answer to everything")
let getStuffMemoized : (obj -> int * string) = memoizeN getStuff
[<Fact>]
let ``repro (crashes with System.NullReferenceException)`` () =
let defaultRecordValue = {
Foo = Unchecked.defaultof<List<string>>
}
getStuffMemoized defaultRecordValue |> ignore
Expected behavior
Ideally, it would just treat any null
values in the object structure as zero when calculating a hash. To be honest, I’m not sure that this is in FSharpPlus’ hands because it’s the GetHashCode
generated by the F# compiler that is crashing. It assumes a valid object, presumably for performance reasons.
Actual behavior
System.NullReferenceException : Object reference not set to an instance of an object.
SomeRecord.GetHashCode(IEqualityComparer comp) line 0
MemoizationKeyWrapper`1.GetHashCode(IEqualityComparer comp)
MemoizationKeyWrapper`1.GetHashCode()
ConcurrentDictionary`2.GetOrAdd(TKey key, Func`2 valueFactory)
MemoizeN.getOrAdd[a,b](ConcurrentDictionary`2 cd, FSharpFunc`2 f, a k)
MemoizeN@28.Invoke(FSharpFunc`2 arg10, a arg20)
Known workarounds
None that will work in a generic fashion (without the caller having to do some custom mangling of their types).
Issue Analytics
- State:
- Created 2 years ago
- Comments:5 (2 by maintainers)
Top GitHub Comments
Ended up getting around this by defining my own record type with custom equality. The record encapsulates the two fields I was originally memoizing on, but overrides
GetHashCode
+Equals
so that only the relevant field is used. By memoizing a function taking this custom record as a parameter, it memoizes how I want.For the record, here is the original suggestion to fix this https://github.com/fsharp/fslang-suggestions/issues/92