F Sharp Programming/Reflection
F# : Reflection |
Reflection allows programmers to inspect types and invoke methods of objects at runtime without knowing their data type at compile time.
At first glance, reflection seems to go against the spirit of ML as it is inherently not type-safe, so typing errors using reflection are not discovered until runtime. However, .NET's typing philosophy is best stated as static typing where possible, dynamic typing when needed, where reflection serves to bring in the most desirable behaviors of dynamic typing into the static typing world. In fact, dynamic typing can be a huge time saver, often promotes the design of more expressive APIs, and allows code to be refactored much further than possible with static typing.
This section is intended as a cursory overview of reflection, not a comprehensive tutorial.
Inspecting Types
editThere are a variety of ways to inspect the type of an object. The most direct way is calling the .GetType()
method (inherited from System.Object
) on any non-null object:
> "hello world".GetType();;
val it : System.Type =
System.String
{Assembly = mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;
AssemblyQualifiedName = "System.String, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089";
Attributes = AutoLayout, AnsiClass, Class, Public, Sealed, Serializable, BeforeFieldInit;
BaseType = System.Object;
ContainsGenericParameters = false;
DeclaringMethod = ?;
DeclaringType = null;
FullName = "System.String";
GUID = 296afbff-1b0b-3ff5-9d6c-4e7e599f8b57;
GenericParameterAttributes = ?;
GenericParameterPosition = ?;
...
Its also possible to get type information without an actual object using the built-in typeof
method:
> typeof<System.IO.File>;;
val it : System.Type =
System.IO.File
{Assembly = mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;
AssemblyQualifiedName = "System.IO.File, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089";
Attributes = AutoLayout, AnsiClass, Class, Public, Abstract, Sealed, BeforeFieldInit;
BaseType = System.Object;
ContainsGenericParameters = false;
DeclaringMethod = ?;
DeclaringType = null;
FullName = "System.IO.File";
...
object.GetType
and typeof
return an instance of System.Type
, which has a variety of useful properties such as:
val Name : string
- Returns the name of the type.
val GetConstructors : unit -> ConstructorInfo array
- Returns an array of constructors defined on the type.
val GetMembers : unit -> MemberInfo array
- Returns an array of members defined on the type.
val InvokeMember : (name : string, invokeAttr : BindingFlags, binder : Binder, target : obj, args : obj) -> obj
- Invokes the specified member, using the specified binding constraints and matching the specified argument list
Example: Reading Properties
editThe following program will print out the properties of any object passed into it:
type Car(make : string, model : string, year : int) =
member this.Make = make
member this.Model = model
member this.Year = year
member this.WheelCount = 4
type Cat() =
let mutable age = 3
let mutable name = System.String.Empty
member this.Purr() = printfn "Purrr"
member this.Age
with get() = age
and set(v) = age <- v
member this.Name
with get() = name
and set(v) = name <- v
let printProperties x =
let t = x.GetType()
let properties = t.GetProperties()
printfn "-----------"
printfn "%s" t.FullName
properties |> Array.iter (fun prop ->
if prop.CanRead then
let value = prop.GetValue(x, null)
printfn "%s: %O" prop.Name value
else
printfn "%s: ?" prop.Name)
let carInstance = new Car("Ford", "Focus", 2009)
let catInstance =
let temp = new Cat()
temp.Name <- "Mittens"
temp
printProperties carInstance
printProperties catInstance
This program outputs the following:
----------- Program+Car WheelCount: 4 Year: 2009 Model: Focus Make: Ford ----------- Program+Cat Name: Mittens Age: 3
Example: Setting Private Fields
editIn addition to discovering types, we can dynamically invoke methods and set properties:
let dynamicSet x propName propValue =
let property = x.GetType().GetProperty(propName)
property.SetValue(x, propValue, null)
Reflection is particularly remarkable in that it can read/write private fields, even on objects which appear to be immutable. In particular, we can explore and manipulate the underlying properties of an F# list:
> open System.Reflection
let x = [1;2;3;4;5]
let lastNode = x.Tail.Tail.Tail.Tail;;
val x : int list = [1; 2; 3; 4; 5]
val lastNode : int list = [5]
> lastNode.GetType().GetFields(BindingFlags.NonPublic ||| BindingFlags.Instance) |> Array.map (fun field -> field.Name);;
val it : string array = [|"__Head"; "__Tail"|]
> let tailField = lastNode.GetType().GetField("__Tail", BindingFlags.NonPublic ||| BindingFlags.Instance);;
val tailField : FieldInfo =
Microsoft.FSharp.Collections.FSharpList`1[System.Int32] __Tail
> tailField.SetValue(lastNode, x);; (* circular list *)
val it : unit = ()
> x |> Seq.take 20 |> Seq.to_list;;
val it : int list =
[1; 2; 3; 4; 5; 1; 2; 3; 4; 5; 1; 2; 3; 4; 5; 1; 2; 3; 4; 5]
The example above mutates the list in place and to produce a circularly linked list. In .NET, "immutable" doesn't really mean immutable and private members are mostly an illusion.
- Note: The power of reflection has definite security implications, but a full discussion of reflection security is far outside of the scope of this section. Readers are encouraged to visit the Security Considerations for Reflection article on MSDN for more information.
Microsoft.FSharp.Reflection Namespace
editWhile .NET's built-in reflection API is useful, the F# compiler performs a lot of magic which makes built-in types like unions, tuples, functions, and other built-in types appear strange using vanilla reflection. The Microsoft.FSharp.Reflection namespace provides a wrapper for exploring F# types.
open System.Reflection
open Microsoft.FSharp.Reflection
let explore x =
let t = x.GetType()
if FSharpType.IsTuple(t) then
let fields =
FSharpValue.GetTupleFields(x)
|> Array.map string
|> fun strings -> System.String.Join(", ", strings)
printfn "Tuple: (%s)" fields
elif FSharpType.IsUnion(t) then
let union, fields = FSharpValue.GetUnionFields(x, t)
printfn "Union: %s(%A)" union.Name fields
else
printfn "Got another type"
Using fsi:
> explore (Some("Hello world"));;
Union: Some([|"Hello world"|])
val it : unit = ()
> explore (7, "Hello world");;
Tuple: (7, Hello world)
val it : unit = ()
> explore (Some("Hello world"));;
Union: Some([|"Hello world"|])
val it : unit = ()
> explore [1;2;3;4];;
Union: Cons([|1; [2; 3; 4]|])
val it : unit = ()
> explore "Hello world";;
Got another type
Working With Attributes
edit.NET attributes and reflection go hand-in-hand. Attributes allow programmers to decorate classes, methods, members, and other source code with metadata used at runtime. Many .NET classes use attributes to annotate code in a variety of ways; it is only possible to access and interpret attributes through reflection. This section will provide a brief overview of attributes. Readers interested in a more complete overview are encouraged to read MSDN's Extending Metadata With Attributes series.
Attributes are defined using [<AttributeName>]
, a notation already seen in a variety of places in previous chapters of this book. The .NET framework includes a number of built-in attributes, including:
- System.ObsoleteAttribute - used to mark source code intended to be removed in future versions.
- System.FlagsAttribute - indicates that an enumeration can be treated as a bit field.
- System.SerializableAttribute - indicates that class can be serialized.
- System.Diagnostics.DebuggerStepThroughAttribute - indicates that the debugger should not step into a method unless it contains a break point.
We can create custom attributes by defining a new type which inherits from System.Attribute
:
type MyAttribute(text : string) =
inherit System.Attribute()
do printfn "MyAttribute created. Text: %s" text
member this.Text = text
[<MyAttribute("Hello world")>]
type MyClass() =
member this.SomeProperty = "This is a property"
We can access attribute using reflection:
> let x = new MyClass();;
val x : MyClass
> x.GetType().GetCustomAttributes(true);;
MyAttribute created. Text: Hello world
val it : obj [] =
[|System.SerializableAttribute {TypeId = System.SerializableAttribute;};
FSI_0028+MyAttribute {Text = "Hello world";
TypeId = FSI_0028+MyAttribute;};
Microsoft.FSharp.Core.CompilationMappingAttribute
{SequenceNumber = 0;
SourceConstructFlags = ObjectType;
TypeId = Microsoft.FSharp.Core.CompilationMappingAttribute;
VariantNumber = 0;}|]
The MyAttribute
class has the side-effect of printing to the console on instantiation, demonstrating that MyAttribute
does not get constructed when instances of MyClass
are created.
Example: Encapsulating Singleton Design Pattern
editAttributes are often used to decorate classes with any kind of ad-hoc functionality. For example, let's say we wanted to control whether single or multiple instances of classes are created based on an attribute:
open System
open System.Collections.Generic
[<AttributeUsage(AttributeTargets.Class)>]
type ConstructionAttribute(singleInstance : bool) =
inherit Attribute()
member this.IsSingleton = singleInstance
let singletons = Dictionary<System.Type,obj>()
let make<'a>() : 'a =
let newInstance() = Activator.CreateInstance<'a>()
let attributes = typeof<'a>.GetCustomAttributes(typeof<ConstructionAttribute>, true)
let singleInstance =
if attributes.Length > 0 then
let constructionAttribute = attributes.[0] :?> ConstructionAttribute
constructionAttribute.IsSingleton
else false
if singleInstance then
match singletons.TryGetValue(typeof<'a>) with
| true, v -> v :?> 'a
| _ ->
let instance = newInstance()
singletons.Add(typeof<'a>, instance)
instance
else newInstance()
[<ConstructionAttribute(true)>]
type SingleOnly() =
do printfn "SingleOnly constructor"
[<ConstructionAttribute(false)>]
type NewAlways() =
do printfn "NewAlways constructor"
let x = make<SingleOnly>()
let x' = make<SingleOnly>()
let y = make<NewAlways>()
let y' = make<NewAlways>()
printfn "x = x': %b" (x = x')
printfn "y = y': %b" (y = y')
Console.ReadKey(true) |> ignore
This program outputs the following:
SingleOnly constructor NewAlways constructor NewAlways constructor x = x': true y = y': false
Using the attribute above, we've completely abstracted away the implementation details of the singleton design pattern, reducing it down to a single attribute. Its worth noting that the program above hard-codes a value of true
or false
into the attribute constructor; if we wanted to, we could pass a string representing a key from the application's config file and make class construction dependent on the config file.