#Intro
I looked at this thinking it would be useful with WebSharper.Forms definition. I was wrong, but I want to keep a trace of this, so here is a blog post about it. This post is not meant to be pedagogical, but it can be useful as an example.
We’ll work on an example DU type hypothetically used to describe form fields:
type DataType = |String of name:string |Int of name:string |Choice of name:string * string list
You can use this type to define forms. For example a form to create a person entry with fields
name
(a string), age
(an int), and occupation
which is either employee
or freelance
:
let personDetails = [ String "name"; Int "age"; Choice ("occupation",["employee";"freelance"]) ]
In my (in the end useless) exploration, for this form I needed to define a function of this form:
let f (name:string) (age:int) (occupation:string) -> (name,age,occupation)
The number of arguments of this function is the number of fields in the form, and it returns a tuple of the same arity. The types of the arguments are determined by the form field’s DU case.
Similarly an form to create a building entry:
let buildingDetails = [ Int "stories"; Choice ("type",["house";"tower"]);String "name" ]
should lead to the definition of a function of this shape (illustrating the name of the function parameters have no importance):
let f (s:int)(t:string)(n:string) = (s,t,n)
Implementation
We’ll use the FSharp.Reflection namespace. It profides functions to define types under FSharpType and to define values under FSharpValue. From the former, we’ll use MakeFunctionType and MakeTupleType. From the latter, we’ll use MakeFunction and MakeTuple.
We start by opening the reflection namespace:
open Microsoft.FSharp.Reflection
Let’s first define a function that will map the DU case of DataType to the field’s data type:
let getDomainForDetail d =
match d with
| String _ -> typeof<string>
| Int _ -> typeof<int>
| Choice _ -> typeof<string>
We the need to define the type of the function, as it needs to be passed to FSharpValue.MakeFunction
.
iFSharpType.MakeFunctionType
only defines one-argument functions. So to define a function of 2 arguments, we need to defined a function of one argument returning a function handling the second argument. See more info about currying if this isn’t clear.
The function handling the last argument will have as return type the type of the tuple to be returned.
For each field of the form we need to definea one argument function. Its return type is also a one-argument function, except for the function handling the last field in the list, as this function’s return type is the type of the tuple to be returned. Here is the implementation of our function computing the function’s type based on the list of fields it gets:
// need to pass allDetails as this is needed to compute the right tuple type.
// Otherwise, in recursive calls of buildFunctionImpl, where only the remaining details are passed to compute the range of the function,
// we don't compute the correct tuple type.
// hence in recursive calls of buildFunctionImpl, details is different from allDetails
let buildFunctionType details allDetails =
let rec buildFunctionTypeAcc details =
match details with
| [] ->
// end recursion
FSharpType.MakeTupleType (allDetails |> List.map (fun d -> getDomainForDetail d ) |> List.toArray)
| h :: t ->
let domain = getDomainForDetail h
FSharpType.MakeFunctionType(domain, buildFunctionTypeAcc t)
buildFunctionTypeAcc details
FSharpValue.MakeFunction
takes 2 arguments: the function type, and its implementation.
The function’s type is computed with the previous function. We need to provide its implementation as a lambda.
It is a one argument function, so it will be of the form fun arg -> ...
.
The body of the lambda is also a one argument function, except at the last step, where we need to build the tuple to be returned, which
will be built with all arguments passed to our lambdas. This suggests we need to accumulate the arguments through recursive calls.
Building a tuple with FSharpValue.MakeTuple
required passing not only the values, but also the type of the tuple. This suggests we need to
also accumulate the types of the lambda’s arguments through recursive calls
I ended up with this implementation:
let buildFunctionImpl allDetails =
let rec buildFunctionImplAcc details typesAcc valuesAcc =
match details with
| [] ->
// build the tuple
let tupleType = FSharpType.MakeTupleType typesAcc
FSharpValue.MakeTuple(valuesAcc, tupleType)
| h::t ->
let argType = getDomainForDetail h
let functionType = buildFunctionType details allDetails
FSharpValue.MakeFunction(
functionType,
(fun arg ->
buildFunctionImplAcc t (Array.append typesAcc [|argType|]) (Array.append valuesAcc [|arg|])))
buildFunctionImplAcc allDetails [||] [||]
We can get the function like this:
let fo = buildFunctionImpl personDetails
Calling the function
We have our function availble through fo
, but how to we call it?
fo is of type obj
, which we can’t call or invoke….
The trick is to get a handle on the function’s Invoke
method, which can be done with
fo.GetType().GetMethod("Invoke")
. One wa have ahandle on the Invoke
method, we can call its … Invoke
method.
Invoke
takes 2 arguments: the function to be invoked, and an obj array of its arguments.
We end up with this call:
let v = fo.GetType().GetMethod("Invoke").Invoke(fo,[| box "jon"|])
|> (fun f -> f.GetType().GetMethod("Invoke").Invoke(f,[| box 27|]) )
|> (fun f -> f.GetType().GetMethod("Invoke").Invoke(f,[| box "freelance"|]))
In FSI, this displays val it: obj = ("jon", 27, "freelance")
, showing the tuple was built correctly. It is returned as a boxed value, hence its obj
type.
By defining our own invoke
function we make it conciser:
let invoke x f = f.GetType().GetMethod("Invoke").Invoke(f,[|x|])
invoke (box "raphael") fo
|> invoke (box 47)
|> invoke (box "freelance")
This function can be passed as argument to a statically typed function, confirming its implementation is fine. Imagine you have a caller defined like this:
let caller (f:string->int->string->string*int*string) (s1:string) (i:int) (s2:string) = f s1 i s2;;
Calling it in FSI with fo
gives this:
> caller (unbox fo) "jon" 47 "freelance";;
val it: string * int * string = ("jon", 47, "freelance")