Reputation: 12087
I have code generating this warning.
There are a few SO related posts, the closest is this one: This construct causes code to be less generic than indicated by the type annotations
but I don't see where it applies because my code is already in a function
so, the code in question is very simple:
let exists (key: 'a) =
r.Exists(string key)
let set (key: 'a) value =
r.Set((string key), value)
let get (key: 'a) =
r.Get(string key)
let setDefault (key: 'a) value =
if not (exists key) then
set key value
what I am trying to achieve is allow passing different enums, strings or even ints as a key, and, whatever the type passed, it would be converted to string (within reason obviously)
but when I use that code with an enum, I get the warning in the title.
So, I have two questions:
Upvotes: 2
Views: 210
Reputation: 6510
You can reproduce this warning with an extremely simple test case, clearing up some of the noise from your example:
let f (x: 'a) =
string x
Looking at this, you may be confused, because the type of the string
function is 'T -> string
, but it's not that simple. To understand what's happening, you have to look at the implementation of the string
function in FSharp.Core:
let inline anyToString nullStr x =
match box x with
| null -> nullStr
| :? System.IFormattable as f -> f.ToString(null,System.Globalization.CultureInfo.InvariantCulture)
| obj -> obj.ToString()
[<CompiledName("ToString")>]
let inline string (value: ^T) =
anyToString "" value
// since we have static optimization conditionals for ints below, we need to special-case Enums.
// This way we'll print their symbolic value, as opposed to their integral one (Eg., "A", rather than "1")
when ^T struct = anyToString "" value
when ^T : float = (# "" value : float #).ToString("g",CultureInfo.InvariantCulture)
when ^T : float32 = (# "" value : float32 #).ToString("g",CultureInfo.InvariantCulture)
when ^T : int64 = (# "" value : int64 #).ToString("g",CultureInfo.InvariantCulture)
when ^T : int32 = (# "" value : int32 #).ToString("g",CultureInfo.InvariantCulture)
when ^T : int16 = (# "" value : int16 #).ToString("g",CultureInfo.InvariantCulture)
when ^T : nativeint = (# "" value : nativeint #).ToString()
when ^T : sbyte = (# "" value : sbyte #).ToString("g",CultureInfo.InvariantCulture)
when ^T : uint64 = (# "" value : uint64 #).ToString("g",CultureInfo.InvariantCulture)
when ^T : uint32 = (# "" value : uint32 #).ToString("g",CultureInfo.InvariantCulture)
when ^T : int16 = (# "" value : int16 #).ToString("g",CultureInfo.InvariantCulture)
when ^T : unativeint = (# "" value : unativeint #).ToString()
when ^T : byte = (# "" value : byte #).ToString("g",CultureInfo.InvariantCulture)
This is using statically-resolved type parameters and using an explicit implementation for each of the listed types, so it is not really as generic as it seems from the type signature. In your case, what's happening is that it's inferring the most compatible type, and because your function is just typed as 'a
, it's picking obj
. So, because you're calling string
and your input parameter is being forced into one of the types that the string
function actually handles (which is actually in anyToString
), and that's obj
.
To make it work in your real-world scenario is actually pretty simple: Just make your functions inline and don't put a type on the parameter at all:
let inline exists key =
r.Exists(string key)
This will infer the type for the parameter and call the right version of string
, and that will work with pretty much anything you want to pass it, including your enums.
Upvotes: 2
Reputation: 564333
The issue is from using string
. If you look at the source for string, you'll see that it's inline
, which means the compiler needs to determine the type at compile time. In your case, that means the generic type has to be resolved before string
can be called, which in turn forces the compiler to pick something that will work - in this case, obj
.
There are two simple ways to work around this - first, you can make your functions inline, which allows the compiler to defer this until the function using your string gets used. This will work in many cases, but can also potentially "push this off" to the consumer of your API later if they do the same thing you're doing. This would look like:
let inline set (key: 'a) value =
r.Set((string key), value)
The other option is to avoid the string
operator, and use the fact that all objects in .NET include ToString
, and call that instead:
let set (key: 'a) value =
r.Set(key.ToString()), value)
Either approach will avoid the warnings and keep the functions generic.
Upvotes: 2