In the latest F# release (May 2009 CTP), the dynamic lookup operator "?" was introduced. I expected people would play with it, but it seems like it went nearly unnoticed. This blog post describes a few ways to use it, it's mostly for fun. Whenever you can, I recommend you to stay in the safe static typed world. But dynamic features sometimes might come in handy...
The '?' operator
I wanted to try it, but it was undefined. Let's give it a quick definition:
> let (?) x y = x, y;; val ( ? ) : 'a -> 'b -> 'a * 'b > 3 ? foo;; val it : int * string = (3, "foo")
Look: even if the variable foo was not defined, the function call works and the foo has been stringified by the compiler. It's as if I wrote 3 ? "foo". So, here's the magic: the F# has added syntactic sugar and, well, nothing more. But it's enough to have.
If "x" is a list, you can write x.Length to compute the length of the list. However, if "x" is downcasted to obj, you can't call its Length property easily, beause it's unsafe: the compiler doesn't know what is inside the obj. We know it only at runtime.
The '?' was designed to address this issue, it is supposed to do some introspection (that's why the property name is given as a string). F# Readme file suggests this basic implementation:
open System.Reflection let (?) (x: 'a) (prop: string) : 'b = let pi = typeof<'a>.GetProperty(prop) pi.GetValue(x, [||]) :?> 'b
This implementation actually doesn't work as typeof<'a> is the compile-time type of x. We can only access members we could have accessed statically: we're getting only the bad side of things (it's unsafe, and we don't win anything).
Here is a way to perform a dynamic property lookup. This is based on the dynamic type, and it also accepts methods without arguments:
let (?) x prop = let flags = BindingFlags.GetProperty ||| BindingFlags.InvokeMethod x.GetType().InvokeMember(prop, flags, null, x, [||]) > let o = [0; 1; 2; 3] :> obj;; val o : obj = [0; 1; 2; 3] > o?Length;; val it : obj = 4 > o?Tail;; val it : obj = [1; 2; 3]
Handle multiple arguments
The limitation of the previous code is obvious: we can't call methods that have arguments. Here is a more complete definition, I found it more convenient to use tuples (thus, I convert them to arrays using reflection).
open Microsoft.FSharp.Reflection let (?) x m args = let args = if box args = null then (* the unit value is null, once boxed *) [||] elif FSharpType.IsTuple (args.GetType()) then FSharpValue.GetTupleFields args else [|args|] x.GetType().InvokeMember(m, BindingFlags.GetProperty ||| BindingFlags.InvokeMethod, null, x, args)
It allows us to write code like:
> let str = "foobar" :> obj;; val str : obj = "foobar" > str?Substring(1, 4);; val it : obj = "ooba" > str?Length();; val it : obj = 6 > str?IndexOf('b');; val it : obj = 3
Sweet, isn't it? Of course, you can always add a call to the unbox function if you'd like to come back to the reassuring and safe world. It might be a good idea to call 'unbox' in the '?' operator definition, type inference will often guess what type you want back.
If we have the following type t:
type t() = let mutable x = 0 member o.n with get() = x and set(newv) = x <- newv
We'll probably want to call its getters and setters. Getters are already usable:
> t()?n();; val it : obj = 0
Notice that the '?' is parsed the same way as the dot operator, it uses the same trick on method applications (the expression "t () ? n ()" fails because of spaces). This way, we can chain expressions:
> 42?ToString()?Length();; val it : obj = 2
What about setters? F# has introduced another operator named "?<-". Yes, a ternary one. Let's define it:
let (?<-) x prop v = x.GetType().InvokeMember(prop, BindingFlags.SetProperty, null, x, [|v|]) > let a = t() :> obj;; val a : obj > a?n <- 4;; val it : obj = null > a?n();; val it : obj = 4
It seems we have a full duck typing system. If we mistakenly call an undefined method, we got a MissingMethodException exception. So be careful!
In many dynamic languages (Ruby, Python, Perl...), classes can define a missing_method method, which is called instead of raising the exception. Our '?' operator could do that too (this is let as an exercise for the reader).
Since it's based on reflection, we can access values that were hidden by the F# compiler. Tuples and sum types have some methods designed for C# interop; unfortunately, they're not available to F# code.
// Access any value of a tuple > (2, "foo", 5.3)?Item3();; val it : obj = 5.3
Using the Ranking sum type from my Poker example:
// safe traditional way > let is_straight = match a with Straight _ -> true | _ -> false;; val is_straight : bool = true // duck type way > let is_straight = a?IsStraight();; val is_straight : obj = true // convert sum type to int, even if it's not an enum > let is_straight = a?Tag();; val is_straight : obj = 4
Dear F# team, could you consider making visible the "Is*" methods from sum types? They are both safe and useful. Thanks!