Let’s take a look at how the base from which all Push types derive is defined.
namespace push.types [<AutoOpen>] module Type = open System.Reflection open push.exceptions open System.Diagnostics open System let Random = Random (int DateTime.UtcNow.Ticks) [<DebuggerDisplay("Value = {Value}")>] [<StructuredFormatDisplay("{StructuredFormatDisplay}")>] [<AbstractClass>] type PushTypeBase () = [<DefaultValue>] val mutable private value : obj [<DefaultValue>] val mutable private myType : string new (v) as this = PushTypeBase() then this.value <- v member t.Value with get() = t.value static member private GetMyType (me : #PushTypeBase) = if System.String.IsNullOrEmpty(me.myType) then me.myType <- (me.GetType().GetCustomAttributes(typeof<PushTypeAttribute>, false).[0] :?> PushTypeAttribute).Name me.myType // if something more than default action is necessary // this should be overridden abstract member Eval : PushTypeBase default t.Eval = t abstract member MyType : string with get default t.MyType with get () = PushTypeBase.GetMyType(t) abstract member isQuotable : bool with get default t.isQuotable with get () = false member t.Raw<'a> () = match t.Value with | π 'a as raw -> raw | _ -> failwithf "could not convert to the right type" abstract StructuredFormatDisplay : obj default t.StructuredFormatDisplay = box t.Value override t.ToString() = t.Value.ToString() abstract Parser : ExtendedTypeParser with get and // override this delegate to parse extended types ExtendedTypeParser = delegate of string -> PushTypeBase
Push types are represented (in all .NET languages) by classes derived from PushTypeBase. This is an abstract class and the listing above shows its definition.
Two properties are essential for all classes, deriving from PushTypeBase:
β’ Value β an object that represents the actual value of the instance of this type
β’ MyType – name of the Push type (i.e. INTEGER) that this object represents.
Implementation of the Value property is straightforward. MyType is slightly harder because it needs to be bootstrapped for each individual push type. In other words, once the instance of this type is created, we need to actually discover its Push type.
An easy way out would be to just let the developer set this property at object instantiation, but since Push types are already decorated by attributes, that specify their names, which is necessary for dynamic discovery, I did not want to introduce code duplication.
So, the function that drives this virtual property, GetMyType, actually takes an instance of the PushTypeBase derived class:
static member private GetMyType (me : #PushTypeBase) =
The β#β in type annotation means βany class, that has #PushTypeBase somewhere in its derivation hierarchy.
This function actually looks at the instance of the class it is given and extracts the name of Push type from its custom attributes. It only does that once, on first call. In the implementations of PushTypeBase it is necessary to define a variable that would refer to an instance of the same type. Kind of ugly, but avoids code duplication. MyType can then be accessed through this variable: Me.MyType
[<PushType("FLOAT")>] type Float = inherit PushTypeBase static member Me = Float() override t.ToString() = t.Raw<float>().ToString("F")
Another function to note is Raw(). This property unboxes the actual value of the Value property and presents it as an F# object or value. A type parameter is necessary for that. For instance, the FLOAT type can return string representations of its objects by overriding the .NET object ToString() functions as shows in the above example.
Noteworthy Stuff
:
1. [<AutoOpen>] attribute at the module level instructs F# to open the module automatically, once its containing namespace is being open. I find it very convenient.
2.
[<DebuggerDisplay("Value = {Value}")>] [<StructuredFormatDisplay("{StructuredFormatDisplay}")>] [<AbstractClass>]
Decorating classes with DebuggerDisplay and StructuredFormatDisplay attributes, and then implementing custom displays saves a lot of time debugging and should generally be done for most if not every object.
Here is a snapshot taken during execution of a simple test:
[TestMethod] public void DoSimple() { var prog = "(CODE.QUOTE (5 INTEGER.+) CODE.QUOTE (3 5 INTEGER.*) CODE.DO CODE.DO)"; Program.ExecPush(prog); Assert.AreEqual(20, TestUtils.Top<long>("INTEGER")); }
This program does the following:
1. Pushes (5 INTEGER.+) on the CODE stack 2. Pushes (3 5 INTEGER.*) on the CODE stack 3. Executes the program on top of the CODE stack (the result is 3 *5 pushed to INTEGER stack) 4. Executes the program on top of the CODE stack (the result is 5 pushed to INTEGER tack and then 5 + 15 pushed on top of the INTEGER stack.
Here is a snapshot of the interpreter state taken during this execution.
Because of DebuggerDisplay and StructuredFormatDisplay for various objects, the state of the INTEGER stack can be clearly observed.
