Thoughts on reflective programming with f#
Cats: Code
Tags: f#, reflective programming
It's been a few days longer than I'd like, but now I have something worth sharing. I bounce back and forth between more framework-oriented work and more application-oriented work, but they do end up being two sides of the same coin. Here I want to talk about framework stuff. Oh and a quick disclaimer - this is more thinking out loud, and less a final solution. Input, as always, is most welcome.
In the oo world, your common unit of function is an object. An object may do more than one thing, but it typically does at least one thing, has at least one "function" (not in the programming sense, but in the "i do something" sense). As such, the problem of storing meta information on your object has long been solved - attributes. If I have an object with data elements on it (fields or properties), then I can encode information for a consuming runtime by applying attributes to it. It is a fairly common design pattern for an object that functions as a component in a larger framework (think any ORM framework as an example) to encode information on its data elements, have the runtime read that information, and populate/read from those elements, and then invoke some method on the object.
In f#, on the other hand, your common unit of function is ... a function. Unlike an object, a function typically does one thing (though a function with side effects most certainly may do more than one). Now, I do still have the concept of attributes in f#, and i can decorate my function with multiple attributes, but there's a slight problem. To implement the design pattern I mentioned before (fields marked by attributes, populated by the runtime), I have to have two things - locally-scoped data elements that are mutable externally. The only way to do that is to implement a class, with mutable properties just like i would in c#. This means that I lose my functional advantage, and now am using f# as a different dialect of an oo language. Feasible, but not pleasant. Or necessary.
Let's take a different approach. I'm going to bring back a piece of functionality i talked about in earlier articles - quotations, and using quotations to determine function signatures. I won't go into those details again, but suppose i have a function like this
[<ReflectedDefinition>] let public sample2 (l_name: string) = let f_name = "alex" (f_name, l_name)
I've shown before that i can use quotations to get at the function signature, and extract the type, name, and position of every parameter to this function. This means that my fictitious runtime, upon learning of this function, can find out that function "sample2" take a parameter "l_name" of type "string". Well that certainly a start. But what happens when I want to pass along more information? For example, what happens when I want to tell the runtime that value l_name will never be the empty string (yay for more contrived examples). Before we answer that question, let's answer a slightly simpler one - how do you handle nulls in f#? Well - we use the Option type, which is effectively a discriminated union, with discriminators None (denoting ... well ... no value), and Some v, denoting that it does contain a value, and the value is v. Option is a generic type, so we could write it as "l_name Option<string>". Or, better yet, we can write it as "l_name string option". Cool. It's succinct, and provides information. Now, since Option is a type, our runtime will see that function sample2 has a single parameter l_name, of type Option<string>, which tells it that it's a string value that may be null.
Okay, back to the original question - how do i inform the runtime of something else - like the string parameter will never be empty? Well, we can use the same thing - we can define our own discriminated union, say... NonEmpty, and change our function signature to be "l_name string NonEmpty". What's even more interesting is that you can compose these values - i can say that the string is optional, but if present, then non empty - "l_name string option NonEmpty" or "l_name string NonEmpty option". So now we have a way of attaching meta information (of sorts) to our parameter values, and it mostly doesn't get in the way.
There are, of course, a few drawbacks and limitations. Two jump to mind - first, these markers aren't parametrizable - meaning that while an actual attribute will let you define parameters that carry the meta information (an attribute for an orm that will denote a field mapping will let you specify the database field name as a string parameter), this approach only allows for marker attributes. Second, while not horribly obtrusive, you do introduce the need to "unwrap" your value. If you do have the example above of l_name string option NonEmpty, you have to do something like l_name.Value.Value to get at the actual l_name.
I don't think either of these are showstoppers. The first one, you can supplement the marker attributes with regular attributes if you must have arbitrary parameters. The second one, you can write a generic decomposition function, something like
/// annotates a session-scoped value type session<'a> = | SessionValue of 'a member x.Value = match x with SessionValue a -> a /// annotates a form-scoped value type form<'a> = | FormValue of 'a member x.Value = match x with FormValue a -> a /// recursively retrieves values from an arbitrarily-annonated value let rec get_value (v: obj) = match v with | : ? session<_> as ssn -> get_value ssn.Value | : ? form<_> as frm -> get_value frm.Value | : ? Option<_> as opt -> match opt with | Some v -> get_value v | None -> null | _ -> v
Edit: this actually ends up not working. The type parameters in the type tests get constrained to obj, meaning that you're testing an arbitrary data type against (as an example) session
Do note that one drawback of get_value is that it is forced to restrict v to be of type obj, and as a result, restricts the return value to be of type obj as well, forcing casts when consuming. I'm still playing with this, and will post an update if i find a better way.
Finally, on the framework side, reading this information from a function signature is actually not horribly difficult either. (Caveat - this implementation is a simple proof-of-concept, and as such isn't overly extensible. You'll notice that the list of possible parameters is hard-coded a tuple of fixed order and length. That can be replaced with a different data structure, but works for now). This sample takes a function signature and uses discriminated union markers to infer scope and directionality of parameters to a given function.
/// active pattern that is matched when input is a generic type whose definition is pattern_type let (|MatchGenericType|_|) (pattern_type: Type) (input: Type) = if input.IsGenericType && input.GetGenericTypeDefinition() = pattern_type then Some <| input.GetGenericTypeDefinition() else None /// active pattern that is matched when input is an option type let (|MatchOptionType|_|) (input: Type) = if input.IsGenericType && input.GetGenericTypeDefinition() = optiont then Some <| input.GetGenericTypeDefinition() else None /// gets the generic type parameter of this type. note that this function /// assumes that the supplied type is a specific definition of a generic /// type, and that it has at least one type parameter let get_generic (t: Type) = t.GetGenericTypeDefinition().GetGenericArguments().[0] /// recursively peels off outer markers of scope/directionality let rec decompose state (a: string * Type) = let name, parm_type = a let form_list, depends, requires, session, request = state if parm_type.IsGenericType then (name, get_generic parm_type) |> decompose (match parm_type with | MatchOptionType opt_type -> form_list, depends @ [name], requires, session, request | MatchGenericType sessiont ssn_type -> form_list, depends, requires, session @ [name], request | MatchGenericType formt form_type -> form_list, depends, requires, session @ [name], request | _ -> raise (new ApplicationException(sprintf "%A is not a known resource marker" parm_type))) else // if this value has been marked by any of these, then we do nothing // otherwise, we assume it to be a requires request value if any_have name [session; form_list] then state elif any_have name [depends] then form_list, depends, requires, session, request @ [name] else state /// builds a list of form, depends, requires, session and request fields /// based on a function signature. signature is a list of string * Type /// describing the input and output parameters of the function we're dealing with. let load signature = List.fold_left (decompose parameter_fields) ([],[],[],[],[]) signature
Note - i hacked this out of a live project, and I'm sure there are some dangling references. I can provide the full (compiling) code to this for those interested.
That's it for now.