问题
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
defaultVal
andtext
are formal parameters and will be inferred. Here,text
is already constrained to bestring
because it is used as the first parameter in a call to the static method,SomeType.TryParse
, as explain later.^a
is a statically resolved type parameter (vs a generic type parameter of the form'a
).^a
will 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 typedefaultVal
is. 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 aboolean
value. This constraint is made explicit by the stipulation of the stated return type on the line beginning with: ^a when ...
. The fact thatdefaultVal
itself 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 aboolean
value. This description is how F# matches the .Net definition of TryParse on DateTime, Int32, TimeSpan, etc. types. Note,byref
is F# equivalent of C#'sout
orref
parameter modifier.let r = ref defaultVal
creates a reference type and copies the provided value,defaultVal
, into it.ref
is one of the ways F# creates mutable types. The other is with themutable
keyword. 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 anif
statement 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.contents
provides the reference to the mutable content ofr
(simulating C#'sout
orref
parameter) 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)
.!r
is how you read a reference value.r.Value
would 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
self
object 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