There are four built-in type providers in F# 3.0. If you’ve wondered how to make a type provider that can be used to interpret data—such as the data in a database, in the cloud, or in an XML file—this chapter will show you how to do it.
As I mentioned at the beginning of Chapter 4, the type provider is an adapter component that reads the underlying information schema and converts it to types in .NET. The information can be from data stores, services, or any other source of structured data. The type-generation process is known as the provided type. The type generated from a type provider can be picked up by Microsoft IntelliSense. In addition to the class type and members, the type provider can provide metadata such as a description of each column in a database table. These descriptions appear as tooltips. The type provider provides compile-time meta-programming support. This definition might seem heavy and difficult to digest. If that is the case, don’t worry. As more and more type provider samples are presented, different aspects of the type provider will be examined and things will become clear.
You need to understand that the type provider types and methods are generated in a lazy way. The lazy generation decreases the code size. Imagine you have a complex database schema and your code accesses only one table in the database. The erased type provider generates only the code for that particular table. This allows the provided type space to be large and even infinite. The built-in type providers use type generation, which involves wrapping existing code-generation tools. This chapter focuses on how to write the erased type provider and the generated type provider. The erased type provider provides types that will be erased to other types, such as System.Object. The following statement is from MSDN (http://msdn.microsoft.com/en-us/library/hh361034.aspx#BK_Erased):
Each provided type is erased to type obj, and all uses of the type will appear as type obj in compiled code. In fact, the underlying objects in these examples are strings, but the type will appear as Object in .NET compiled code. As with all uses of type erasure, you can use explicit boxing, unboxing, and casting to subvert erased types. In this case, a cast exception that isn’t valid may result when the object is used. A provider runtime can define its own private representation type to help protect against false representations. You can’t define erased types in F# itself. Only provided types may be erased. You must understand the ramifications, both practical and semantic, of using either erased types for your type provider or a provider that provides erased types. An erased type has no real .NET type. Therefore, you cannot do accurate reflection over the type, and you might subvert erased types if you use runtime casts and other techniques that rely on exact runtime type semantics. Subversion of erased types frequently results in type cast exceptions at runtime.
The erased type provider generates types that exist only at design time, whereas the generated type provider generates a real type that is compiled in the assembly.
F# provides an API that you can use to write your own type providers. The type-provider author requires several files. A type-provider template is available to help make the type-provider authoring process easier. The type-provider template contains several API files that are also published on the F# Sample Pack site (http://fsharp3sample.codeplex.com/). The type-provider feature is new in F# 3.0 and, as such, will not work with previous versions of F#.
As an alternative to performing a manual setup of the Visual Studio solution and project, you can use a Visual Studio template package published at the Visual Studio Gallery, which can be downloaded from http://visualstudiogallery.msdn.microsoft.com/43d00ffd-1b9a-4581-a942-da85b6a83b9c. After the package is installed, the type-provider project can be created via the New Project dialog box, as shown in Figure 5-1.
The type-provider project is a class library project, and it contains four files. The type-provider class, which is named TypeProvider1.fs, is the backbone of the project. It contains the type-provider code. TestScript.fsx is a script file that can be used to test the type provider. The generated project is shown in Figure 5-2.
Example 5-1 contains the code needed to reference a type provider. The type-provider type is denoted as the provided type, and the method and property is denoted as a provided method and provided property. The first line adds a reference to the Type Provider dynamic-link library (DLL). After the reference is ready, the second and third lines create the provided type and assign it to the value named t.
It is a good practice to first design how an end user will invoke the type provider. For example, TPTestType should have a parameterless constructor that does not require any special initialization code. Also, it has a property called Property1.
The type-provider class code is shown in Example 5-2. As you saw in Example 5-1, the class should have a parameterless constructor and one property. The type-provider class should inherit from the TypeProviderFromNamespaces class and be decorated with the TypeProvider attribute. The type-provider type is created by the ProvidedTypeDefinition class. There are four parameters and, for now, only two of them are of interest to us. The namespace is specified when the ProvidedTypeDefinition instance is created. Like all types in the .NET world, it needs a base class. In the sample code, the base type is typeof<obj>. The provided type definition, named newT, is added at the end of the code, by calling AddNamespace with the namespace variable.
Once the type is created, the method and property are easy to understand. ProvidedMethod takes a method name, parameters, a return type, a Boolean that indicates whether or not the property is static, and a function that contains a code quotation that will be executed whenever the method is invoked. In the sample code, InvokeCode is 1+1, so it will always return 2. The property is added by creating a ProvidedProperty with a property name, a property type, an indication of whether it is static or not, and getter/setter code.
The function logic has to be a code quotation. The code quotation is a tree structure that represents F# code. There is no direct equivalent in C#. You can think of it as being like C# LINQ. There are two ways to generate an F# code quotation. One is to use <@@ and @@> operators, and the other way is to use Expr functions. Both approaches can generates the same code quotation, so you can choose either way. The sample code in this chapter demonstrates both ways to generate code quotation.
The F# class cannot be instantiated without a constructor; therefore, the constructor is mandatory for a type-provider type, although in most cases it does nothing. ProvidedConstructor takes no parameters, and the constructor code does nothing. The provided method, property, and constructor are finally added to the ProvidedTypeDefinition variable newT by invoking the AddMember function.
namespace Samples.FSharp.HelloWorldTypeProvider open System.Reflection open Microsoft.FSharp.Core.CompilerServices open Samples.FSharp.ProvidedTypes [<TypeProvider>] type public TypeProvider1() as this = inherit TypeProviderForNamespaces() // Get the assembly and namespace used to house the provided types let thisAssembly = Assembly.GetExecutingAssembly() let rootNamespace = "Samples.ShareInfo.TPTest" let baseTy = typeof<obj> let newT = ProvidedTypeDefinition(thisAssembly, rootNamespace, "TPTestType", Some baseTy) // add other property and method definition let m = ProvidedMethod( methodName = "methodName", parameters = [], returnType = typeof<int>, IsStaticMethod = false, InvokeCode = fun args -> <@@ 1 + 1 @@> ) let ctor = ProvidedConstructor(parameters = [], InvokeCode = fun args -> <@@ (* base class initialization or null*) () @@>) let prop2 = ProvidedProperty(propertyName = "Property1", propertyType = typeof<string>, IsStatic=false, GetterCode= (fun args -> <@@ "Hello!" @@>), SetterCode = (fun args -> <@@ printfn "setter code" @@>)) do prop2.AddXmlDocDelayed(fun () -> "xml comment") do newT.AddMember(m) newT.AddMember(prop2) newT.AddMember(ctor) do this.AddNamespace(rootNamespace, [newT]) [<TypeProviderAssembly>] do ()
The test script is lightweight and good for testing and debugging a small type provider, but it is not the only way to do that. For complex type providers, using two instances of Visual Studio is a better choice. Use one instance of Visual Studio to develop the type provider and refer to that instance as Developing Visual Studio. Use the other instance of Visual Studio to host the testing project, and refer to it as Testing Visual Studio. Developing Visual Studio needs to attach to the testing Visual Studio.
Because the type-provider DLL is locked by Visual Studio, restarting Visual Studio is unavoidable. If the test script is used, make sure that the script file is not the current file when you are closing the project; otherwise, it will be opened automatically and consequently lock the DLL the next time that the project is opened.
If you understood the F# type-provider template code from the prior section, the HelloWorld type-provider example in this section will be easy for you to follow. However, if any of the code presented in the last section is not clear, the HelloWorld type provider example will provide some clarity. This type-provider example has five methods and five properties. Example 5-3, which defines how users invoke the HelloWorld type provider, can be used to design and test the type provider.
#r @".inDebugHelloWorldTypeProvider.dll" let assertFunction x = if not x then failwith "expression is false" type T = Samples.ShareInfo.TPTest.HelloTypeProvider let t = T() assertFunction( t.Method1() = 1 ) assertFunction( t.Method2() = 2 ) assertFunction( t.Method3() = 3 ) assertFunction( t.Method4() = 4 ) assertFunction( t.Method5() = 5 ) assertFunction( t.Property6 = "Property 6") assertFunction( t.Property7 = "Property 7") assertFunction( t.Property8 = "Property 8") assertFunction( t.Property9 = "Property 9") assertFunction( t.Property10 = "Property 10")
In Example 5-3, assert is the F# keyword used to check for a TRUE/FALSE value, but it works only when the DEBUG symbol is defined. In the sample, a user-defined function named assert is used instead.
First create the Type Provider project using the Type Provider template with HelloWorldTypeProvider as the project name. (See Figure 5-3.) The template, which can be downloaded from http://visualstudiogallery.msdn.microsoft.com/43d00ffd-1b9a-4581-a942-da85b6a83b9c, has four files created by default. The TypeProvider1.fs file is where the type-provider code will be placed.
Because the type provider generates a type named HelloWorldTypeProvider, ProvidedTypeDefinition needs Example 5-7 to be provided as the third parameter in the constructor:
let newT = ProvidedTypeDefinition(thisAssembly, rootNamespace, "HelloWorldTypeProvider", Some
baseTy)
As mentioned previously, the type provider has five methods and five properties. The template adds the code to generate a single method and property (as shown in Example 5-4) by creating ProvidedMethod and ProvidedProperty. As a result, it is not difficult to add a List.map to the generated code so that five methods and five properties (which you can see in Example 5-5) are generated by the type provider.
let m = ProvidedMethod( methodName = "methodName", parameters = [], returnType = typeof<int>, IsStaticMethod = false, InvokeCode = fun args -> <@@ 1 + 1 @@> ) let prop2 = ProvidedProperty(propertyName = "Property1", propertyType = typeof<string>, IsStatic=false, GetterCode= (fun args -> <@@ "Hello!" @@>), SetterCode = (fun args -> <@@ printfn "setter code" @@>))
let ms = [ 1..5 ] |> List.map (fun i -> let m = ProvidedMethod( methodName = sprintf "Method%d" i, parameters = [], returnType = typeof<int>, IsStaticMethod = false, InvokeCode = fun args -> <@@ i @@>) m) let props = [ 6..10 ] |> List.map (fun i -> let prop2 = ProvidedProperty(propertyName = sprintf "Property%d" i, propertyType = typeof<string>, IsStatic=false, GetterCode= (fun args -> <@@ sprintf "Property %d" i @@>), SetterCode = (fun args -> <@@ () @@>)) prop2.AddXmlDocDelayed(fun () -> sprintf "xml comment for Property%d" i) prop2)
Because the default constructor is good enough for the HelloWorldTypeProvider project, you can now add a ProvidedMethod list, a ProvidedProperty list, and a constructor to the ProvidedTypeDefinition. (See Example 5-6.) The code for the type provider after these changes are made is shown in Example 5-7.
do ms |> Seq.iter newT.AddMember props |> Seq.iter newT.AddMember newT.AddMember(ctor)
// Learn more about F# at http://fsharp.net // See the 'F# Tutorial' project for more help. namespace Samples.FSharp.HelloWorldTypeProvider open System.Reflection open Microsoft.FSharp.Core.CompilerServices open Samples.FSharp.ProvidedTypes [<TypeProvider>] type public TypeProvider1() as this = inherit TypeProviderForNamespaces() // Get the assembly and namespace used to house the provided types let thisAssembly = Assembly.GetExecutingAssembly() let rootNamespace = "Samples.ShareInfo.TPTest" let baseTy = typeof<obj> let newT = ProvidedTypeDefinition(thisAssembly, rootNamespace, "HelloWorldTypeProvider", Some baseTy) // add other property and method definition let ms = [ 1..5 ] |> List.map (fun i -> let m = ProvidedMethod( methodName = sprintf "Method%d" i, parameters = [], returnType = typeof<int>, IsStaticMethod = false, InvokeCode = fun args -> <@@ i @@>) m) let ctor = ProvidedConstructor(parameters = [], InvokeCode = fun args -> <@@ (* base class initialization or null*) () @@>) let props = [ 6..10 ] |> List.map (fun i -> let prop2 = ProvidedProperty( propertyName = sprintf "Property%d" i, propertyType = typeof<string>, IsStatic=false, GetterCode= (fun args -> <@@ sprintf "Property %d" i @@>), SetterCode = (fun args -> <@@ () @@>)) prop2.AddXmlDocDelayed(fun () -> sprintf "xml comment for Property%d" i) prop2) do ms |> Seq.iter newT.AddMember props |> Seq.iter newT.AddMember newT.AddMember(ctor) do this.AddNamespace(rootNamespace, [newT]) [<TypeProviderAssembly>] do ()
Because most of the work is done by the template, the basic rules to write a simple type provider are as follows:
The type-provider type is defined by ProvidedTypeDefinition.
If a method is needed, create an instance of the ProvidedMethod type and add it to the ProvidedTypeDefinition instance.
If a property is needed, create an instance of the ProvidedProperty type and add it to the ProvidedTypeDefinition instance.
You will always need a constructor, and it also needs to be added to the ProvidedTypeDefinition instance. The default constructor that is generated by the template is good for most cases.
As long as the F# snippet add-on and latest snippet-file Visual Studio extensions are installed, the snippet to generate ProvidedMethod, ProvidedProperty, and ProvidedConstructor is under Type Providers And Query, Write Type Provider, as shown in Figure 5-4. The F# code snippet add-on can be downloaded from http://visualstudiogallery.msdn.microsoft.com/d19080ad-d44c-46ae-b65c-55cede5f708b, and the latest snippet file is located as an open source project at http://fsharpcodesnippet.codeplex.com/.
As you know, the type provider is used to generate a new type with class members, such as methods and properties. HelloWorldTypeProvider generates class members from a predefined data schema. The predefined data schema in this case requires five methods and five properties. As software developers, we always look for something that can be created programmatically. Is there any way to create the class members from some variable passed in by a user? The answer is ”Yes.” The regular-expression type provider dynamically generates class members based on a regular-expression parameter passed in by a user, as shown in Example 5-8.
type T = Samples.ShareInfo.TPTest.RegularExpressionTypeProvider< @"(?<AreaCode>^d{3})-(?<PhoneNumber>d{3}-d{4}$)" > let reg = T() let result = T.IsMatch("425-555-2345") let r = reg.Match("425-555-2345").AreaCode.Value
The first line in the listing shows how the type provider takes the parameters between < and >. This is the most common way for a type provider to get parameters. The third line shows a static method, but this is not a problem because ProvidedMethod can take IsStaticMethod = true
. The last line is the most interesting part. The Match function returns a variable that has an AreaCode property. But the AreaCode property does not look like any standard .NET property. It is a generated property from the regular-expression group AreaCode. From the preceding analysis, something is needed to take and interpret the regular-expression pattern string and the type provider needs at least two types. One is the type-provider type, and the other one is a type with an AreaCode property.
First you need to get the regular-expression pattern string. To parameterize the type provider, you pass a list of ProvidedStaticParameters containing the name and type of each parameter to the DefineStaticParameter method, along with an instantiation callback function, as shown in Example 5-9. At runtime, values the user supplies for each static parameter will be passed into the second argument of the callback function in the same order that they were defined.
let staticParams = [ProvidedStaticParameter("pattern", typeof<string>)] do newT.DefineStaticParameters( parameters=staticParams, instantiationFunction=(fun typeName parameterValues -> ... . ))
Therefore, when using static parameters, the creation of additional types and members must be performed inside the callback function. Consequently, the type provider’s code structure changes as shown in Example 5-10. The only change from HelloWorldTypeProvider is that now the operation must be performed on the ty variable defined inside the instantiation function. The instantiation function must return a new type. If RegexTypeProvider can be viewed as a generic type and the regular-expression pattern string as a type parameter, the type returned from the instantiation function is a new type after applying the type parameter. In Example 5-8, the type T was the new type returned from the instantiation function. This new type is the target to which you attach properties and methods.
let staticParams = [ProvidedStaticParameter("pattern", typeof<string>)] do newT.DefineStaticParameters( parameters=staticParams, instantiationFunction=(fun typeName parameterValues -> match parameterValues with | [| :? string as pattern|] -> let ty = ProvidedTypeDefinition( thisAssembly, rootNamespace, typeName, Some baseTy, HideObjectMethods = true) let ctor = ProvidedConstructor( parameters = [], InvokeCode = fun args -> <@@ (* base class initialization or null*) () @@>) ty.AddMember(ctor) ty ))
The HideObjectMethods property determines whether the object method, such as ToString, can be shown in IntelliSense.
To make the IsMatch static method work, a few problems need to be solved:
How to define a provided function with a parameter
How to get parameter values inside of the provided method
In Example 5-11, function parameters are defined using ProvidedParameter, which requires a parameter name and parameter type. The parameter passed into the function is named args, and it is of type Expr list. The way to get the value out of the Expr list is to use a quotation splicing operator (%%) and specify its type. Because the function is a static method, the first element in the args list is the parameter you want. If the method is not a static method, %%args.[0]
is the pointer to the instance.
let m = ProvidedMethod( methodName = "IsMatch", parameters = [ ProvidedParameter("input", typeof<string>) ], returnType = typeof<bool>, IsStaticMethod = true, InvokeCode = fun args -> <@@ System.Text.RegularExpressions.Regex.IsMatch((%%args.[0]:string), pattern) @@>) ty.AddMember(m)
Both % and %% are quotation splicing operators. Their job is to enable a user to insert a code expression object into a quotation. The difference between % and %% is that % inserts a typed expression but %% inserts an untyped expression. In the code sample just shown, args is an F# expression, and the %% operator converts the expression into a quotation.
The last line in Example 5-8 returns a type that has an AreaCode property, which is a group name defined in the regular-expression pattern string. As a result, a new type is needed to host this property, and it can be returned from the Match method. (See Example 5-12.) The new type is named matchTy. It is created and used as the return type for matchMethod. The group names are retrieved, and the AreaCode property is added to matchTy. The property assumes its parent object is System.Text.RegularExpressions.Match type.
let matchTy = ProvidedTypeDefinition("Match", baseType = Some baseTy, HideObjectMethods = true) for group in r.GetGroupNames() do // ignore the group named 0, which represents all input if group <> "0" then let prop = ProvidedProperty( propertyName = group, propertyType = typeof<System.Text.RegularExpressions.Group>, GetterCode = fun args -> <@@ ((%%args.[0]:obj):?>System.Text.RegularExpressions.Match).Groups. [group] @@>) matchTy.AddMember prop let matchMethod = ProvidedMethod( methodName = "Match", parameters = [ProvidedParameter("input", typeof<string>)], returnType = matchTy, InvokeCode = fun args -> <@@ ((%%args.[0]:obj):?>System.Text.RegularExpressions.Regex).Match(%%args. [1]):>obj @@>) ty.AddMember(matchMethod) ty.AddMember(matchTy)
A little extra code is needed in the matchTy constructor, because the AreaCode property needs its calling object to be a Match type. Example 5-13 shows the necessary code additions.
let ctor = ProvidedConstructor( parameters = [], InvokeCode = fun args -> <@@ System.Text.RegularExpressions.Regex(pattern) :> obj @@>) ty.AddMember(ctor)
Before presenting the complete code for the regular expression type provider, there is one thing I need to clarify. The code ((%%args.[0]:obj) :?> System.Text.RegularExpressions.Match)
in Example 5-12 might seem odd when you review the value type that is then converted to a Match type. The reason this is needed is that the matchTy type needs to have a base type of System.Object. Although you could use a base type of System.Text.RegularExpression.Match, and consequently avoid the conversion, the end-user experience would be diminished by this. The problem with using System.Text.RegularExpression is that viewing Match via IntelliSense shows all the built-in members from the Match type. This makes the generated AreaCode property more difficult to find. Figure 5-5 shows a side-by-side comparison that illustrates this issue. The complete code for the regular-expression type provider is shown in Example 5-14.
// Learn more about F# at http://fsharp.net // See the 'F# Tutorial' project for more help. namespace Samples.FSharp.RegularExpressionTypeProvider open System.Reflection open Microsoft.FSharp.Core.CompilerServices open Samples.FSharp.ProvidedTypes [<TypeProvider>] type public TypeProvider1() as this = inherit TypeProviderForNamespaces() // Get the assembly and namespace used to house the provided types let thisAssembly = Assembly.GetExecutingAssembly() let rootNamespace = "Samples.ShareInfo.TPTest" let baseTy = typeof<obj> let newT = ProvidedTypeDefinition(thisAssembly, rootNamespace, "RegularExpressionTypeProvider", Some baseTy) let staticParams = [ProvidedStaticParameter("pattern", typeof<string>)] do newT.DefineStaticParameters( parameters=staticParams, instantiationFunction=(fun typeName parameterValues -> match parameterValues with | [| :? string as pattern|] -> let ty = ProvidedTypeDefinition( thisAssembly, rootNamespace, typeName, Some baseTy, HideObjectMethods = true) let r = System.Text.RegularExpressions.Regex(pattern) let m = ProvidedMethod( methodName = "IsMatch", parameters = [ProvidedParameter("input", typeof<string>)], returnType = typeof<bool>, IsStaticMethod = true, InvokeCode = fun args -> <@@ System.Text.RegularExpressions.Regex.IsMatch( (%%args.[0]:string), pattern) @@> ) ty.AddMember(m) let ctor = ProvidedConstructor( parameters = [], InvokeCode = fun args -> <@@ System.Text.RegularExpressions.Regex(pattern) :> obj @@>) ty.AddMember(ctor) let matchTy = ProvidedTypeDefinition("Match", baseType = Some baseTy, HideObjectMethods = true) for group in r.GetGroupNames() do // ignore the group named 0, which represents all input if group <> "0" then let prop = ProvidedProperty( propertyName = group, propertyType = typeof<System.Text.RegularExpressions. Group> GetterCode = fun args -> <@@ ((%%args.[0]:obj) :?>System.Text.RegularExpressions.Match).Groups. [group] @@>) matchTy.AddMember prop let matchMethod = ProvidedMethod( methodName = "Match", parameters = [ProvidedParameter("input", typeof<string>)], returnType = matchTy, InvokeCode = fun args -> <@@ ((%%args.[0]:obj) :?>System.Text.RegularExpressions.Regex) .Match(%%args.[1]) :>obj @@>) ty.AddMember(matchMethod) ty.AddMember(matchTy) ty )) do this.AddNamespace(rootNamespace, [newT]) [<TypeProviderAssembly>] do ()
The regular-expression type provider uses a static parameter to provide the type information. The static parameter can also point to a file that contains the type information. The current implementation allows a user to generate only .NET 1.x-compatible types. One exception is unit-of-measure types. Type providers do support the generation of unit-of-measure types. The CSV type provider demonstrates how to get type information from a local CSV file and how to generate unit-of-measure types.
As with the previous examples, the first step is to design how the API should look for the end user. (See Example 5-15.) The sample CSV file is shown in Example 5-16.
#r @".inDebugCSVTypeProvider.dll" type T = Samples.ShareInfo.TPTest.CSVTypeProvider<"TextFile1.csv"> let t = T() for row in t.Data do let time = row.Time printfn "%f" (float time)
The type information is stored in the first line of the CSV file. Each row, except the first row, also needs a type to hold all of the fields. ProvidedMeasureBuilder is needed when creating unit-of-measure types. Example 5-17 shows how to create kg, meter, and kg/m2.
let measures = ProvidedMeasureBuilder.Default // make the kilogram and meter let kg = measures.SI "Kilogram" let m = measures.SI "Meter" // make float<kg> type let float_kg = measures.AnnotateType(typeof<float>,[kg]) // make float<m2> type let kgpm2 = measures.Ratio(kg, measures.Square m) // make float<kg/m2> type let dkgpm2 = measures.AnnotateType(typeof<float>,[kgpm2]) // make Nullable<float< kg/m2>> type let nullableDecimal_kgpm2 = typedefof<System.Nullable<_>>.MakeGenericType [|dkgpm2 |]
Referencing a local file is an easy task as long as the user gives the correct file path. If a relative path is preferred, TypeProviderConfig can be used. The instance of a type provider can take an instance of a type named TypeProviderConfig during construction, which contains the resolution folder for the type provider, the list of referenced assemblies, and other information. Example 5-18 shows how to use the TypeProviderConfig value.
[<TypeProvider>] type public CsvProvider(cfg:TypeProviderConfig) as this = inherit TypeProviderForNamespaces() ... // Resolve the filename relative to the resolution folder. let resolvedFilename = System.IO.Path.Combine(cfg.ResolutionFolder, filename)
Now that the two problems are solved, the CSV type provider can be implemented as shown in Example 5-19. The structure of the code for the CSV type provider is similar to that of the regular-expression type provider. The Data property returns a CSV file line, which is represented as a Row.
// Learn more about F# at http://fsharp.net // See the 'F# Tutorial' project for more help. namespace Samples.FSharp.CSVTypeProvider open System.Reflection open Microsoft.FSharp.Core.CompilerServices open Samples.FSharp.ProvidedTypes open System.IO open System.Text.RegularExpressions module CsvFileModule = let data filename = [| for line in File.ReadAllLines(filename) |> Seq.skip 1 do yield line.Split(',') |> Array.map float |] [<TypeProvider>] type public CsvProvider(cfg:TypeProviderConfig) as this = inherit TypeProviderForNamespaces() // Get the assembly and namespace used to house the provided types. let asm = System.Reflection.Assembly.GetExecutingAssembly() let ns = "Samples.ShareInfo.TPTest" // Create the main provided type. let csvTy = ProvidedTypeDefinition(asm, ns, "CSVTypeProvider", Some(typeof<obj>)) // Parameterize the type by the file to use as a template. let filename = ProvidedStaticParameter("filename", typeof<string>) do csvTy.DefineStaticParameters([filename], fun tyName [| :? string as filename |] -> // Resolve the filename relative to the resolution folder. let resolvedFilename = Path.Combine(cfg.ResolutionFolder, filename) // Get the first line from the file. let headerLine = File.ReadLines(resolvedFilename) |> Seq.head // Define a provided type for each row, erasing to a float[]. let rowTy = ProvidedTypeDefinition("Row", Some(typeof<float[]>)) // Extract header names from the file, splitting on commas. // use Regex matching to get the position in the row at which the field occurs let headers = Regex.Matches(headerLine, "[^,]+") // Add one property per CSV field. for i in 0 .. headers.Count - 1 do let headerText = headers.[i].Value // Try to decompose this header into a name and unit. let fieldName, fieldTy = let m = Regex.Match(headerText, @"(?<field>.+) ((?<unit>.+))") if m.Success then let fieldName = m.Groups.["field"].Value let unitName = m.Groups.["unit"].Value let units = ProvidedMeasureBuilder.Default.SI unitName let fieldType = ProvidedMeasureBuilder.Default.AnnotateType(typeof<float>,[uni ts]) (fieldName, fieldType) else // no units, just treat it as a normal float headerText, typeof<float> let fieldProp = ProvidedProperty(fieldName, fieldTy, GetterCode = fun [row] -> <@@ (%%row:float[]).[i] @@>) // Add metadata that defines the property's location in the referenced file. fieldProp.AddDefinitionLocation(1, headers.[i].Index + 1, filename) rowTy.AddMember fieldProp // Define the provided type let ty = ProvidedTypeDefinition(asm, ns, tyName, Some(typeof<obj>)) // Add a parameterless constructor that loads the file that was used to define the schema. let ctor0 = ProvidedConstructor([], InvokeCode = fun _ -> <@@ () @@>) // Add a more strongly typed Data property, which uses the existing property at runtime. let prop = ProvidedProperty("Data", typedefof<seq<_>>.MakeGenericType(rowTy), GetterCode = fun _ -> <@@ CsvFileModule.data resolvedFilename @@>) ty.AddMember prop ty.AddMember ctor0 // Add the row type as a nested type. ty.AddMember rowTy ty) // Add the type to the namespace. do this.AddNamespace(ns, [csvTy]) [<TypeProviderAssembly>] do ()
Another approach is to use a user-defined CSV class to process the file and return a list of data. The type provider’s job is to provide unit-of-measure types and extra class members to the design-time IntelliSense. The CSV class and type provider code are shown in Example 5-20. The CSV file’s first line is parsed by using a regular expression; the column name, which also contains the unit-of-measure types, is retrieved and generated. The CSV class returns the Data property, and its content is passed into the generated type rowTy. The rowTy type provides the unit-of-measure type and named property for each data item.
// Learn more about F# at http://fsharp.net // See the 'F# Tutorial' project for more help. namespace Samples.FSharp.CSVTypeProvider open System.Reflection open Microsoft.FSharp.Core.CompilerServices open Samples.FSharp.ProvidedTypes open System.IO open System.Text.RegularExpressions // CSV file class type CsvFile(filename) = let data = [| for line in File.ReadAllLines(filename) |> Seq.skip 1 do yield line.Split(',') |> Array.map float |] member __.Data = data // CSV file type provider code with unit-of-measure support [<TypeProvider>] type public CsvProvider(cfg:TypeProviderConfig) as this = inherit TypeProviderForNamespaces() // Get the assembly and namespace used to house the provided types. let asm = System.Reflection.Assembly.GetExecutingAssembly() let ns = "Samples.ShareInfo.TPTest" // Create the main provided type. let csvTy = ProvidedTypeDefinition(asm, ns, "CSVTypeProvider", Some(typeof<obj>)) // Parameterize the type by the file to use as a template. let filename = ProvidedStaticParameter("filename", typeof<string>) do csvTy.DefineStaticParameters([filename], fun tyName [| :? string as filename |] -> // Resolve the filename relative to the resolution folder. let resolvedFilename = Path.Combine(cfg.ResolutionFolder, filename) // Get the first line from the file. let headerLine = File.ReadLines(resolvedFilename) |> Seq.head // Define a provided type for each row, erasing to a float[]. let rowTy = ProvidedTypeDefinition("Row", Some(typeof<float[]>)) // Extract header names from the file and use // Regex to get the position in the row at which the field occurs let headers = Regex.Matches(headerLine, "[^,]+") // One property per CSV field. for i in 0 .. headers.Count - 1 do let headerText = headers.[i].Value // Try to decompose this header into a name and unit. let fieldName, fieldTy = let m = Regex.Match(headerText, @"(?<field>.+) ((?<unit>.+))") if m.Success then let fieldName = m.Groups.["field"].Value let unitName = m.Groups.["unit"].Value let units = ProvidedMeasureBuilder.Default.SI unitName let fieldType = ProvidedMeasureBuilder.Default.AnnotateType(typeof<float>,[units]) (fieldName, fieldType) else // no units, just treat it as a normal float headerText, typeof<float> let fieldProp = ProvidedProperty(fieldName, fieldTy, GetterCode = fun [row] -> <@@ (%%row:float[]).[i] @@>) // Add metadata that defines the property's location in the referenced file. fieldProp.AddDefinitionLocation(1, headers.[i].Index + 1, filename) rowTy.AddMember fieldProp // Define the provided type let ty = ProvidedTypeDefinition(asm, ns, tyName, Some(typeof<CsvFile>)) // Add a parameterless constructor that loads the file that was used to define the schema. let ctor0 = ProvidedConstructor([], InvokeCode = fun [] -> <@@ CsvFile(resolvedFilename) @@>) // Add a more strongly typed Data property, which uses the existing property at runtime. let prop = ProvidedProperty("Data", typedefof<seq<_>>.MakeGenericType(rowTy), GetterCode = fun [csvFile] -> <@@ (%%csvFile:CsvFile). Data @@>) ty.AddMember prop ty.AddMember ctor0 // Add the row type as a nested type. ty.AddMember rowTy ty) // Add the type to the namespace. do this.AddNamespace(ns, [csvTy]) [<TypeProviderAssembly>] do ()
The csvFile
value in the fun [csvFile] -> <@@ (%%csvFile:CsvFile).Data @@>)
uses an active pattern to decompose the arguments into a single element list. This code could also be written as fun args -> <@@ ((%%args.[0]):CsvFile).Data @@>)
.
Two interesting concepts have been introduced in the implementation of this type provider. The type-provider type and provided class members are derived from the schema information, which defines the meaning of the data. The regular-expression pattern and CSV file header in the schema defines how the data is presented. Any schematized data can potentially have a type provider that helps to interpret and process the data. A few examples include image data and network transmission data.
Starting with Excel 2007, a new file format with extension XLSX was introduced. This file format is the Office Open XML File format, which is an open international, ECMA-376, Second Edition, and ISO/IEC 29500 standard. This new format is an XML-format file that can be accessed by using the Open XML SDK. The Open XML SDK can be downloaded from http://www.microsoft.com/en-us/download/details.aspx?id=5124. After the SDK is installed successfully, DocumentFormat.OpenXml.dll is installed under the C:Program FilesOpen XML SDK folder.
The sample Excel file Book1.xlsx has three cells. The C1 cell value is computed by adding the values in A1 and B1. The screen shot is shown in Figure 5-6. This file needs to be put in the project folder where the type-provider project file is located.
Example 5-21 creates an ExcelFile class to access the XLSX file using the Open XML SDK. To run this code, DocumentFormat.OpenXml.dll, System.Xml.dll, and WindowsBase.dll have to be added to the project reference.
module ExcelClass open DocumentFormat.OpenXml.Packaging open DocumentFormat.OpenXml.Spreadsheet open DocumentFormat.OpenXml type CellContent = { Name: string* string; Value : string; Formula : string; Cell : Cell} with override this.ToString() = sprintf "%s%s(%s, %s)" (fst this.Name) (snd this.Name) this.Value this.Formula let (|CellName|) (name:string) = let filterBy f = name |> f System.Char.IsLetter |> Seq.toArray |> fun l -> System.String l let col = filterBy Seq.takeWhile let row = filterBy Seq.skipWhile (col, row) type ExcelFile(filename:string, editable:bool) = let myWorkbook = SpreadsheetDocument.Open(filename, editable) let part = myWorkbook.WorkbookPart let cells = part.Workbook.Descendants<Sheet>() |> Seq.map (fun sheet -> let a = part.GetPartById sheet.Id.Value let c = match a with | :? WorksheetPart as part -> let rows = part.Worksheet.Descendants<Row>() [ for row in rows do for cell in row.Descendants<Cell>() do let formula = if cell.CellFormula = null then "" else cell.CellFormula.Text match cell.CellReference.Value with | CellName(col,row) -> yield { Cell = cell Name = col, row Formula = formula Value = cell.CellValue.Text } ] | _ -> [ ] (sheet.Name.Value, c)) |> Seq.map snd |> Seq.collect id |> Seq.toList member this.Cells = cells member this.Cell(col,row) = cells |> Seq.tryFind (fun cell->cell.Name=(col,row)) member this.Cell(col,row, v) = let cell = this.Cell(col,row) match cell with | Some c -> if c.Cell.CellFormula = null then c.Cell.CellValue.Text <- v | None -> () member this.Close() = myWorkbook.Close()
The type-provider script, presented in Example 5-22, shows what class members are exposed to the end user. The code opens Book1.xlsx and reads the A1 cell content first. Then the A1 content is changed to 99.
#r @".inDebugTypeProviderTemplate2.dll" type T = Samples.Excel.ExcelTypeProvider<"Book1.xlsx"> let t = T() let (Some(cell)) = t.GetCell(t.A, t.''1'') printfn "%A" cell.Value t.SetCell(t.A, t.''1'', "99") t.Close() let t2 = T() let (Some(cell2)) = t2.GetCell(t2.A, t2.''1'') printfn "%A" cell2.Value t2.Close()
The Close method needs to be invoked; otherwise, the script will generate an error after it is executed once. The error message is “Cannot access Book1.xlsx because it is opened by another process.”
The type provider code, shown in Example 5-23, provides several methods. It uses System.Object as a base class to hide the ExcelFile members. It also provides Excel columns and row names as read-only properties.
// Learn more about F# at http://fsharp.net // See the 'F# Tutorial' project for more help. namespace Samples.FSharp.TypeProviderTemplate2 open System.Reflection open Microsoft.FSharp.Core.CompilerServices open Samples.FSharp.ProvidedTypes open ExcelClass [<TypeProvider>] type public TypeProvider1(cfg:TypeProviderConfig) as this = inherit TypeProviderForNamespaces() // Get the assembly and namespace used to house the provided types let thisAssembly = Assembly.GetExecutingAssembly() let rootNamespace = "Samples.Excel" let baseTy = typeof<obj> let newT = ProvidedTypeDefinition( thisAssembly, rootNamespace, "ExcelTypeProvider", Some baseTy) let filename = ProvidedStaticParameter("filename", typeof<string>) do newT.DefineStaticParameters( [filename], fun tyName [| :? string as filename |] -> let path = System.IO.Path.Combine (cfg.ResolutionFolder, filename) let ty = ProvidedTypeDefinition( thisAssembly, rootNamespace, tyName, Some(typeof<obj>)) ty.AddMember( ProvidedConstructor([], InvokeCode = fun _ -> <@@ ExcelFile(path, true) @@>)) let file = ExcelFile(path, false) let rows = file.Cells |> Seq.map (fun cell -> cell.Name) |> Seq.map (fun (col,row) -> row) |> Seq.distinct let cols = file.Cells |> Seq.map (fun cell -> cell.Name) |> Seq.map (fun (col,row) -> col) |> Seq.distinct rows |> Seq.append cols |> Seq.iter (fun n -> ty.AddMember( ProvidedProperty( n, typeof<string>, GetterCode= fun _ -> <@@ n @@>))) file.Close() let mi = ProvidedMethod( "GetCell", [ProvidedParameter("row", typeof<string>); ProvidedParameter("col", typeof<string>)], typeof<CellContent option>, InvokeCode = fun [me; row; col] -> <@@ ((%%me:obj):?>ExcelFile).Cell((%%row:string), (%%col:string)) @@>) let close = ProvidedMethod( "Close", [], typeof<unit>, InvokeCode = fun [me] -> <@@ ((%%me:obj) :?> ExcelFile).Close() @@>) let save = ProvidedMethod( "SetCell", [ProvidedParameter("row", typeof<string>) ProvidedParameter("col", typeof<string>) ProvidedParameter("value", typeof<string>)], typeof<unit>, InvokeCode = fun [me; row; col; v] -> <@@ ((%%me:obj) :?> ExcelFile). Cell((%%row:string), (%%col:string), (%%v:string)) @@>) ty.AddMember(mi) ty.AddMember(save) ty.AddMember(close) ty) do this.AddNamespace(rootNamespace, [newT]) [<TypeProviderAssembly>] do ()
Until now, there has been no way to share information or coordinate the actions between class methods within a type provider. The type-provider methods do not show relationships when invoked. The interesting part about Example 5-24 is that it stores the file name in the base class and enables methods to share that information between method calls.
A simple example is shown in Example 5-24. The type-provider base provides a non-object-based property that can be used to store information that can be shared between method calls. The type provider defines two methods, and these two provided methods provide a wrapper around this existing property. The code is very close to the HelloWorld type provider except for the base class.
One thing that needs to be called out is the constructor. Because the provided type is erased to the base type, the constructor needs to return the base type instance instead of nothing. Example 5-25 shows how to invoke the type provider. The F1 function sets the underlying base class value and the F2 function can retrieve it. This code seems to do nothing, but it opens the door to a more interesting scenario, which will be presented in the next section.
namespace Samples.FSharp.ShareInfoProvider
open System.Reflection
open Microsoft.FSharp.Core.CompilerServices
open Samples.FSharp.ProvidedTypes
open System.IO
open System.Collections.Generic
// base class the type provider erased to
type BaseType2() =
member val X = "" with get, set
[<TypeProvider>]
type public CheckedRegexProvider() as this =
inherit TypeProviderForNamespaces()
// Get the assembly and namespace used to house the provided types
let thisAssembly = Assembly.GetExecutingAssembly()
let rootNamespace = "Samples.ShareInfo.TPTest"
let baseTy = typeof<BaseType2>
let regexTy = ProvidedTypeDefinition(thisAssembly, rootNamespace, "TPTestType", Some
baseTy)
let f1 = ProvidedMethod(
methodName = "F1",
parameters = [ProvidedParameter("input", typeof<string>)],
returnType = typeof<unit>,
IsStaticMethod = false,
InvokeCode = fun args -> <@@ (%%args.[0]:BaseType2).X <-
(%%args.[1]) @@>)
let f2 = ProvidedMethod(
methodName = "F2",
parameters = [],
returnType = typeof<string>,
IsStaticMethod = false,
InvokeCode = fun args -> <@@ (%%args.[0]:BaseType2).X @@>)
// constructor needs to return the BaseType2 instance
let ctor = ProvidedConstructor(
parameters = [],
InvokeCode = fun args -> <@@ BaseType2() @@>)
do
regexTy.AddMember ctor
regexTy.AddMember f1
regexTy.AddMember f2
do this.AddNamespace(rootNamespace, [regexTy])
[<TypeProviderAssembly>]
do ()
#r @".inDebugShareInfoSampleTypeProvider.dll" type T = Samples.ShareInfo.TPTest.TPTestType let t = T() t.F1("hello") let a = t.F2() printfn "%A" a // the print result is hello
Because the provided type is based on BaseType2 and because the constructor returns an instance of BaseType2, the variable t in the test script is of type BaseType2.
Imagine that you have a sealed class and need to add some logic for each member function, such as logging the function usage. The most common technique for a C# developer is to write a wrapper class with the sealed class instance as a variable, as shown in Example 5-26. It would be a tedious and boring task to refactor the sealed class. Reflection can be an option, but the performance impact of this approach is something that cannot be ignored.
// sealed class A public sealed class A { public void F1() { ... } } // new class to add logging function around the F1 defined in sealed class A public class MyClass { private A a = new A(); public void F1() { LogBefore(); a.F1(); LogAfter(); } }
A custom type provider is another way to perform meta-programming, and it has a performance advantage over reflection. The type provider logic is simple:
Generate the same provided method with the same function name and signature.
In the function implementation, first invoke the LogBefore method, then invoke the base class’ method, and finally invoke the LogAfter method.
As usual, the way to invoke the type provider is defined first, as you can see in Example 5-27. The sealed class and the logging function are defined in Example 5-28.
#r @".inDebugShareInfoSampleTypeProvider.dll" type T = Samples.ShareInfo.TPTest.TPTestType let t = T() //invoke F1 and also output logging info t.F1("hello") //invoke F2 and also output logging info t.F2(2)
log before Samples.FSharp.ShareInfoProvider.BaseType2 "F1" hello log after Samples.FSharp.ShareInfoProvider.BaseType2 "F1" log before Samples.FSharp.ShareInfoProvider.BaseType2 "F2" 2 log after Samples.FSharp.ShareInfoProvider.BaseType2 "F2"
[< Sealed >] type BaseType2() = member this.F1(s) = printfn "%s" s member this.F2(i) = printfn "%d" i // logging functions type LoggingFunctions = static member LogBeforeExecution(obj:BaseType2, methodName:string) = printfn "log before %A %A" obj methodName static member LogAfterExecution(obj:BaseType2, methodName:string) = printfn "log after %A %A" obj methodName
The next task is determining how to invoke the logging functions and base class method by using a quotation. The following two problems need to be solved, and the code that provides the solutions is shown in Example 5-29:
Invoke a method. The method call is performed by Expr.Call, which takes three parameters if the method is not a static method and only two parameters if it is a static method. The parameter to Expr.Call is created by using Expr.Value.
Invoke a sequence of statements. Invoking statements is handled by Expr.Sequential. It might seem like a problem that this only takes two elements, especially when more than two statements need to be invoked. Actually, this is not a problem at all. If the second parameter is an Expr.Sequential, it has an extra space to hold the following statement.
let baseTExpression = <@@ (%%args.[0]:BaseType2) @@> let mi = baseTy.GetMethod(methodName) // get LogBefore function code quotation let logExpr = Expr.Call(typeof<LoggingFunctions>.GetMethod("LogBeforeExecution"), [ baseTExpression; Expr.Value(methodName) ]) // invoke the base class's method let invokeExpr = Expr.Call(baseTExpression, mi, args.Tail) // get logAfter function code quotation let logAfterExpr = Expr.Call(typeof< LoggingFunctions >.GetMethod("LogAfterExecution"), [ baseTExpression; Expr.Value(methodName) ]) // invoke logBefore, base method, and logAfter Expr.Sequential(logExpr, Expr.Sequential(invokeExpr, logAfterExpr))
The complete code is presented in Example 5-30. Reflection is used to get the method names from the base class and to generate the provided methods. Because the reflection code is only executed at compile time (design time), the generated IL (which is shown in Example 5-31) does not contain any reflection code. This wrapper class has visibility to the base class’ public methods and properties, and the cost to generate and maintain it is significantly lower than the manual way shown in Example 5-26.
The wrapper type provider can work for cases in which source code is not available. The Activator.CreateInstance method is not something new for C# developers. The DLL path and type name can be passed in as a string, and CreateInstance can be used to create the sealed class instance.
namespace Samples.FSharp.ShareInfoProvider open System.Reflection open Microsoft.FSharp.Core.CompilerServices open Samples.FSharp.ProvidedTypes open System.IO open System.Collections.Generic open Microsoft.FSharp.Quotations [<Sealed>] type BaseType2() = member this.F1(s) = printfn "%s" s member this.F2(i) = printfn "%d" i type LoggingFunctions = static member LogBeforeExecution(obj:BaseType2, methodName:string) = printfn "log before %A %A" obj methodName static member LogAfterExecution(obj:BaseType2, methodName:string) = printfn "log after %A %A" obj methodName [<TypeProvider>] type public CheckedRegexProvider() as this = inherit TypeProviderForNamespaces() // Get the assembly and namespace used to house the provided types let thisAssembly = Assembly.GetExecutingAssembly() let rootNamespace = "Samples.ShareInfo.TPTest" let baseTy = typeof<BaseType2> let regexTy = ProvidedTypeDefinition(thisAssembly, rootNamespace, "TPTestType", Some baseTy) let methods = baseTy.GetMethods() let getParameters (mi:MethodInfo) = let parameters = mi.GetParameters() parameters |> Seq.map (fun p -> ProvidedParameter(p.Name, p.ParameterType)) |> Seq.toList let providedMethods = methods |> Seq.map (fun m ->let methodName = m.Name ProvidedMethod( methodName = methodName, parameters = (getParameters m), returnType = m.ReturnType, IsStaticMethod = false, InvokeCode = fun args -> let baseTExpression = <@@ (%%args.[0]:BaseType2) @@> let mi = baseTy.GetMethod(methodName) let logExpr = Expr.Call( typeof<LoggingFunctions> .GetMethod("LogBeforeExecution"), [baseTExpression; Expr. Value(methodName) ]) let invokeExpr = Expr.Call(baseTExpression, mi, args. Tail) let logAfterExpr = Expr.Call( typeof<LoggingFunctions> .GetMethod("LogAfterExecuti on"), [baseTExpression;Expr. Value(methodName)] ) Expr.Sequential( logExpr, Expr.Sequential(invokeExpr, logAfterExpr)) )) let ctor = ProvidedConstructor( parameters = [], InvokeCode = fun args -> <@@ BaseType2() @@>) do regexTy.AddMember ctor providedMethods |> Seq.iter regexTy.AddMember do this.AddNamespace(rootNamespace, [regexTy]) [<TypeProviderAssembly>] do ()
IL_0000: nop IL_0001: newobj instance void [ShareInfoSampleTypeProvider]Samples.FSharp.ShareInfoProvider.BaseType2::.ctor() IL_0006: box [ShareInfoSampleTypeProvider]Samples.FSharp.ShareInfoProvider.BaseType2 IL_000b: unbox.any [ShareInfoSampleTypeProvider]Samples.FSharp.ShareInfoProvider.BaseType2 IL_0010: dup IL_0011: stsfld class [ShareInfoSampleTypeProvider]Samples.FSharp.ShareInfoProvider.BaseType2 '<StartupCode$Cons oleApplication1>.$Program'::t@5 IL_0016: stloc.0 IL_0017: call class [ShareInfoSampleTypeProvider]Samples.FSharp.ShareInfoProvider. BaseType2 Program::get_t() IL_001c: ldstr "F1" IL_0021: call void [ShareInfoSampleTypeProvider]Samples.FSharp.ShareInfoProvider.InsertFunctions::LogBeforeEx ecution(class [ShareInfoSampleTypeProvider]Samples.FSharp.ShareInfoProvider.BaseType2, string) IL_0026: call class [ShareInfoSampleTypeProvider]Samples.FSharp.ShareInfoProvider. BaseType2 Program::get_t() IL_002b: ldstr "hello" IL_0030: callvirt instance void [ShareInfoSampleTypeProvider]Samples.FSharp.ShareInfoProvider.BaseType2::F1(string) IL_0035: call class [ShareInfoSampleTypeProvider]Samples.FSharp.ShareInfoProvider.BaseType2 Program::get_t() IL_003a: ldstr "F1" IL_003f: call void [ShareInfoSampleTypeProvider]Samples.FSharp.ShareInfoProvider.InsertFunctions::LogAfterExe cution(class [ShareInfoSampleTypeProvider]Samples.FSharp.ShareInfoProvider.BaseType2, string) IL_0044: call class [ShareInfoSampleTypeProvider]Samples.FSharp.ShareInfoProvider.BaseType2 Program::get_t() IL_0049: ldstr "F2" IL_004e: call void [ShareInfoSampleTypeProvider]Samples.FSharp.ShareInfoProvider.InsertFunctions::LogBeforeEx ecution(class [ShareInfoSampleTypeProvider]Samples.FSharp.ShareInfoProvider.BaseType2, string) IL_0053: call class [ShareInfoSampleTypeProvider]Samples.FSharp.ShareInfoProvider. BaseType2 Program::get_t() IL_0058: ldc.i4.2 IL_0059: callvirt instance void [ShareInfoSampleTypeProvider]Samples.FSharp.ShareInfoProvider.BaseType2::F2(int32) IL_005e: call class [ShareInfoSampleTypeProvider]Samples.FSharp.ShareInfoProvider. BaseType2 Program::get_t() IL_0063: ldstr "F2" IL_0068: call void [ShareInfoSampleTypeProvider]Samples.FSharp.ShareInfoProvider.InsertFunctions::LogAfterExe cution(class [ShareInfoSampleTypeProvider]Samples.FSharp.ShareInfoProvider.BaseType2, string) IL_006d: ret
At first glance, the wrapper type provider does not appear to derive its type and members from a schema. In this case, the schema is being defined by the data returned from TypeInfo and MethodInfo via reflection. From this sample, I can say that the base class is another way to get schema data in addition to static type parameters and local files. The type provider has the ability to rearrange the data, which are methods and properties, to its new inheritance location and insert customized code during the relocation. The next sample shows how to make an inheritance structure with a type provider.
With C#, the decision was made not to support multi-inheritance. Discussions about the advantages or disadvantages of multi-inheritance often open a can of worms. I am not going to pick a side in this discussion. Instead, this example attempts to demonstrate that a type provider can create a new inheritance hierarchy. Because the user has full control over how to generate the class members, she can decide how to handle any duplicated function names or other nasty problems. This example uses only unique function names and leaves the duplicated-function-name problem to the reader.
First assume C# classes need to be handled by the type provider. The C# class definition is presented in Example 5-32. The C# classes that are sealed make the traditional inheritance more difficult. Note that if the class is located in another assembly, you to make sure that the assembly can be found (as shown in Example 5-33) if the DLL is located next to the type provider DLL.
namespace ClassLibrary1 { public sealed class Class1 { public void F1() { Console.WriteLine("from Class1.F1"); } public void F2() { Console.WriteLine("from Class1.F2"); } } public sealed class Class2 { public void F11() { Console.WriteLine("from Class2.F11"); } public void F12() { Console.WriteLine("from Class2.F12"); } } }
[<TypeProvider>] type public CheckedRegexProvider(tpc : Microsoft.FSharp.Core.CompilerServices.TypeProviderConfig) as this = inherit TypeProviderForNamespaces() let handler = System.ResolveEventHandler(fun _ args -> let asmName = AssemblyName(args.Name) // assuming that we reference only dll files let expectedName = asmName.Name + ".dll" let expectedLocation = // we expect to find this assembly near the dll with type provider let d = System.IO.Path.GetDirectoryName(tpc.RuntimeAssembly) System.IO.Path.Combine(d, expectedName) if System.IO.File.Exists expectedLocation then Assembly.LoadFrom expectedLocation else null ) do System.AppDomain.CurrentDomain.add_AssemblyResolve handler interface System.IDisposable with member this.Dispose() = System.AppDomain.CurrentDomain.remove_AssemblyResolve handler
Example 5-34 shows how to use the type provider to generate the provided method whenever the user wants to. The straightforward way is to use a module to hold instances of C# classes and then create provided methods. From the test script, the inheritance is not obvious, but compared to the next example, this approach is more straightforward.
namespace Samples.FSharp.ShareInfoProvider open System.Reflection open Microsoft.FSharp.Core.CompilerServices open Samples.FSharp.ProvidedTypes open System.IO open System.Collections.Generic open Microsoft.FSharp.Quotations open ClassLibrary1 [<AutoOpen>] module Module = let c1 = Class1() let c2 = Class2() [<TypeProvider>] type public CheckedRegexProvider(tpc : Microsoft.FSharp.Core.CompilerServices.TypeProviderConfig) as this = inherit TypeProviderForNamespaces() let handler = System.ResolveEventHandler(fun _ args -> let asmName = AssemblyName(args.Name) // assuming that we reference only dll files let expectedName = asmName.Name + ".dll" let expectedLocation = // we expect to find this assembly near the dll with type provider let d = System.IO.Path.GetDirectoryName(tpc.RuntimeAssembly) System.IO.Path.Combine(d, expectedName) if System.IO.File.Exists expectedLocation then Assembly.LoadFrom expectedLocation else null ) do System.AppDomain.CurrentDomain.add_AssemblyResolve handler // Get the assembly and namespace used to house the provided types let thisAssembly = Assembly.GetExecutingAssembly() let rootNamespace = "Samples.ShareInfo.TPTest" let baseTy = typeof<obj> let regexTy = ProvidedTypeDefinition( thisAssembly, rootNamespace, "MultiInheritanceTypeProvider", Some baseTy) let getParameters (mi:MethodInfo) = let parameters = mi.GetParameters() parameters |> Seq.map (fun p -> ProvidedParameter(p.Name, p.ParameterType)) |> Seq.toList let methodsFromC1 = c1.GetType().GetMethods() let providedMethodsFromC1 = methodsFromC1 |> Seq.map (fun m ->let methodName = m.Name ProvidedMethod( methodName = methodName, parameters = (getParameters m), returnType = m.ReturnType, IsStaticMethod = false, InvokeCode = fun args -> let baseTExpression = <@@ c1 @@> let mi = c1.GetType().GetMethod(methodName) let invokeExpr = Expr.Call(baseTExpression, mi, args.Tail) invokeExpr )) let methodsFromC2 = c2.GetType().GetMethods() let providedMethodsFromC2 = methodsFromC2 |> Seq.map (fun m ->let methodName = m.Name ProvidedMethod( methodName = methodName, parameters = (getParameters m), returnType = m.ReturnType, IsStaticMethod = false, InvokeCode = fun args -> let baseTExpression = <@@ c2 @@> let mi = c2.GetType().GetMethod(methodName) let invokeExpr = Expr.Call(baseTExpression, mi, args.Tail) invokeExpr )) let ctor = ProvidedConstructor( parameters = [], InvokeCode = fun args -> <@@ System.Object() @@>) do regexTy.AddMember ctor providedMethodsFromC1 |> Seq.iter regexTy.AddMember providedMethodsFromC2 |> Seq.iter regexTy.AddMember do this.AddNamespace(rootNamespace, [regexTy]) interface System.IDisposable with member this.Dispose() = System.AppDomain.CurrentDomain.remove_AssemblyResolve handler [<TypeProviderAssembly>] do ()
#r @".inDebugShareInfoSampleTypeProvider.dll" #r @".inDebugClassLibrary1.dll" type T = Samples.ShareInfo.TPTest.MultiInheritanceTypeProvider let t = T() t.F1() t.F2() t.F11() t.F12()
If the class does not have complex constructors and creating an object is straightforward, you can use the static parameter and Activator.CreateInstance to create the instance and host it in the base class. Because the static parameter passed into the type provider can only be a basic type such as int and string, this approach is not the best choice for the object that needs a non-basic type parameter. The base class uses a dictionary that creates a relationship between the type name and type instance, as shown in Example 5-35. This version of a type provider takes a DLL file path and two type names, which are used to create the instance for that type. This version is more general, but it requires that the instance creation be simple.
type MyBase(dll:string, names:string array) = let assembly = Assembly.LoadFile(dll) let myDict = names |> Array.map (fun name -> name, System.Activator.CreateInstance(System.Type. GetType(name))) |> dict member this.GetObj(name:string) = myDict.[name] member this.GetMethods(name) = myDict.[name].GetType().GetMethods() member this.GetMethod(name, methodName) = myDict.[name].GetType().GetMethod(methodName)
namespace Samples.FSharp.ShareInfoProvider open System.Reflection open Microsoft.FSharp.Core.CompilerServices open Samples.FSharp.ProvidedTypes open System.IO open System.Collections.Generic open Microsoft.FSharp.Quotations open ClassLibrary1 type MyBase(dll:string, names:string array) = let assembly = Assembly.LoadFile(dll) let myDict = names |> Array.map (fun name -> name, System.Activator.CreateInstance(System.Type. GetType(name))) |> dict member this.GetObj(name:string) = myDict.[name] member this.GetMethods(name) = myDict.[name].GetType().GetMethods() member this.GetMethod(name, methodName) = myDict.[name].GetType().GetMethod(methodName) [<TypeProvider>] type public CheckedRegexProvider(tpc : Microsoft.FSharp.Core.CompilerServices.TypeProviderConfig) as this = inherit TypeProviderForNamespaces() let handler = System.ResolveEventHandler(fun _ args -> let asmName = AssemblyName(args.Name) // assuming that we reference only dll files let expectedName = asmName.Name + ".dll" let expectedLocation = // we expect to find this assembly near the dll with type provider let d = System.IO.Path.GetDirectoryName(tpc.RuntimeAssembly) System.IO.Path.Combine(d, expectedName) if System.IO.File.Exists expectedLocation then Assembly.LoadFrom expectedLocation else null ) do System.AppDomain.CurrentDomain.add_AssemblyResolve handler // Get the assembly and namespace used to house the provided types let thisAssembly = Assembly.GetExecutingAssembly() let rootNamespace = "Samples.ShareInfo.TPTest" let baseTy = typeof<obj> let regexTy = ProvidedTypeDefinition(thisAssembly, rootNamespace, "MultiInheritanceTypeProvider", Some baseTy) let getParameters (mi:MethodInfo) = let parameters = mi.GetParameters() parameters |> Seq.map (fun p -> ProvidedParameter(p.Name, p.ParameterType)) |> Seq.toList let staticParams = [ProvidedStaticParameter("dll", typeof<string>); ProvidedStaticParameter("type", typeof<string>); ProvidedStaticParameter("type2", typeof<string>)] do regexTy.DefineStaticParameters( parameters=staticParams, instantiationFunction=(fun typeName parameterValues -> let name = parameterValues.[1] :?> string; let name2 = parameterValues.[2] :?> string; let dll = System.IO.Path.Combine( System.IO.Path.GetDirectoryName(tpc.RuntimeAssembly), parameterValues.[0] :?> string) let r = MyBase(dll, [| name; name2 |]) let ty = ProvidedTypeDefinition( thisAssembly, rootNamespace, typeName, Some typeof<obj>, HideObjectMethods = true) [| name; name2 |] |> Seq.iter (fun n -> let methods = r.GetMethods(n) let providedMethods = methods |> Seq.map (fun m ->let methodName = m.Name ProvidedMethod( methodName = methodName, parameters = (getParameters m), returnType = m.ReturnType, IsStaticMethod = false, InvokeCode = fun args -> let baseValue = r let baseTExpression = <@@ ((%%args.[0]:obj) :?> MyBase) .GetObj(n) @@> let mi = baseValue.GetObj(n) .GetType() .GetMethod(methodName) let t = System.Type.GetType(n) let invokeExpr = Expr.Call(Expr. Coerce(baseTExpression, t), mi, args.Tail) invokeExpr )) providedMethods |> Seq.iter ty.AddMember) let ctor = ProvidedConstructor( parameters = [], InvokeCode = fun args -> <@@ MyBase(dll, [| name; name2 |]) :> obj @@>) ty.AddMember(ctor) ty )) let ctor = ProvidedConstructor(parameters = [], InvokeCode = fun args -> <@@ System.Object() @@>) do regexTy.AddMember ctor do this.AddNamespace(rootNamespace, [regexTy]) interface System.IDisposable with member this.Dispose() = System.AppDomain.CurrentDomain.remove_AssemblyResolve handler [<TypeProviderAssembly>] do ()
Test script
#r @".inDebugShareInfoSampleTypeProvider.dll" #r @".inDebugClassLibrary1.dll" type T = Samples.ShareInfo.TPTest.MultiInheritanceTypeProvider< "ClassLibrary1.dll", "ClassLibrary1.Class1, ClassLibrary1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null", "ClassLibrary1.Class2, ClassLibrary1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"> let t = T() t.F1() t.F2() t.F11() t.F12()
Because the dictionary returns a System.Object type, and because it needs to be converted to the correct type, the conversion code Expr.Coerce(baseTExpression, t)
is required.
The wrapper and multi-inheritance type provider sample shows how a type provider can be used for meta-programming. The data passed into the type provider can originate from a base class or module inside the type-provider code in addition to the static parameter, local files, or both.
This section shows you how to use the XML type provider’s design-time feature. The definition of the type provider shows that it is a design-time adapter component. The example I present in this section explores the design-time part of the definition. Many applications use XML files as feeders of input data. The problem with this approach is that XML can become complicated and errors in this text file can be checked only at run time. The XML type provider moves validation that is typically done during run time to design time.
InvokeCode in ProvidedMethod returns a code quotation, and it can also contain other code. The quotation will be executed at run time, whereas the other code is executed at design time. This is where the validation code can be inserted and executed.
The production code and the sample XML file are shown in Example 5-37 and Example 5-38, respectively. Both listings require that the XML file meet certain criteria: the start value must be equal or smaller than the end value. If the validation can happen only at run time, that could be a disaster. Another approach is to use a validation tool, such as a check-in gate validation. None of these approaches can provide a comparable coding experience if this validation can be performed at design time. Ideally, when a user invokes the method, he will see the error right away.
type MyProductionCode() = member this.Work() = let xml = XDocument.Load("XmlFile1.xml") let start = xml.Descendants(XName.Get("Start")) |> Seq.head let end' = xml.Descendants(XName.Get("End")) |> Seq.head // if start > end, something bad will happen if Convert.ToInt32(start.Value) > Convert.ToInt32(end'.Value) then failwith "report illegal parameter from runtime"
<?xml version="1.0" encoding="utf-8" ?> <Config> <Start>6</Start> <End>5</End> </Config>
As usual, the test script is defined first. (See Example 5-39.) If the XML file is invalid, the error will be shown in Visual Studio, as you can see in Figure 5-7. The developer will know immediately that the configuration file is invalid.
#r @".inDebugXMLTypeProvider.dll" type T = Samples.ShareInfo.TPTest.XMLTypeProvider<"XmlFile1.xml"> let t = T() let a = t.Work()
The beauty of this type provider is that the validation code will be executed only at design time. Unlike the test case that executed regardless of whether the function was invoked, the final binary allows the function to be invoked only if the validation passes. To recap, the type provider moves the run-time failure check to design time. The completed code is shown in Example 5-40.
// Learn more about F# at http://fsharp.net // See the 'F# Tutorial' project for more help. namespace Samples.FSharp.XMLTypeProvider open System.Reflection open Microsoft.FSharp.Core.CompilerServices open Samples.FSharp.ProvidedTypes open System.Xml.Linq open Microsoft.FSharp.Quotations open System type MyProductionCode() = member this.Work() = let xml = XDocument.Load("XmlFile1.xml") let start = xml.Descendants(XName.Get("Start")) |> Seq.head let end' = xml.Descendants(XName.Get("End")) |> Seq.head // if start > end, something bad will happen if Convert.ToInt32(start.Value) > Convert.ToInt32(end'.Value) then failwith "report illegal parameter from runtime" type MyTest() = member this.Test1(xml:string) = let xml = XDocument.Load(xml) let start = xml.Descendants(XName.Get("Start")) |> Seq.head let end' = xml.Descendants(XName.Get("End")) |> Seq.head if Convert.ToInt32(start.Value) > Convert.ToInt32(end'.Value) then failwith "report illegal parameter from design-time" [<AutoOpen>] module TestModule = let test = MyTest() [<TypeProvider>] type public TypeProvider1(cfg : Microsoft.FSharp.Core.CompilerServices.TypeProviderConfig) as this = inherit TypeProviderForNamespaces() // Get the assembly and namespace used to house the provided types let thisAssembly = Assembly.GetExecutingAssembly() let rootNamespace = "Samples.ShareInfo.TPTest" let baseTy = typeof<obj> let newT = ProvidedTypeDefinition(thisAssembly, rootNamespace, "XMLTypeProvider", Some baseTy) let ctor = ProvidedConstructor( parameters = [], InvokeCode = fun args -> <@@ (* base class initialization or null*) () @@>) let staticParams = [ProvidedStaticParameter("xml file name", typeof<string>)] do newT.DefineStaticParameters( parameters = staticParams, instantiationFunction=(fun typeName parameterValues -> let baseTy = typeof<MyProductionCode> let ty = ProvidedTypeDefinition( thisAssembly, rootNamespace, typeName, baseType = Some baseTy) let xmlFileName = System.IO.Path.Combine( cfg.ResolutionFolder, parameterValues.[0] :?> string) let m = ProvidedMethod( methodName = "Work", parameters = [], returnType = typeof<unit>, IsStaticMethod = false, InvokeCode = fun args -> test.Test1(xmlFileName) <@@ (%%args.[0]:MyProductionCode).Work() @@> ) let ctor = ProvidedConstructor( parameters = [], InvokeCode = fun args -> <@@ MyProductionCode() @@>) ty.AddMember(ctor) ty.AddMember(m) ty.AddXmlDoc "xml comment" ty)) do newT.AddMember(ctor) do this.AddNamespace(rootNamespace, [newT]) [<TypeProviderAssembly>] do ()
The type provider gives the user a place to design his own design-time logic. You can use type providers to better take advantage of the editing power of Visual Studio without getting into the details of the Visual Studio SDK.
Directed Graph Markup Language (DGML) is supported in Visual Studio. DGML is used to draw a diagram representing a graph structure that contains nodes and edges. You can use DGML to represent a state machine, where the node is the state and the edge is the possible transition between states. Figure 5-8 illustrates how the DGML is shown in Visual Studio. It represents a state machine with four states: State0 to State3. The DGML file is actually an XML file. Example 5-41 shows the content of the DGML file that is shown in Figure 5-8. The Nodes section defines all the nodes in the graph, and the Links section defines all the edges.
<?xml version="1.0" encoding="utf-8"?> <DirectedGraph GraphDirection="LeftToRight" Layout="Sugiyama" xmlns="http://schemas.microsoft.com/vs/2009/dgml"> <Nodes> <Node Id="State0" Bounds="0,19.02,53.36,25.96" /> <Node Id="State1" Bounds="170.095,93.0200000000001,53.36,25.96" /> <Node Id="State2" Bounds="85.0474999999999,93.02,53.36,25.96" /> <Node Id="State3" Bounds="255.1425,93.02,53.3600000000001,25.96" /> </Nodes> <Links> <Link Source="State0" Target="State1" Bounds="53.3600006103516,32,131.471939086914,53.2166442871094" /> <Link Source="State0" Target="State2" Bounds="41.5977897644043,44.9799995422363,48.4222755432129,42.1323204040527" /> <Link Source="State0" Target="State3" Bounds="43.9286956787109,1.24344978758018E-14,229.335739135742,84.6670761108398" /> <Link Source="State1" Target="State2" Bounds="132.513122558594,118.980003356934,50.1091003417969,16.2699966430664" /> <Link Source="State2" Target="State3" Bounds="138.407501220703,82.2404556274414,108.355163574219,16.3045043945313" /> <Link Source="State3" Target="State1" Bounds="217.560623168945,118.980003356934,50.1091156005859,16.2699966430664" /> </Links> <Properties> <Property Id="Bounds" DataType="System.Windows.Rect" /> <Property Id="GraphDirection" DataType="Microsoft.VisualStudio.Diagrams.Layout.LayoutOrientation" /> <Property Id="Layout" DataType="System.String" /> </Properties> </DirectedGraph>
As I did with the other type-provider samples, I first define how a user uses the DGML-file type provider. The DGML file defines a state machine shown in Figure 5-8. Example 5-42 has the following three parts and shows how a user can use the type provider:
Part I creates the type provider, which takes two parameters. One parameter specifies where the DGML file is. The other parameter specifies the initial state of the state machine. At this point, the state machine is created from the DGML file.
Part II defines a set of transition functions. The transition is encapsulated in an object expression that implements the IState interface, which is defined in the following code sample. EnterFunction and ExitFunction will be invoked when the current state is going into the enter or exit state.
type IState = // function which will be invoked when entering a state abstract member EnterFunction : unit -> unit // function which will be invoked when exiting a state abstract member ExitFunction : unit->unit
Part III performs a series of transition operations. From the DGML definition, you can see that the first three transitions are valid. The last transition, which is from the current state of State3 to State2, is invalid and thus the transition function is not executed.
// Part I #r @".inDebugTypeProviderTemplate1.dll" type T = Samples.ShareInfo.TPTest.TPTestType< """C:MyCodeTypeProviderTemplate1TypeProviderTemplate1Graph1.dgml""", "State0"> let t = T() // Part II // define print function let syncRoot = ref 0 let print str = lock(syncRoot) (fun _ -> printfn "%s" str) // object expression for transition let printObj = { new StateMachineTypeProvider.IState with member this.EnterFunction() = print ("Enter " + t.CurrentState) member this.ExitFunction() = print ("Exit " + t.CurrentState) } // set the transition functions t.SetFunction(t.State0, printObj) t.SetFunction(t.State1, printObj) t.SetFunction(t.State2, printObj) t.SetFunction(t.State3, printObj) // Part III // valid transitions t.TransitTo_State1() t.TransitTo_State2() t.TransitTo_State3() // invalid transition t.TransitTo_State2()
The state machine reads the DGML file and generates a graph structure. MailboxProcessor is applied to process the message in an asynchronous way. The actual implementation defines two classes, as shown in Example 5-43. The DGMLClass type represents the DGML file with a list of nodes and links defined in the file. The StateMachine class inherits from DGMLClass. It adds MailboxProcessor to process the transition asynchronously and several member methods to set the transition functions. These two classes can be treated like a library that can process any DGML file.
DGMLClass representing the DGML file
namespace StateMachineTypeProvider open System.Xml open System.Xml.Linq // define the state interface type IState = // function which will be invoked when entering a state abstract member EnterFunction : unit -> unit // function which will be invoked when exiting a state abstract member ExitFunction : unit->unit // define a node record type Node = { Name : string; NextNodes : Node list} // define DGML DU type DGML = | Node of string | Link of string * string // define DGML class type DGMLClass() = let mutable nodes = Unchecked.defaultof<Node list> let mutable links = Unchecked.defaultof<DGML list> let mutable currentState = System.String.Empty // current state member this.CurrentState with get() = currentState and private set(v) = currentState <- v // all links in the DGML file member this.Links with get() = links and private set(v) = links <- v // all nodes in the DGML file member this.Nodes with get() = nodes and private set(v) = nodes <- v // initialize the state machine from fileName and set the initial state to initState member this.Init(fileName:string, initState) = let file = XDocument.Load(fileName, LoadOptions.None) this.Links <- file.Descendants() |> Seq.filter (fun node -> node.Name.LocalName = "Link") |> Seq.map (fun node -> let sourceName = node.Attribute(XName.Get("Source")).Value let targetName = node.Attribute(XName.Get("Target")).Value DGML.Link(sourceName, targetName)) |> Seq.toList let getNextNodes fromNodeName= this.Links |> Seq.filter (fun (Link(a, b)) -> a = fromNodeName) |> Seq.map (fun (Link(a,b)) -> this.FindNode(b)) |> Seq.filter (fun n -> match n with Some(x) -> true | None -> false) |> Seq.map (fun (Some(n)) -> n) |> Seq.toList this.Nodes <- file.Descendants() |> Seq.filter (fun node -> node.Name.LocalName = "Node") |> Seq.map (fun node -> DGML.Node( node.Attribute(XName.Get("Id")).Value) ) |> Seq.map (fun (Node(n)) -> { Node.Name=n; NextNodes = [] }) |> Seq.toList this.Nodes <- this.Nodes |> Seq.map (fun n -> { n with NextNodes = (getNextNodes n.Name) } ) |> Seq.toList this.CurrentState <- initState // find the node by the given nodeName member this.FindNode(nodeName) : Node option= let result = this.Nodes |> Seq.filter (fun n -> n.Name = nodeName) if result |> Seq.isEmpty then None else result |> Seq.head |> Some // current node member this.CurrentNode with get() = this.Nodes |> Seq.filter (fun n -> n.Name = this.CurrentState) |> Seq.head // determine if one node can transit to another one represented by the nodeName member this.CanTransitTo(nodeName:string) = this.CurrentNode.NextNodes |> Seq.exists (fun n -> n.Name = nodeName) // force current state to a new state member this.ForceStateTo(args) = this.CurrentState <- args // assert function used for debugging member this.Assert(state) = if this.CurrentState <> state then failwith "assertion failed"
StateMachine class inheriting from DGMLClass
// state machine class which inherits the DGML class // and uses a MailboxProcessor to perform asynchronous message processing type StateMachine() as this = inherit DGMLClass() let functions = System.Collections.Generic.Dictionary<string, IState>() let processor = new MailboxProcessor<string>(fun inbox -> let rec loop () = async { let! msg = inbox.Receive() if this.CanTransitTo(msg) then this.InvokeExit(this.CurrentNode.Name) this.ForceStateTo(msg) this.InvokeEnter(msg) return! loop () } loop ()) do processor.Start() // define the second constructor taking the file name and initial state name new(fileName, initState) as secondCtor= new StateMachine() then secondCtor.Init(fileName, initState) // asynchronously transit to a new state member this.TransitTo(state) = processor.Post(state) // set the transition function member this.SetFunction(name:string, state:IState) = if functions.ContainsKey(name) then functions.[name] <- state else functions.Add(name, state) // invoke the Exit function member private this.InvokeExit(name:string) = if functions.ContainsKey(name) then functions.[name].ExitFunction() // invoke the Enter function member private this.InvokeEnter(name:string) = if functions.ContainsKey(name) then functions.[name].EnterFunction()
The type-provider implementation uses the StateMachine type as a base class. It generates the transition function TransitTo_<target node name> and the assert function Assert_<node name>. Because it is easy to make a typo when specifying the state name, I chose to generate the state name as a read-only property. SetFunction is not generated from the type provider; it is a function defined on the StateMachine class. The code is shown in Example 5-44.
namespace Samples.FSharp.TypeProviderTemplate1 open System.Reflection open Microsoft.FSharp.Core.CompilerServices open Samples.FSharp.ProvidedTypes open StateMahcineTypeProvider [<TypeProvider>] type public TypeProvider1() as this = inherit TypeProviderForNamespaces() // Get the assembly and namespace used to house the provided types let thisAssembly = Assembly.GetExecutingAssembly() let rootNamespace = "Samples.ShareInfo.TPTest" let baseTy = typeof<StateMachine> let newT = ProvidedTypeDefinition(thisAssembly, rootNamespace, "TPTestType", Some baseTy) // define two static parameters: one is the DGML file name and // the other is the initial state let staticParams = [ProvidedStaticParameter("dgml file name", typeof<string>); ProvidedStaticParameter("init state", typeof<string>)] do newT.DefineStaticParameters( parameters=staticParams, instantiationFunction=(fun typeName parameterValues -> let ty = ProvidedTypeDefinition( thisAssembly, rootNamespace, typeName, baseType = Some baseTy) let dgml, initState = parameterValues.[0] :?> string, parameterValues.[1] :?> string // generate a new StateMachine instance to generate the read-only state properties let stateMachine = StateMachine() stateMachine.Init(dgml, initState) let stateProperties = stateMachine.Nodes |> Seq.map (fun n -> let name = n.Name let prop1 = ProvidedProperty(propertyName = n.Name, propertyType = typeof<string>, IsStatic=false, GetterCode= (fun args -> <@@ name @@>)) prop1 ) stateProperties |> Seq.iter ty.AddMember // generate the assert functions let asserts = stateMachine.Nodes |> Seq.map (fun n -> let name = n.Name let assertFunction = ProvidedMethod( methodName = sprintf "Assert_%s" name, parameters = [], returnType = typeof<unit>, IsStaticMethod = false, InvokeCode = fun args -> <@@ (%%args.[0] :> StateMachine). Assert(name) @@> ) assertFunction) asserts |> Seq.iter ty.AddMember // generate the transition functions let transits = stateMachine.Nodes |> Seq.map (fun node -> let name = node.Name let m = ProvidedMethod( methodName = sprintf "TransitTo_%s" name, parameters = [], returnType = typeof<unit>, IsStaticMethod = false, InvokeCode = fun args -> <@@ (%%args.[0] :> StateMachine). TransitTo(name) @@> ) m) transits |> Seq.iter ty.AddMember let setFunction = ProvidedMethod( methodName = "SetFunction", parameters = [ProvidedParameter("name", typeof<string>); ProvidedParameter("state class",typeof<IState>)], returnType = typeof<unit>, IsStaticMethod = false, InvokeCode = fun args -> <@@ (%%args.[0] :> StateMachine) .SetFunction(%%args.[1] :> string, %%args.[2] :> IState) @@> ) ty.AddMember(setFunction) // define the constructor let ctor = ProvidedConstructor( parameters = [], InvokeCode = fun args -> <@@ StateMachine(dgml, initState) @@>) ty.AddMember(ctor) ty.AddXmlDoc "xml comment" ty)) do this.AddNamespace(rootNamespace, [newT]) [<TypeProviderAssembly>] do ()
The type provider does not erase the type to System.Object anymore; therefore, IntelliSense shows a full list of class members from the base class. In the sample, SetFunction is visible in the IntelliSense drop-down list.
This sample shows a way to enhance the experience of working with a DGML file using a general-purpose library. The type provider can customize the property according to a specific DGML file, and any breaking change on the file will be reflected at compile time.
The XML type provider shows how type providers handle design-time logic and run-time logic. Actually, design-time logic and run-time logic can be separated into different assemblies. The design-time DLL not only provides the design-time logic, it also inserts extra logic to change the final assembly’s logic. Example 5-45 shows the run-time DLL. The TypeProvider attribute is the key that indicates which DLL is the type-provider assembly.
The sample solution has two projects. One is for the run-time assembly, and the other is the design-time type-provider code. The structure of the solution is shown in Figure 5-9.
namespace Samples.MiniTypeSpace.Runtime
type ObjectBase() =
member x.Kind = "I'm an object"
type ObjectRepresentation(contents:string) =
inherit ObjectBase()
member x.Contents = contents
type RuntimeMethods() =
static member Create(s:string) = ObjectRepresentation(s + " - hello!") :> ObjectBase
static member GetContents(s:ObjectBase) = (s :?> ObjectRepresentation).Contents
// This declaration points the compiler to the right design-time DLL for the selected
runtime DLL.
open Microsoft.FSharp.Core.CompilerServices
[<assembly:TypeProviderAssembly("DesignTimeSeparation")>]
do()
The design-time type-provider code is listed in Example 5-46. It generates class members to invoke the functions present in Example 5-45.
// Learn more about F# at http://fsharp.net // See the 'F# Tutorial' project for more help. namespace Samples.FSharp.DesignTimeSeparation open System.Reflection open Microsoft.FSharp.Core.CompilerServices open Samples.FSharp.ProvidedTypes open Microsoft.FSharp.Quotations [<TypeProvider>] type public TypeProvider1(config: TypeProviderConfig) as this = inherit TypeProviderForNamespaces() let assem = config.RuntimeAssembly let runtimeAssembly = System.Reflection.Assembly.ReflectionOnlyLoadFrom(assem) let objectBase = runtimeAssembly.GetType("Samples.MiniTypeSpace.Runtime.ObjectBase") let runtimeMethods = runtimeAssembly.GetType("Samples.MiniTypeSpace.Runtime. RuntimeMethods") let createMethod = runtimeMethods.GetMethod("Create") let getContentsMethod = runtimeMethods.GetMethod("GetContents") // Get the assembly and namespace used to house the provided types let rootNamespace = "Samples.ShareInfo.TPTest" let baseTy = objectBase let newT = ProvidedTypeDefinition(runtimeAssembly, rootNamespace, "TPTestType", Some baseTy) let ctor = ProvidedConstructor([ProvidedParameter("Contents",typeof<string>)], InvokeCode = (fun args -> Expr.Call(createMethod, [ args.[0] ]))) // This is a property. At runtime, the property just gets the string value let prop2 = ProvidedProperty( "Property2", typeof<string>, GetterCode= (fun args -> Expr.Call(getContentsMethod, [ args.[0] ]))) do prop2.AddXmlDocDelayed(fun () -> "xml comment") do newT.AddMember(prop2) newT.AddMember(ctor) do this.AddNamespace(rootNamespace, [newT]) [<TypeProviderAssembly>] do ()
Invoking the type provider, as shown in Example 5-47, is similar to the other example. The reference part is the main difference. In the other examples, there is only one assembly, but now there are two assemblies. The run-time DLL is what our test code needs to reference in order to use this type provider.
The last example in this chapter is a generated type provider. The generated type provider behaves like a code generator. It does not support lazy type generation like an erased type provider does. However, there are several advantages to using a generated type provider versus an erased type provider. The generated type provider generates an assembly that can be used from other language. Because the generated assembly is present, you can use .NET reflection to retrieve type information. I’ll start by showing you how to write a generated type provider. The code needed to create a generated type provider, which is shown in Example 5-48, is similar to the erased version except that a new ProvidedAssembly class is needed. Let’s start from a simple HelloWorld type-provider project, TypeProviderTemplate3, which is created from the TypeProvider template. If you compare Example 5-48 and Example 5-7, there are a few differences. There is an assembly variable whose type is ProvidedAssembly. The ProvidedType newT needs to set its IsErased and SuppressRelocation properties to false before it can be added to ProvidedAssembly.
namespace Samples.FSharp.TypeProviderTemplate3 open System.Reflection open Microsoft.FSharp.Core.CompilerServices open Samples.FSharp.ProvidedTypes [<TypeProvider>] type public TypeProvider1() as this = inherit TypeProviderForNamespaces() // Get the assembly and namespace used to house the provided types let assembly = ProvidedAssembly(@"c:mycode t.dll") let thisAssembly = Assembly.GetExecutingAssembly() let rootNamespace = "Samples.ShareInfo.TPTest" let baseTy = typeof<obj> let newT = ProvidedTypeDefinition(thisAssembly, rootNamespace, "TPTestType", Some baseTy) // add other property and method definition let m = ProvidedMethod( methodName = "methodName", parameters = [], returnType = typeof<int>, IsStaticMethod = false, InvokeCode = fun args -> <@@ 1 + 1 @@> ) let ctor = ProvidedConstructor(parameters = [], InvokeCode = fun args -> <@@ obj() @@>) let prop2 = ProvidedProperty(propertyName = "Property1", propertyType = typeof<string>, IsStatic=false, GetterCode= (fun args -> <@@ "Hello!" @@>), SetterCode = (fun args -> <@@ printfn "setter code" @@>)) do prop2.AddXmlDocDelayed(fun () -> "xml comment") do newT.AddMember(m) newT.AddMember(prop2) newT.AddMember(ctor) newT.IsErased <- false newT.SuppressRelocation <- false assembly.AddTypes [newT] do this.AddNamespace(rootNamespace, [newT]) [<TypeProviderAssembly>] do ()
Be sure that the C:MyCode folder is created because the DLL will be generated there.
The constructor needs to return obj()
. If the default <@@ () @@>
is used, the constructor will return a NULL value and generate a runtime error.
You need to create another project to consume the generated type or types, so you create an F# console project for that purpose. Instead of adding a reference to the generated tt.dll under C:MyCode, you need to add a reference to the output of the type-provider project TypeProviderTemplate3. Figure 5-10 shows the Console project references.
Example 5-49 shows Program.fs, which creates the generated type and prints out the property value.
type T = Samples.ShareInfo.TPTest.TPTestType let t = T() let p = t.Property1 printfn "t.Property1 value is %A" p ignore <| System.Console.ReadKey()
For a generated type provider, you can create fields to hold values by using ProvidedField. Example 5-50 shows how to create a field and expose it.
namespace Samples.FSharp.TypeProviderTemplate3 open System.Reflection open Microsoft.FSharp.Core.CompilerServices open Samples.FSharp.ProvidedTypes [<TypeProvider>] type public TypeProvider1() as this = inherit TypeProviderForNamespaces() // Get the assembly and namespace used to house the provided types let assembly = ProvidedAssembly(@"c:mycode t.dll") let thisAssembly = Assembly.GetExecutingAssembly() let rootNamespace = "Samples.ShareInfo.TPTest" let baseTy = typeof<obj> let newT = ProvidedTypeDefinition(thisAssembly, rootNamespace, "TPTestType", Some baseTy) // add other property and method definition let m = ProvidedMethod( methodName = "methodName", parameters = [], returnType = typeof<int>, IsStaticMethod = false, InvokeCode = fun args -> <@@ 1 + 1 @@> ) let ctor = ProvidedConstructor(parameters = [], InvokeCode = fun args -> <@@ obj() @@>) let prop2 = ProvidedProperty(propertyName = "Property1", propertyType = typeof<string>, IsStatic=false, GetterCode= (fun args -> <@@ "Hello!" @@>), SetterCode = (fun args -> <@@ printfn "setter code" @@>)) let field1 = ProvidedField("myField", typeof<string>) let prop3 = ProvidedProperty( propertyName = "Property2", propertyType = typeof<string>, IsStatic=false, GetterCode= (fun [me] -> Microsoft.FSharp.Quotations.Expr. FieldGet(me,field1)), SetterCode = (fun [me;v] -> Microsoft.FSharp.Quotations.Expr. FieldSet(me,field1, v))) do prop2.AddXmlDocDelayed(fun () -> "xml comment") do newT.AddMember(m) newT.AddMember(prop2) newT.AddMember(field1) newT.AddMember(prop3) newT.AddMember(ctor) newT.IsErased <- false newT.SuppressRelocation <- false assembly.AddTypes [newT] do this.AddNamespace(rootNamespace, [newT]) [<TypeProviderAssembly>] do ()
The test code is presented in Example 5-51. From the execution result, you can see the field is created in the assembly and that you can use Property2 to access the field.
type T = Samples.ShareInfo.TPTest.TPTestType let t = T() let p = t.Property1 printfn "t.Property1 value is %A" p let b = t.GetType().GetField( "myField", System.Reflection.BindingFlags.Instance ||| System.Reflection.BindingFlags.NonPublic) printfn "found myField? %A" ( b<>null ) printfn "t.Property2 value is %A" t.Property2 t.Property2 <- "aa" printfn "t.Property2 value is %A" t.Property2 ignore <| System.Console.ReadKey()
Execution result
t.Property1 value is "Hello!" found myField? True t.Property2 value is <null> t.Property2 value is "aa"
The type-provider API also provides a way to decorate a property with an attribute. Example 5-52 shows how to add an attribute to a property and how to generate the type that is returned from the DefineStaticParameters function.
namespace Samples.FSharp.TypeProviderTemplate4 open System.Reflection open Microsoft.FSharp.Core.CompilerServices open Samples.FSharp.ProvidedTypes type MyAttribute() = inherit System.Attribute() [<TypeProvider>] type public TypeProvider1() as this = inherit TypeProviderForNamespaces() // Get the assembly and namespace used to house the provided types let thisAssembly = Assembly.GetExecutingAssembly() let rootNamespace = "Samples.ShareInfo.TPTest" let baseTy = typeof<obj> let newT = ProvidedTypeDefinition(thisAssembly, rootNamespace, "TPTestType", Some baseTy) let assembly = ProvidedAssembly(@"c:Mycode t.dll") let data = ProvidedStaticParameter("filename", typeof<string>) do newT.DefineStaticParameters( [data], fun tyName [| :? string as data |] -> let ty = ProvidedTypeDefinition( thisAssembly, rootNamespace, tyName, Some(typeof<obj>)) ty.AddMember(ProvidedConstructor([], InvokeCode = fun _ -> <@@ obj() @@>)) let p = ProvidedProperty(data, typeof<string>, GetterCode = fun [me] -> <@@ data @@>) p.AddAttribute(typeof<MyAttribute>) ty.AddMember(p) ty.IsErased <- false ty.SuppressRelocation <- false assembly.AddTypes [ty] ty) do newT.IsErased <- false newT.SuppressRelocation <- false this.AddNamespace(rootNamespace, [newT]) assembly.AddTypes [newT] [<TypeProviderAssembly>] do ()
Example 5-53 shows the attribute is correctly added to the property.
type T = Samples.ShareInfo.TPTest.TPTestType<"PropertyA"> let t = T() printfn "propertyA value is %A" t.PropertyA let pi = t.GetType().GetProperty("PropertyA") printfn "propertyA has attribute %s" ( pi.GetCustomAttributes(typeof<Samples.FSharp.TypeProviderTemplate4.MyAttribute>, true) |> Seq.head |> fun attribute -> attribute.ToString() ) ignore <| System.Console.ReadKey()
Execution result
propertyA value is "PropertyA" propertyA has attribute Samples.FSharp.TypeProviderTemplate4.MyAttribute
The F# snippets are designed to make the act of authoring a type provider even easier. The snippet project is published at http://fsharpcodesnippet.codeplex.com/ as an open source project. Each type provider snippet does not have shortcut support; however, you can press Ctrl+K,X to bring up a list of all snippets, as shown in Figure 5-11. Table 5-1 lists the commonly used code snippets for type-provider authoring.
The type-provider mechanism that shipped with F# 3.0 has some limitations. The first limitation that a reader might have already noticed is that the static parameter can only take basic types, such as integer and string. The other limitation is that the type provider can only generate .NET 1.x types. Most of the F# specific types such as record are not supported. The only exception is the units of measure types, which are supported in this release. These limitations will be eliminated in a future release, but for now we have to live with them.