We have seen how the evaluation engine recursively evaluates an expression tree and associates a value with each Expression
object. We have also seen how the values associated with the Expression
object are examined only by the individual function processors. Let's look at the function processor for the "+" function again
case "+":
{
try
{
return Functor.SetValue(Arguments.Aggregate(0.0d, (result, x) => result += x.Evaluate().ValueAsNumber);
}
catch
{ }
return Functor.SetNil();
}
We notice the generic Aggregate
function on Arguments
returns a double
, and consequently the lambda expression returns a double
, using the native +=
operator to operate on double
values. This means that we have to somehow express the value of the Expression x
as a double. Further, we need to ensure that the value associated with the Functor
is henceforth interpretable as a double.
We also need to handle the case where it cannot be expressed as a number, and we'll deal with that in a moment.
We can also imagine other scalar types - like strings and boolean values - of immense importance when we want to do comparison expressions like:
(> a b)
We therefore create a Variant
class (again a throwback to my C/C++ days) where a class can encapsulate a given value and represent it, if possible, in various types. In our case, we are going to support strings, numbers (doubles) and booleans, so our variant class looks something like:
public class VariantValue
{
public E_VALUETYPE ValueType { get; protected internal set; }
public object UntypedValue { get; protected internal set; }
public virtual bool IsNumber
{
get
{
try
{
CastToNumber(this.UntypedValue);
return true;
}
catch
{
return false;
}
}
}
protected static double CastToNumber(object value)
{
try
{
return System.Convert.ToDouble(value);
}
catch
{
throw new InvalidCastException("Cannot express (" + value.ToString() + ") as a Number");
}
}
public virtual double ValueAsNumber
{
get
{
try
{
return CastToNumber(this.UntypedValue);
}
catch
{
return double.NaN;
}
}
set
{
this.UntypedValue = value;
this.ValueType = E_VALUETYPE.Number;
}
}
#region Boolean ...
#region String ...
protected internal virtual VariantValue SetValue(bool value) { this.ValueAsBoolean = value; return this; }
protected internal virtual VariantValue SetValue(double value) { this.ValueAsNumber = value; return this; }
protected internal virtual VariantValue SetValue(string value) { this.ValueAsString = value; return this; }
protected internal virtual VariantValue SetNil() ...
protected internal virtual VariantValue SetValue(VariantValue value) ...
protected internal virtual VariantValue SetValue(object value) ...
}
We can see how the VariantValue
class stores the raw value in the UntypedValue
property and tries to represent it as a Number, Boolean or String when required. This is important because a value can have more than one representation.
The Is...
accessor properties allow callers to query if the value can be represented in the appropriate type.
The ValueAs...
accessor properties return the value as the appropriate type if possible. They will never throw any exceptions. They are helped by the internal CastTo...
functions, which do throw exceptions. When we examine the SetValue(object)
function, we'll see how the exceptions play a crucial role in helping us decide how best to represent a generic object.
The ValueAs...
mutator properties save the raw value in the UntypedValue
property. Since the type of the value is known to them, they also store the type information in the ValueType
property.
The code below is the SetValue(object)
function. It is called when the type-specific version of SetValue
cannot be called (for example when the value is obtained from a symbol table - look at the section on integration).
We successively attempt to coerce the value into one of the three known types, using the exceptions thrown from the CastTo...
functions to signal that coercion was unsuccessful.
protected internal virtual VariantValue SetValue(object value)
{
if (value == null) { return SetNil(); }
// [true] can be coerced to a number if we don't do this
if ((value is long) || (value is double))
{
this.ValueAsNumber = CastToNumber(value); return this;
}
if (value is bool)
{
this.ValueAsBoolean = CastToBoolean(value); return this;
}
try
{
this.ValueAsNumber = CastToNumber(value);
return this;
}
catch
{ }
try
{
this.ValueAsBoolean = CastToBoolean(value);
return this;
}
catch
{ }
try
{
this.ValueAsString = CastToString(value);
return this;
}
catch
{ }
this.UntypedValue = value;
this.ValueType = E_VALUETYPE.Uninitialized;
return this;
}
Since every Expression
object is associated with a value, we can make life easy and simply derive the Expression
class from the VariantValue
class. We are effectively saying that an Expression
is a VariantValue
.
public class Expression : VariantValue
...
We're close to putting the finishing touches on our expression evaluator. The last post in this series will deal with integration with the calling environment, allowing us to use the evaluator within the context of any other application.
No comments:
Post a Comment