Good things always come to an end, right? Not necessarily. By now we’ve all seen just how awesome type providers are and you might have even made one or you’re wanting to make one. One of the cool things about them though is that you can start to make type systems which never end. For example in the Brainfuck type provider you can create programs of any length by accessing types which belong to types so you can create a program like so.
Now in this case, everything up until the ConsoleOutput is a type and ConsoleOuput is just a property on the final type. We’ll look at how you’d go about implementing this small section of the type provider, so I’ll assume you already know the basics of type providers. If you don’t and want to learn check out this post by Michael Newton here and maybe even visit F# London in May here. If you want to follow along at home and implement the steps as we go through then you can download the skeleton project from here.
The key thing that we’re going to be looking at here is how to create the same series of types onto each other and how to get some state flowing through. We’ll be making a type provider which allows us to create words through a type system.
So you’ll have the shell of your type provider with the base type that it provides and now you want to provide a lot more. The first thing to do is to look at the ProvidedType.AddMemberDelayed() method. This method takes a function as a parameter which is of the form unit -> MemberInfo. A MemberInfo is what all of our Provided_ types inherit from so we can add pretty much anything. In our case we’ll be using a combination of ProvidedType and ProvidedProperty.
We’ll create a function now which matches the definition then, so how’s about something like the following.
let rec createTypes () = let ty = ProvidedTypeDefinition("A", None) ty.AddMemberDelayed(createTypes) ty let rootType = ProvidedTypeDefinition(asm, ns, "StartHere", None) do rootType.AddMemberDelayed(createTypes)
And then in our provided type we’ll call AddMemberDelayed passing in a reference to our function. If we test that now we’ll see that we have a type provider which won’t stop providing a child type with the same name. Let’s now switch to using the AddMembersDelayed, AddMemberDelayed is great if we only want to add 1 thing at a time, but we want to add the alphabet each time. It’s pretty much the same concept except instead of creating a function like unit -> MemberInfo we create a function like unit -> MemberInfo list.
let rec createTypes () = alphabet |> Seq.map (fun t -> let ty = ProvidedTypeDefinition((t |> string), None) ty.AddMembersDelayed(createTypes) ty) |> Seq.toList do rootType.AddMembersDelayed(createTypes)
So now we have the alphabet available through our type provider. It would be nice if we could now pass some state through and then access our word through a property. We’ll modify our function definition to be of string -> MemberInfo list but now it doesn’t match the required function definition. We’ll store our word so far in a closure and pass that closure to the AddMembersDelayed method instead. It’ll look something like this.
let rec createTypes wordSoFar = alphabet |> Seq.map (fun t -> let newChar = t |> string let newWord = wordSoFar + newChar let ty = ProvidedTypeDefinition(newChar, None) ty.AddMembersDelayed(fun () -> createTypes newWord) ty) |> Seq.toList do rootType.AddMembersDelayed(fun () -> createTypes "")
And we can then easily add in a property to access the word so far like this.
let rec createTypes wordSoFar = alphabet |> Seq.map (fun t -> let newChar = t |> string let newWord = wordSoFar + newChar let ty = ProvidedTypeDefinition(newChar, None) ty.AddMembersDelayed(fun () -> createTypes newWord) let wordProp = ProvidedProperty("Word", typeof<string>, IsStatic=true, GetterCode = fun args -> <@@ newWord @@>) ty.AddMember(wordProp) ty) |> Seq.toList
And now you can access the words like so:
Word.TypeProvider.StartHere.T.E.S.T.Word // returns "TEST"
Type providers can look really complex at first but they’re actually really easy to work with and make something pretty cool pretty quickly. Also if you wanted to expand this further you could load in a word list from a static parameter, then parse that into a trie and only allow the type system to create words from that word list. (Here‘s a hint from Chris Smith if you wanted to try that)