Using and Abusing the F# Dynamic Lookup Operator
Lately, I’ve been playing with such things as MongoDB using F# to rapidly prototype ideas. With that, I’ve tried to rid myself of magic strings by using the F# dynamic lookup operator. I’ll cover exactly what I’m doing in the next post when using MongoDB, but in this post I’d like to explore a little of what you could do with a little noticed dynamic lookup operator in F#.
The Dynamic Lookup Operator
Much like C# 4.0 has the ability to do dynamic lookup, F# also has the same capability, although in a different capacity. The language has support for a dynamic lookup get operator ( ? ) and set operator ( ?<- ), but note that I said support and not actual implementation. The actual implementation is up to you and how you want to use it.
The Get Operator
Let’s start off by implementing the get operator to get a public property (not super useful yet).
let (?) (this : 'Source) (prop : string) : 'Result = let p = this.GetType().GetProperty(prop) p.GetValue(this, null) :?> 'Result
We can write an example that verifies the behavior such as comparing a normal invocation and our “dynamic” invocation, as in this example of comparing getting the length of a string.
[<Fact>] let ``Dynamic lookup should equal normal``() = areEqual "foo".Length "foo"?Length
But, we’re not limited to that. For example, we could instead decide that we want to get a private member instead of a public one. We could change up our example from above just slightly.
let (?) (this : 'Source) (prop : string) : 'Result = let flags = BindingFlags.GetProperty ||| BindingFlags.NonPublic let p = this.GetType().GetProperty(prop, flags) p.GetValue(this, null) :?> 'Result
We can verify our behavior of this idea with a little example, for example invoking a private property in a custom class such as the following:
type PrivateProperty(propValue : string) = member private __.PropValue with get() = propValue [<Fact>] let ``Can invoke private property``() = let expected = "foo" let p = PrivateProperty(expected) let actual : string = p?PropValue areEqual expected actual
So, as you can see, this is quite useful so far, but we’ve only scratched the surface. We’ve just dealt with properties so far, but what about methods? The same applies here as well. In this instance, let’s take a method that could have any number of arguments and invoke it just as if we would normally.
open Microsoft.FSharp.Reflection open System.Reflection let (?) (this : 'Source) (member' : string) (args : 'Args) : 'Result = let argArray = if box args = null then null elif FSharpType.IsTuple (args.GetType()) then FSharpValue.GetTupleFields args else [|args|] let flags = BindingFlags.GetProperty ||| BindingFlags.InvokeMethod this.GetType().InvokeMember(member', flags, null, this, argArray) :?> 'Result
What we needed to do here is take not only the member but the arguments as well. If the arguments object is null (or unit), then we pass a null objet array, else if our type is a tuple, then we create an array from our tuple, and finally if it’s a single argument, then we make it into an array. Finally we invoke our member with the flags to say it is either a property getter or a method and invoke with our arguments. Let’s write an example of how to use this.
[<Fact>] let ``Can invoke method with dynamic lookup``() = isTrue ("dynamic"?Length() > 0) isTrue ("dynamic"?EndsWith("ic")) areEqual "dyn" ("dynamic"?Substring(0,3))
Above are three tests which show no arguments, a single argument, and finally tupled arguments. But what about setters?
The Set Operator
Just as we have the ( ? ) operator reserved for the get operator, we have the ( ?<- ) operator reserved for the set. This allows us to be able to set any number of things with the same ease as the get. For example, we could set a public property:
let (?<-) (this : 'Source) (property : string) (value : 'Value) = this.GetType().GetProperty(property).SetValue(this, value, null)
This simply gets the property and then sets the appropriate value. And then we can write a test as an example of how it should work.
type MutablePropertyClass(value : string) = let mutable v = value member __.Value with get() = v and set(value) = v <- value [<Fact>] let ``Dynamic setter should set property``() = let m = MutablePropertyClass("foo") m?Value <- "dynamic" areEqual "dynamic" m.Value
In this example, we have a simple class with a property with a getter and setter and then we set our property using our special operator and then compare the values. Once again, this is flexible enough where we could set private properties much as we retrieved them above. Now, let’s put them together for a few quick examples.
The Dictionary Example
One idea that we could go with is instead of using string keys for dealing with key value pairs, we could use our dynamic getter and setter instead. Nothing like a bit of syntactic sugar to brighten your day to lessen the noise. Our goal for this is to go from:
let d = Dictionary<string,obj>() d.["StaticKey"] <- "Meh"
To something like the following:
let d = Dictionary<string,obj>() d?DynamicKey <- "Totally!"
How could we pull this one off? Well, we could implement the dynamic lookup getter and setter to do nothing more than look in the dictionary as follows:
open System.Collections let (?) (this : #IDictionary) key = this.[key] let (?<-) (this : #IDictionary) key value = this.[key] <- value
Now we can verify our behavior with our test that we should have written first.
let ``Dictionary should get and set dynamically``() = let d = Dictionary<string,obj>() d?DynamicKey <- "hello" areEqual d.["DynamicKey"] d?DynamicKey
A solution like this could work well for situations like MongoDB, which we’ll get into with our next post. But, could go a step further and to say anything with the Item property could be ours for the taking.
The Item Indexer Property Example
In the .NET libraries, we have a few classes which expose an indexer property like an array, a Dictionary, and even a string. It would be nice to have a way to both get and set this value through the use of our dynamic lookup operator, but how could we pull this off? In some previous posts, I talked about generic restrictions in the F# language, and one in particular stands out, the member restriction operator. This operators lets us constrain a given object parameter so that it must implement the following methods and properties.
Remember, our overall goal is to support calling this:
open System.Collections.Generic let d = Dictionary<string,string>() d?DynamicKey <- "Totally!" type IndexedObject() = let d = Dictionary<string,string>() member self.Item with get(key) = d.[key] and set key value = d.[key] <- value let i = IndexedObject() h?IndexedKey <- "IndexedValue"
So, what we have is both a Dictionary<TKey,TValue> and a custom indexed object. Since they do not follow an inheritance chain which exposes an Item property, we cannot use the example from above with the IDictionary. Instead, we’ll use member restrictions for both the method get_Item and set_Item.
let inline (?) this key = ( ^a : (member get_Item : ^b -> ^c) (this,key)) let inline (?<-) this key value = ( ^a : (member set_Item : ^b * ^c -> ^d) (this,key,value))
What you see is that we’re calling both the get_Item and set_Item properties, which are the raw method names for the Item property. Now we’re able to verify our test using our IndexedObject class from above:
let ``Can use dynamic indexed getter and setter``() = let i = IndexedObject() i?SomeKey <- "Some value" areEqual i.["SomeKey"] i?SomeKey
A big word of caution when using this approach is that you’ll get a nice warning from the F# compiler that doing this may cause unverified code as F# treats the Item property specially. For example, all value types must be boxed before putting them into the collection.
let h = Hashtable() h?MyKey <- 3 // Don't do this h?MyKey <- box 3 // Do this
Very powerful way of doing things but also you must be careful with this approach if you use it at all.
So, as you can see, F#, just like C# has the facilities for dynamic lookup, although in F# you are left to implement it yourself in any which way you like. One great thing about these operator is that it doesn’t require .NET 4.0 to implement and use them. But with any power comes of course some responsibility for it, so use it wisely.