The Road to Delphi

Delphi – Free Pascal – Oxygene

A quick guide to evaluate and compile expressions using the LiveBindings expression evaluator.

4 Comments

The LiveBindings technology introduced in Delphi XE2, includes a set of interfaces, classes and methods to evaluate and compile expressions.

Today I will show you how you can use these classes to build, compile and evaluate simple (and complex) expressions. You can use these expressions for example to define formulas to calculate taxes, generate hashes, encrypt data or use in any situation where you need calculate a value where the values or factors may change (anyway the possibilities are endless), Also you can store these expressions in a XML file or a database and use them as needed.

Note: In this article is not used the TBindingExpression class directly, instead are used the raw classes and methods of the of livebindings expression evaluator, because you can gain much more flexibility to build your expressions.

Before to begin is necessary know the basic elements to build, compile and evaluate an expression.

  • The IScope is the base Interface to hold the objects used to make the evaluation and compilation, here you store the methods , classes and values which will be used to build the expression.
  • The Compile method located in the System.Bindings.Evaluator unit, is used to compile the expression using a IScope interface, this method will return a ICompiledBinding interface which can be used to evaluate and get the result of the compilation.
  • The ICompiledBinding interface allows the evaluation of the compiled expression.
  • The TNestedScope class allows you to merge IScopes.

Basic Example

The more basic expression which you can use is based in the BasicOperators Scope (located in the System.Bindings.EvalSys unit), this allows you evaluate only numbers and the basic arithmetic operations, check this sample.

{$APPTYPE CONSOLE}

uses
  System.Rtti,
  System.Bindings.EvalProtocol,
  System.Bindings.Evaluator,
  System.Bindings.EvalSys,
  System.SysUtils;

procedure DoIt;
Var
  LScope : IScope;
  LCompiledExpr : ICompiledBinding;
  LResult : TValue;
begin
  LScope:= BasicOperators;
  LCompiledExpr:= Compile('((1+2+3+4)*(25/5))-(10)', LScope);
  LResult:=LCompiledExpr.Evaluate(LScope, nil, nil).GetValue;
  if not LResult.IsEmpty then
    Writeln(LResult.ToString);
end;

begin
 try
    DoIt;
 except
    on E:Exception do
        Writeln(E.Classname, ':', E.Message);
 end;
 Writeln('Press Enter to exit');
 Readln;
end.

Registering a Constant

Now if you need evaluate the value of a constant you must create a IScope descendent and add the constants to register, finally you must merge the new scope with the original using the TNestedScope.


{$APPTYPE CONSOLE}

uses
  System.Rtti,
  System.Bindings.EvalProtocol,
  System.Bindings.Evaluator,
  System.Bindings.EvalSys,
  System.SysUtils;

procedure DoIt;
Var
  LScope : IScope;
  LCompiledExpr : ICompiledBinding;
  LResult : TValue;
  LDictionaryScope: TDictionaryScope;
begin
  LScope:= TNestedScope.Create(BasicOperators, BasicConstants);
  LDictionaryScope := TDictionaryScope.Create;
  //add a set of constants to the Scope
  LDictionaryScope.Map.Add('MinsPerHour', TValueWrapper.Create(MinsPerHour));
  LDictionaryScope.Map.Add('MinsPerDay', TValueWrapper.Create(MinsPerDay));
  LDictionaryScope.Map.Add('MSecsPerSec', TValueWrapper.Create(MSecsPerSec));
  LDictionaryScope.Map.Add('MSecsPerDay', TValueWrapper.Create(MSecsPerDay));

  //merge the scopes
  LScope:= TNestedScope.Create(LScope, LDictionaryScope);

  LCompiledExpr:= Compile('MinsPerHour*24', LScope);
  LResult:=LCompiledExpr.Evaluate(LScope, nil, nil).GetValue;
  if not LResult.IsEmpty then
    Writeln(LResult.ToString);

end;

begin
 try
    DoIt;
 except
    on E:Exception do
        Writeln(E.Classname, ':', E.Message);
 end;
 Writeln('Press Enter to exit');
 Readln;
end.

In the above code the Scope is initialized using the TNestedScope class to merge the BasicOperators and BasicConstants (this Scope define the values True, False, nil, and Pi) scopes

  LScope:= TNestedScope.Create(BasicOperators, BasicConstants);

Tip 1 : The constants and identifiers in the expression are case-sensitive.

Using Methods

The livebindings expression evaluator include a set of basic methods (ToStr, ToVariant, ToNotifyEvent, Round, Format, UpperCase, LowerCase, FormatDateTime, StrToDateTime, Math_Min, Math_Max) which can be used in our expressions, these are defined in the System.Bindings.Methods unit and must be accessed the TBindingMethodsFactory class.

Check this sample code which uses the Format function.

{$APPTYPE CONSOLE}

uses
  System.Rtti,
  System.Bindings.EvalProtocol,
  System.Bindings.Evaluator,
  System.Bindings.EvalSys,
  System.Bindings.Methods,
  System.SysUtils;

procedure DoIt;
Var
  LScope : IScope;
  LCompiledExpr : ICompiledBinding;
  LResult : TValue;
  LDictionaryScope: TDictionaryScope;
begin
  LScope:= TNestedScope.Create(BasicOperators, BasicConstants);
  //add the registered methods
  LScope := TNestedScope.Create(LScope, TBindingMethodsFactory.GetMethodScope);
  LCompiledExpr:= Compile('Format("%s using the function %s, this function can take numbers like %d or %n as well","This is a formated string","Format",36, Pi)', LScope);
  LResult:=LCompiledExpr.Evaluate(LScope, nil, nil).GetValue;
  if not LResult.IsEmpty then
    Writeln(LResult.ToString);
end;

begin
 try
    DoIt;
 except
    on E:Exception do
        Writeln(E.Classname, ':', E.Message);
 end;
 Writeln('Press Enter to exit');
 Readln;
end.

Tip 2 : The strings in the expressions can be surrounded in double or single quotes.

Registering a Custom Method

Most of the times when you build an expression, you will need register a custom method, this can be easily done using the TBindingMethodsFactory class.

The first step is create a function wich returns a IInvokable interface. For this you can use the MakeInvokable method and then you write the implementation of your function as an anonymous method.

Finally using the TBindingMethodsFactory.RegisterMethod function you can register the custom method.

Check this sample code which implement a custom function called IfThen :)

{$APPTYPE CONSOLE}

uses
  System.Rtti,
  System.TypInfo,
  System.Bindings.Consts,
  System.Bindings.EvalProtocol,
  System.Bindings.Evaluator,
  System.Bindings.EvalSys,
  System.Bindings.Methods,
  System.SysUtils;

{
function IfThen(AValue: Boolean; const ATrue: Integer; const AFalse: Integer): Integer;
function IfThen(AValue: Boolean; const ATrue: Int64; const AFalse: Int64): Int64;
function IfThen(AValue: Boolean; const ATrue: UInt64; const AFalse: UInt64): UInt64;
function IfThen(AValue: Boolean; const ATrue: Single; const AFalse: Single): Single;
function IfThen(AValue: Boolean; const ATrue: Double; const AFalse: Double): Double;
function IfThen(AValue: Boolean; const ATrue: Extended; const AFalse: Extended): Extended;
}
function IfThen: IInvokable;
begin
  Result := MakeInvokable(
    function(Args: TArray<IValue>): IValue
      var
        IAValue: IValue;
        AValue: Boolean;
        IATrue, IAFalse: IValue;
     begin
        //check the number of passed parameters
        if Length(Args) <> 3 then
          raise EEvaluatorError.Create(sFormatArgError);

         IAValue:=Args[0];
         IATrue :=Args[1];
         IAFalse:=Args[2];

         //check if the parameters has values
         if IATrue.GetValue.IsEmpty or IAFalse.GetValue.IsEmpty then
          Exit(TValueWrapper.Create(nil))
         else
         //check if the parameters has the same types
         if IATrue.GetValue.Kind<>IAFalse.GetValue.Kind then
          raise EEvaluatorError.Create('The return values must be of the same type')
         else
         //check if the first parameter is boolean
         if (IAValue.GetType.Kind=tkEnumeration) and (IAValue.GetValue.TryAsType<Boolean>(AValue)) then //Boolean is returned as tkEnumeration
         begin
           if AValue then
            //return the value for True condition
            Exit(TValueWrapper.Create(IATrue.GetValue))
           else
            //return the value for the False condition
            Exit(TValueWrapper.Create(IAFalse.GetValue))
         end
         else raise EEvaluatorError.Create('The first parameter must be a boolean expression');
     end
     );
end;

procedure DoIt;
Var
  LScope : IScope;
  LCompiledExpr : ICompiledBinding;
  LResult : TValue;
  LDictionaryScope: TDictionaryScope;
begin
  LScope:= TNestedScope.Create(BasicOperators, BasicConstants);

    //add a custom method
    TBindingMethodsFactory.RegisterMethod(
        TMethodDescription.Create(
          IfThen,
          'IfThen',
          'IfThen',
          '',
          True,
          '',
          nil));

  //add the registered methods
  LScope := TNestedScope.Create(LScope, TBindingMethodsFactory.GetMethodScope);
  LCompiledExpr:= Compile('Format("The sentence is %s", IfThen(1>0,"True","False"))', LScope);
  LResult:=LCompiledExpr.Evaluate(LScope, nil, nil).GetValue;
  if not LResult.IsEmpty then
    Writeln(LResult.ToString);
end;

begin
 try
    DoIt;
 except
    on E:Exception do
        Writeln(E.Classname, ':', E.Message);
 end;
 Writeln('Press Enter to exit');
 Readln;
end.

Tip 3 : Remember use TBindingMethodsFactory.UnRegisterMethod function to unregister your custom method.

Registering a Class

In order to use your own class in an expression you must create a Scope using the TObjectWrapper class or the WrapObject method.

{$APPTYPE CONSOLE}

uses
  System.Rtti,
  System.TypInfo,
  System.Bindings.Consts,
  System.Bindings.EvalProtocol,
  System.Bindings.Evaluator,
  System.Bindings.EvalSys,
  System.Bindings.ObjEval,
  System.SysUtils;

Type
 TMyClass= class
  function Random(Value:Integer): Integer;
 end;

{ TMyClass }
function TMyClass.Random(Value:Integer): Integer;
begin
  Result:=System.Random(Value);
end;

procedure DoIt;
Var
  LScope : IScope;
  LCompiledExpr : ICompiledBinding;
  LResult : TValue;
  LDictionaryScope: TDictionaryScope;
  M : TMyClass;
begin
  M := TMyClass.Create;
  try
    LScope:= TNestedScope.Create(BasicOperators, BasicConstants);
    //add a object
    LDictionaryScope := TDictionaryScope.Create;
    LDictionaryScope.Map.Add('M', WrapObject(M));
    LScope := TNestedScope.Create(LScope, LDictionaryScope);
    LCompiledExpr:= Compile('M.Random(10000000)', LScope);
    LResult:=LCompiledExpr.Evaluate(LScope, nil, nil).GetValue;
    if not LResult.IsEmpty then
      Writeln(LResult.ToString);
  finally
    M.Free;
  end;
end;

begin
 try
    Randomize;
    DoIt;
 except
    on E:Exception do
        Writeln(E.Classname, ':', E.Message);
 end;
 Writeln('Press Enter to exit');
 Readln;
end.

Author: Rodrigo

Just another Delphi guy.

4 thoughts on “A quick guide to evaluate and compile expressions using the LiveBindings expression evaluator.

  1. Very good article
    but in what unit is declared TArray type for IfThen
    it exists in XE2?

  2. If you want to use a variable in the expression handler that you can update without
    recompling the expression in the register constant example change the

    LDictionaryScope.Map.Add('MinsPerHour', TValueWrapper.Create(MinsPerHour));
    

    to

    LDictionaryScope.Map.Add('MinsPerHour', TValueWrapperIndirect.Create(@variable));
    

    then declare

      variable : TValue; // somewhere
    
    
    // simple implementation of address of TValue for expression Handler
    PValue = ^TValue;
    TValueWrapperIndirect = class(TInterfacedObject, IWrapper, IValue, ILocation)
      Fvalue      : PValue;
      function GetType: PTypeInfo;
      function GetValue: TValue;
      procedure SetValue(const AValue: TValue);
      constructor create(AValue: Pointer);
    end;
    
    { TValueWrapperIndirect }
    
    constructor TValueWrapperIndirect.Create(AValue: pointer);
    begin
      FValue := AValue;
    end;
    
    function TValueWrapperIndirect.GetType: PTypeInfo;
    begin
      Result := FValue^.TypeInfo;
    end;
    
    function TValueWrapperIndirect.GetValue: TValue;
    begin
      Result := FValue^;
    end;
    
    procedure TValueWrapperIndirect.SetValue(const AValue: TValue);
    begin
      FValue^ := AValue;
    end;
    

Leave a comment