During the Type Provider Live recording, Ryan asked me about basing erased provided types on dictionary types, and then exposing nicely typed properties to access data stored within the dictionary.
This will sound familiar to users of a number of dynamically typed languages as in many cases objects in these languages are just dictionaries under the hood.
This is such a common thing to be doing in a type provider that I thought it was worth writing up a working example that can then be modified to your individual situation. I've presented the entire listing below with comments, but there is one particular trick I'll explain in a bit more detail. Let's have a look at let bindings in quotations!
So, normally when you write a let binding in F#, and end up writing something like this:
1234
letmyFunction()=letx=10x+10
Here, the body of function myFunction is an expression that evaluates to 20. But it turns out that this is actually syntax sugar for:
12
letmyFunction()=letx=10inx+10
A quotation in F# always represents a single expression, so it shouldn't come as a surprise at this point that the Expr.Let class has a constructor this three arguments. The variable being bound, the value to bind to it, and the body in which it is used. So if you want to express the body of the function above you end up with something like this:
The trick you need to know is that Expr.Var produces an Expr that represents a place where a variable will be used. But it creates an untyped Expr, and this can (and does) cause issues with type inference. We can work around this by making use of typed expressions, represented by the generic Expr<'a> class. The type provider API takes the untyped version, but you can convert back to the untyped version either by calling the Raw property on the typed expression or just by using it to help construct an expression which contains the typed expression but which is untyped itself using the Expr classes.
In the code below, notice the use of <@ ... @> and % rather than <@@ ... @@> and %% to work with typed expressions rather than untyped.
123456789101112
openFSharp.QuotationstypeGD=System.Collections.Generic.Dictionary<string,string>letdictExpr=letgdVar=Var("gd",typeof<GD>)letgdExpr=Expr.VargdVar|>Expr.Cast<GD>// Expr.Cast forces this to be a typed expressionletaddValue=Expr.Let(gdVar,<@GD()@>,<@%gdExpr.["one"]<-"the number one"@>)// the line above fails without typed expressions
With that out of the way, we're good to go. The type provider below is a simple wrapper around a string, string dictionary. It looks like this in use:
1234567891011
typeMyType=DictProvider.ParaProvider<"name1, name2">letthing=MyType("1","2")thing.name1// "1"thing.name2// "2"thing.name1<-"not one. Muhahahaha!"thing.name2<-"that's why you shouldn't make things mutable"thing.name1// "not one. Muhahahaha!"
You'll get different properties depending which strings you supply as parameters.
moduleDictProvideropenSystem.ReflectionopenFSharp.Core.CompilerServicesopenFSharp.QuotationsopenProviderImplementation.ProvidedTypestypeGD=System.Collections.Generic.Dictionary<string,string>[<TypeProvider>]typeDictionaryProvider()asthis=inheritTypeProviderForNamespaces()letns="DictProvider"letasm=Assembly.GetExecutingAssembly()letcreateTypetypeName(parameters:obj[])=// We'll get our property names by just splitting// our single parameter on commasletpropNames=(parameters.[0]:?>string).Split','|>Array.map(funs->s.Trim())// Each of our properties has setter code to set the value in the dict with the// name of the property, and getter code with gets the same valueletaPropname=ProvidedProperty(name,typeof<string>,IsStatic=false,GetterCode=(funargs-><@@(%%args.[0]:GD).[name]@@>),SetterCode=(funargs-><@@(%%args.[0]:GD).[name]<-(%%args.[1]:string)@@>))// Here we set the type to be erased to as "GD" (our type alias for a dictionary)// If we want to hide the normal dictionary methods, we could use:// 'myType.HideObjectMethods <- true'// But here we'll just let people use the type as a dictionary as well.letmyType=ProvidedTypeDefinition(asm,ns,typeName,Sometypeof<GD>)// Make sure we add all the properties to the object.propNames|>Array.map(funpropName->aProppropName)|>List.ofArray|>myType.AddMembers// We'll want a constructor that takes as many parameters as we have// properties, as we'll want to set the value in the dictionary of our// properties during construction. If we don't, trying to use the properties// will result in a key not found exception.letcstorParams=propNames|>Array.map(funpropName->ProvidedParameter(propName,typeof<string>))|>List.ofArray// Here's the constructor code where we set each property in turn.// Notice how the fold keeps on building up a larger let expression,// adding a set value line at the top of the expression each time through.// Our initial state (a line with only the dictionary variable on) is always// left last, so this is what will be returned from the constructor.letcstorCode=fun(args:Exprlist)->letdictionaryVar=Var("dictionary",typeof<GD>)letdictionary:Expr<GD>=dictionaryVar|>Expr.Var|>Expr.CastletsetValues=args|>Seq.zippropNames|>Seq.fold(funstate(name,arg)-><@(%dictionary).[name]<-(%%arg:string)%state@>)<@%dictionary@>Expr.Let(dictionaryVar,<@GD()@>,setValues)// Build the constructor out of our helpersletcstor=ProvidedConstructor(cstorParams,InvokeCode=cstorCode)// And make sure you add it to the class!myType.AddMembercstormyTypeletprovider=ProvidedTypeDefinition(asm,ns,"ParaProvider",Sometypeof<obj>)letparameters=[ProvidedStaticParameter("PropNames",typeof<string>)]doprovider.DefineStaticParameters(parameters,createType)this.AddNamespace(ns,[provider])[<assembly:TypeProviderAssembly>]do()