Month: March 2014

Creating an infinite type system with F#’s type providers

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.

Brainfuck.TypeProvider.``+``.``+``.``.``.ConsoleOuput

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)

Advertisements

A quick COM and F# tip

COM’s got a few quirky nuances. I spent this evening battling a problem in which a C# wrapper had been written around a COM API. This C# wrapper defined an interface called IMediaDet, it also declared a class called MediaDet which should inherit from the interface but doesn’t. Instead the interface and the class shared the same GUID. So I had a C# example which looked like this.

[Guid("65BD0710-24D2-4ff7-9324-ED2E5D3ABAFA"),...]
interface IMediaDet {}

[Guid("65BD0710-24D2-4ff7-9324-ED2E5D3ABAFA"),...]
class MediaDet {}

var mediaDet = new MediaDet() as IMediaDet;

At first you’d think that mediaDet should always be null but it wasn’t, it was actually being assigned due to the underlying COM implementation. Unfortunately in F# something like the above statement isn’t allowed and rightly so, it doesn’t make sense. Thankfully Ross McKinlay was available on Twitter to help and suggested the following

let mediaDet : IMediaDet = unbox(new MediaDet())

The point of the unbox is to avoid the type checker. For the most part though, you should always listen to the compiler and avoid doing things like this as it stops you trying to do something stupid like use COM.