问题
Presently we do this...
let parseDate defaultVal text =
match DateTime.TryParse s with
| true, d -> d
| _ -> defaultVal
Is it possible to do this...
let d : DateTime = tryParse DateTime.MinValue "2015.05.01"
回答1:
Yes. Welcome to the world of member constraints, ref, and byref values.
let inline tryParseWithDefault
defaultVal
text
: ^a when ^a : (static member TryParse : string * ^a byref -> bool)
=
let r = ref defaultVal
if (^a : (static member TryParse: string * ^a byref -> bool) (text, &r.contents))
then !r
else defaultVal
defaultValandtextare formal parameters and will be inferred. Here,textis already constrained to bestringbecause it is used as the first parameter in a call to the static method,SomeType.TryParse, as explain later.^ais a statically resolved type parameter (vs a generic type parameter of the form'a).^awill be resolved at compile time to a specific type. Consequently, the function hosting it must be markedinline, which means that each invocation of the function will become an in-place replacement of that invocation with the actual body of the function, wherein each static type parameter will become a specific type; in this case, whatever typedefaultValis. There is no base type or interface type constraints restricting the possible type ofdefaultVal. However, you can provide static and instance member constraints such as is done here. Specifically, the result value (and thereforedefaultVal) must apparently have a static member called,TryParse, that both accepts astring, a reference to a mutable instance of that type, and return abooleanvalue. This constraint is made explicit by the stipulation of the stated return type on the line beginning with: ^a when .... The fact thatdefaultValitself is a possible result constrains it to be of the same type as^a. (The constraint is also implicit elsewhere throughout the function).: ^a when ^a : (static ....describes the result type,^a, as having a static member called TryParse of typestring * ^a byref -> bool. That is to say, the result type will have a static member that accepts astring, a reference to an instance of itself (and therefore mutable), and will return abooleanvalue. This description is how F# matches the .Net definition of TryParse on DateTime, Int32, TimeSpan, etc. types. Note,byrefis F# equivalent of C#'soutorrefparameter modifier.let r = ref defaultValcreates a reference type and copies the provided value,defaultVal, into it.refis one of the ways F# creates mutable types. The other is with themutablekeyword. The difference is that mutable stores its value on the stack while ref stores its in main memory/heap and holds an address (on the stack) to it. The latest version of F# will seek to automatically upgrade mutable designations to ref depending on the context, allowing you to code only in terms of mutable.if (^a : (static...is anifstatement over the invocation results of the TryParse method on the statically inferred type,^a. This TryParse is passed,(text, &r.contents), per its(string * ^a byref)signature. Here,&r.contentsprovides the reference to the mutable content ofr(simulating C#'soutorrefparameter) per the expectation of TryParse. Note, we are off the reservation here and certain F# niceties for inter-operating with the .Net framework do not extend out this far, in particular, the automatic rolling up of space separated F# parameters into .net framework function parameters as a tuple. Hence, the parameters are provide to the function as tuple,(text, &r.contents).!ris how you read a reference value.r.Valuewould also work.
The TryParse methods provided by .Net seems to always set a value for the out parameter. Consequently, a default value is not strictly required. However, you need a result value holder, r, and it must have an initial value, even null. I didn't like null. Another option, of course, is to impose another constraint on ^a that demands a default value property of some sort.
The following subsequent solution removes the need for a default parameter by using the Unchecked.defaultof< ^a > to derive a suitable placeholder value from the "inferred result" type (yes, it feels like magic). It also uses the Option type to characterize success and failure obtaining a result value. The result type is therefore, ^a option.
tryParse
text
: ^a option when ^a : (static member TryParse : string * ^a byref -> bool)
=
let r = ref Unchecked.defaultof< ^a >
if (^a : (static member TryParse: string * ^a byref -> bool) (text, &r.contents))
then Some (!r)
else None
And, per @kvb suggestions, the following brevity is possible. In this case, type inference is employed to both stipulate the type constraint on ^a as a consequence of it's invocation in the if (^a : ...)) expression and to also establish the type of the mutable buffer r for TryParse's out parameter. I have since come to learn this is how FsControl does some of it's magic
let inline tryParseWithDefault defaultVal text : ^a option =
let mutable r = defaultVal
if (^a : (static member TryParse: string * ^a byref -> bool) (text, &r))
then Some r
else None
let inline tryParse text = tryParseWithDefault (Unchecked.defaultof<_>) text
For the case of using type constraints on instance member such as type constraining fsharp's dynamic member lookup custom operator, ?, such that the type of the subject must contain a FindName:string->obj member, the syntax is as follows:
let inline (?) (instanceObj:^A) (property:string) : 'b =
(^A : (member FindName:string -> obj) (instanceObj, property)) :?> 'b
Note:
- The actual signature of instance methods explicitly specifies the
selfobject that is normally a hidden first parameter - This solution also promotes whatever the result to,
'b
A sample usage would be the following:
let button : Button = window?myButton
let report : ReportViewer = window?reportViewer1
来源:https://stackoverflow.com/questions/33161244/in-f-is-it-possible-to-have-a-tryparse-function-that-infers-the-target-type