Overlord Zurg
Overlord Zurg

Reputation: 3794

F# Type Provider development: When providing a method, how to access parameters of variable number and type?

My question is this:

How can I splice the expressions from a list into a quotation when I don't know the number and types of those expressions at design time?

At the bottom I've included the full code for a type provider. (I've stripped the concept down to demonstrate the problem.) My issue occurs at these lines:

  let func = ProvidedMethod((*...*), InvokeCode = fun args ->
        <@@ let stringParts =
                args
                |> List.mapi (fun i arg -> 
                    if paramTypes.[i] = typeof&lt;int&gt; then
                        sprintf "%i" (%%arg: int)...

On the lambda parameter arg, I get the following error:

error FS0446: The variable 'arg' is bound in a quotation but is used as part of a spliced expression. This is not permitted since it may escape its scope.``

I can't figure out how to write code such that I "extract" the parameter values when the numbers and types of the values are not known at provider-design time (although they will be known at compile time).

When I DO know of a parameter's existence and type at design time, I can do this:

printfn "%A" (%%args.[0]: int)

But I can't figure out how to get from the Expr list input to an obj list within the quotation.

Here's the full type provider code:

[<TypeProvider>]
type SillyProviderDefinition(config: TypeProviderConfig) as self = 
    inherit TypeProviderForNamespaces()

let sillyType = ProvidedTypeDefinition(THIS_ASSEMBLY, NAMESPACE, "SillyProvider", Some typeof<obj>)
    do sillyType.DefineStaticParameters([ProvidedStaticParameter("argTypes", typeof<string>)], fun typeName args ->
        let retType = ProvidedTypeDefinition(typeName, Some typeof<obj>)        

        let paramTypes =
            (args.[0] :?> string).Split([|'|'|])
            |> Array.map (function
                | "int" -> typeof<int>
                | "string" -> typeof<string>
                | x -> failwithf "Invalid argType %A. Only string or int permitted" x)
        let parameters =
            paramTypes
            |> Array.mapi (fun i p -> ProvidedParameter(sprintf "arg%i" i, p))
            |> Array.toList

        let func = ProvidedMethod("Stringify", parameters, typeof<string>, IsStaticMethod = true, InvokeCode = fun args ->
            <@@ let stringParts =
                    args
                    |> List.mapi (fun i arg ->
                        if paramTypes.[i] = typeof<int> then
                            sprintf "%i" (%%arg: int)
                        elif paramTypes.[i] = typeof<string> then
                            (%%arg: string)
                        else
                            failwith "Unexpected arg type")
                //printfn "%A" (%%args.[0]: int)
                String.Join("", stringParts) @@>)

        do retType.AddMember func
        do sillyType.AddMember retType
        retType)

    do self.AddNamespace(NAMESPACE, [sillyType])

Upvotes: 3

Views: 183

Answers (1)

Tomas Petricek
Tomas Petricek

Reputation: 243051

As a minimal example, let's say that we have list with types and a list with some quotations (in the context of type provider, you have the list of types and args is the list of quotations, possibly also containing the this instance):

open Microsoft.FSharp.Quotations

let tys = [ typeof<int>; typeof<string> ]
let args = [ Expr.Value(42); Expr.Value("test"); ]

We want to construct an expression that calls formatInt or formatString depending on the type and then concatenates all the formatted strings:

let formatInt (n:int) = string n
let formatString (s:string) = s

Now, it's important to distinguish what happens in the provided quoted code (quote-level) and in the ordinary code that is run to generate the quotation (code-level). At code-level, we iterate over all the types and quoted arguments and generate a list of quotations with calls to formatInt or formatString - those can be typed Expr<string> because they have the same type:

let formattedArgList = 
  [ for t, e in List.zip tys args ->
      if t = typeof<int> then <@ formatInt %%e @>
      elif t = typeof<string> then <@ formatString %%e @>
      else failwith "!" ]

Now you can build a list expression by calling fold at code-level and using the list :: operator at the quote-level:

let listArgExpr = 
  formattedArgList
  |> List.fold (fun state e -> <@ %e::%state @>) <@ [] @>

And now you can construct a quotation with quoted String.concat call:

<@ String.concat "," %listArgExpr @>

Upvotes: 6

Related Questions