Tutorial on Implementing a F# Type Provider At Home

Abstract


This article demonstrates the implementation of a custom Type Provider in F#, developed as a class library project using Visual Studio 2022. The Type Provider accepts a string parameter at design time and performs case transformation—inverting uppercase characters to lowercase and vice versa—making the string available at compile time rather than runtime. This example illustrates the metaprogramming capabilities of F# Type Providers as compile-time code generators.


Prerequirements



Materializing the Executable Project


  1. Launch Visual Studio 2022.
  2. Select "Create a new project".
  3. In the search bar, type "F# Console App" and select the template without the (.NET Framework) suffix(because I did not test with any other framework version, it may work, or not.). Click "Next".
  4. Name your project (e.g., CaseChanging).
  5. Select .NET 9 as the framework version, and click "Create".

This will create an F# executable project with a default Program.fs file, which you can run now to ensure that everything is set up correctly.


Materializing the Library Project for the Type Provider


  1. Right-click the solution in the Solution Explorer window.
  2. Click Add → New Project.
  3. In the search bar, type "F# Class Library" and select the template (again, not the .NET Framework version). Click "Next".
  4. Give it a name (e.g., CaseChangingProvider). Click "Next".
  5. And, importantly, set the .NET version to .NET Standard 2.0, and click "Create". This is crucial for Type Provider's in-process execution.

This will create an F# class library project with a default Library.fs file. This library will contain our custom Type Provider. You will now add this library project as a dependency of the Executable Project.


Incorporating the Necessary SDK Files


Prior to implementing our custom Type Provider, we first need to include the F# Type Provider SDK helper files. These files provide the foundational types and interfaces for building Type Providers and are named ProvidedTypes.fsi (signature file) and ProvidedTypes.fs (implementation file).

To obtain ProvidedTypes.fsi and ProvidedTypes.fs, go to the fsprojects/FSharp.TypeProviders.SDK repository on GitHub, select the latest commit where the tests are passing (e.g., this one). Navigate to the src directory and manually copy or download these two files into your Library Project. Here is the correct order in which the project compiles, you can move them in the .fsproj file or by using the Alt+(up arrow)|Alt+(down arrow) shortcut combo in the Solution window.

  1. ProvidedTypes.fsi
  2. ProvidedTypes.fs
  3. Library.fs(left from the project creation)


Implementing the Provider


Rename the Library.fs file to CaseChangingProvider.fs

Clear its contents and we will start fabricating the Type by setting up the namespace and imports:

CaseChangingProvider.fs
namespace MyProvider open System open System.Reflection open ProviderImplementation.ProvidedTypes // This comes from ProvidedTypes.fs/.fsi open Microsoft.FSharp.Core.CompilerServices // To make this assembly recognized as containing Type Providers. [<assembly: TypeProviderAssembly>] do ()

Next, we will define the core Type Provider class.

CaseChangingProvider.fs
[<TypeProvider>] type public CaseChangingProvider(config: TypeProviderConfig) as this = inherit TypeProviderForNamespaces(config) // In many other online examples this is a parameterless constructor. let providerNamespace = "MyProvider.CaseChanger" let thisAssembly = Assembly.GetExecutingAssembly() // Logic to transform the input string, this function will modify the input string. let transformString (input: string) = let switch c = if Char.IsUpper c then Char.ToLower c else Char.ToUpper c String.Join("", input |> Seq.map switch)

We will implement a function to generate the type based on its given generic name.

CaseChangingProvider.fs(inside CaseChangingProvider)
let buildGenericType (inputString: string) (dynamicTypeName: string) = let providedType = ProvidedTypeDefinition( thisAssembly, providerNamespace, dynamicTypeName, Some typeof<obj> ) providedType.AddXmlDoc $"Provides compile-time case transformation for the input string: '{inputString}'." let transformedValue = transformString inputString // Add a static property to the generated type that holds the transformed string let staticValueProperty = ProvidedProperty( propertyName = "Value", propertyType = typeof<string>, isStatic = true, getterCode = (fun _args -> <@@ transformedValue @@>) // Quotation embeds the value at compile time ) staticValueProperty.AddXmlDoc $""" The compile-time transformed string. Original input: '{inputString}' Transformed value: '{transformedValue}' """ providedType.AddMember staticValueProperty providedType

The generic root type is subsequently defined as the following.

CaseChangingProvider.fs(inside CaseChangingProvider)
let rootType = ProvidedTypeDefinition( thisAssembly, providerNamespace, "Transform", // The name of the type you'll use to access the provider, e.g., Transform<"MYSTRING"> Some typeof<obj> ) do rootType.AddXmlDoc $""" Case Changing Type Provider. Use this type with a static string parameter to get a transformed string. Example: type MyTransformed = {providerNamespace}.Transform<"exampleSTRING"> let value = MyTransformed.Value """ // Define the static parameters the 'Transform' type accepts. // In this case, a single string parameter named "input". let staticParameters = [ ProvidedStaticParameter("input", typeof<string>) ] do rootType.DefineStaticParameters( parameters = staticParameters, instantiationFunction = (fun typeNameWithArgs suppliedArguments -> match suppliedArguments with | [| :? string as actualInputString |] -> buildGenericType actualInputString typeNameWithArgs | _ -> // This will result in a compile-time error if the arguments are incorrect. failwith "Invalid static arguments. This Type Provider expects a single string argument." ) )

Lastly, the type is registered under the designated namespace.

CaseChangingProvider.fs(inside CaseChangingProvider)
do this.AddNamespace(providerNamespace, [rootType])

Rebuild the solution using Ctr+Shift+B . If file locking errors occur during rebuild—such as when Visual Studio locks the output DLL—you may need to restart Visual Studio 2022 to release the handle.
The process cannot access the file 'bin\Debug\netstandard2.0\TypeMaster.dll' because it is being used by another process. The file is locked by: "Microsoft Visual Studio 2022"


Using the Provider


Returning to the Executable Project, inside Program.fs write:

Program.fs
module Program // Opening the provider open MyProvider.CaseChanger [<EntryPoint>] let main args = printfn "%s %s" Transform<"HELLO">.Value Transform<"world!">.Value 0

Ctrl+F5 and a window will pop up, displaying hello WORLD!


Ending Thoughts


This tutorial introduced the core concepts of F# Type Providers by implementing a compile-time string transformer. While the functionality is simple, it demonstrates the essential mechanisms—static parameters, compile-time type generation, and embedding metadata—which can be extended toward more sophisticated compile-time integrations. A good exercise might be to build a runtime O(1) Fibonacci calculator using compile-time generated types.


Comments page.