Is there a way in F# to type-test against a generic type without specifying the instance type?
I'm trying to pattern match against a few types that I care about for SQL generation. Ideally I'd like to do this:
let rec getSafeValue record (prop: PropertyInfo) =
match prop.GetValue(record, null) with
| :? string as str -> "'开发者_运维问答" + str + "'"
| :? Option<_> as opt ->
match opt with
| Some v -> getSafeValue v prop
| None -> "null"
| _ as v -> v.ToString()
The problem is that here, the type parameter to Option<_>
gets constrained to match that of record
, which ends up being just obj
.
I know I can do some pain-in-the-behind reflection-based check (check that it's a generic type and that it's an option type based on the name), but I'd rather avoid that if at all possible.
No, there's no good way to do this using F#'s built-in constructs. However, you could build your own reusable active pattern for this sort of thing:
open Microsoft.FSharp.Reflection
open Microsoft.FSharp.Quotations
open Microsoft.FSharp.Quotations.DerivedPatterns
open Microsoft.FSharp.Quotations.Patterns
let (|UC|_|) e o =
match e with
| Lambdas(_,NewUnionCase(uc,_)) | NewUnionCase(uc,[]) ->
if (box o = null) then
// Need special case logic in case null is a valid value (e.g. Option.None)
let attrs = uc.DeclaringType.GetCustomAttributes(typeof<CompilationRepresentationAttribute>, false)
if attrs.Length = 1
&& (attrs.[0] :?> CompilationRepresentationAttribute).Flags &&& CompilationRepresentationFlags.UseNullAsTrueValue <> enum 0
&& uc.GetFields().Length = 0
then Some []
else None
else
let t = o.GetType()
if FSharpType.IsUnion t then
let uc2, fields = FSharpValue.GetUnionFields(o,t)
let getGenType (t:System.Type) = if t.IsGenericType then t.GetGenericTypeDefinition() else t
if uc2.Tag = uc.Tag && getGenType (uc2.DeclaringType) = getGenType (uc.DeclaringType) then
Some(fields |> List.ofArray)
else None
else None
| _ -> failwith "The UC pattern can only be used against simple union cases"
Now your function might look something like this:
let rec getSafeValue (item:obj) =
match item with
| :? string as str -> "'" + str + "'"
| UC <@ Some @> [v] -> getSafeValue v
| UC <@ None @> [] -> "null"
| _ as v -> v.ToString()
This can't work in F# without covariance. Assuming that you're happy for v
to be of type obj
, you want to be able to treat Option<anything>
as if it was Option<obj>
. Without covariance, Option<anything>
and Option<obj>
are independent types.
When I put your code in F# Interactive, it seems to make 'record' a generic param. Maybe it works differently in the normal compiler. Anyway, it is probably picking up that obj
type due to the first argument of GetValue
being type obj
.
I'm sorry I can't test this right now, but give this a shot. The box
function uses a generic param, so that might do the trick.
let rec getSafeValue record (prop: PropertyInfo) =
match prop.GetValue(box record, null) with
| :? string as str -> "'" + str + "'"
| :? Option<_> as opt ->
match opt with
| Some v -> getSafeValue v prop
| None -> "null"
| _ as v -> v.ToString()
Some v -> getSafeValue v prop will only work if v is of the same type as record. (or deriving from that that type) otherwise the first line will fail. you can't say prop.GetValue(record,null) unless the property pointed to by prop makes sense (aka is part of the type) in the context of the first argument.
If it's the same type you can do:
let rec getSafeValue (record:'a) (prop: PropertyInfo) =
match prop.GetValue(box record, null) with
| :? string as str -> "'" + str + "'"
| :? Option<'a> as opt ->
match opt with
| Some v -> getSafeValue v prop
| None -> "null"
| _ as v -> v.ToString()
but if the type of v is derived from 'a it will match the last case so for the above to work they'd need to be exactly the same type
精彩评论