7
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Delphi Expression Engine 詳解 (Re: 総和 (Σ) 関数を書く)

Last updated at Posted at 2021-12-27

発端

DEKO さんのこの記事

総和 (Σ) 関数を書く
https://qiita.com/ht_deko/items/52b76827682e28403f46

事前に

とあったので!

今回は Expression Engine の詳解も兼ねて書いてみました。

方針

Delphi RTL に搭載されている LiveBindings を裏で支える仕組み System.Bindings.* を使って、文字列を式として評価させます。
文字列を式として評価させる関数を一般的に Eval といいます。
System.Bindings にはまんま System.Bindings.EvalSys や System.Bindings.Evaluator といったユニットがあります。

Expression Engine

では、この Bindings の仕組みを使って文字列を計算式として評価させてみます。
基本的には以下の2ステップで評価できます。

  1. Scope を作り変数や定数、関数を登録する
  2. Expression を生成し評価させる

1. Scope を作り変数や定数、関数を登録する

Scope とは、いわゆる可視性の意味のスコープとほぼ同じ意味です。
つまり、Expression Engine に対して、こういう定数や関数が見えますよ、と教えてあげるものです。

Scope は IScope インターフェースとして定義されています。
IScope を返すクラスは System.Bindings.EvalSys に定義されています。

IScope 実装クラス

クラス名 意味
TPairScope メソッドを表す Key Value 型
TObjectMemberGroupScope 1つの入力に対して複数の値を返す型

IScope のコレクション

クラス名 意味
TDictionaryScope Scope を KeyValue で管理するディクショナリ
TNestedScope 階層化された Scope のコレクション
TNamespaceScope 名前空間による Scope のコレクション

これらは全て IScope インターフェースを返します。
基底クラスは TInterfacedObject なので自動的に解放されます。

よく使うのは TPairScope と TDictionaryScope, TNestedScope ぐらいでしょう。

TDictionaryScope を使って定数を定義してみると…

var Scope := TDictionaryScope.Create;
Scope.Map.Add('Foo', TValueWrapper.Create(42));

こんな感じになります。
TDictionaryScope の Key は String, Value は IInterface 型(Interface の基底型)です。
TValueWrapper を使うと通常の値を IValue として表せるので、これを使って Value の値を設定します。

これで式中に Foo という文字列があった場合、42 が入っている定数として処理されます。

次に TPairScope を使ってメソッドを定義すると…

var Scope := 
  TPairScope.Create(
    'Square', 
    MakeInvokable(
      function(Args: TArray<IValue>): IValue
      begin
        var V := Args[0].GetValue.AsExtended;
        V := V * V;
        Result := TValueWrapper.Create(V);
      end
    )
  );

こんな感じになります。
TPairScope の Key は String, Value は IInvokable 型です。
MakeInvokable 関数を使うと IInvokable として処理を返せます。
上記の例では Square という名前で自乗した値を返す処理を書いてみました。

これで、変数同様、式中に Square という文字列があった場合、自乗を返す関数として処理されます。

2. Expression を生成し評価させる

TBindingExpression が実際に式を評価するクラスです。
今回はこれを継承した TBindingExpressionDefault を使います。
このクラスは演算子(+,-,*,/ など)や代表的な定数(True,False,πなど) が定義された状態になっています。
こちらで演算子を定義する必用がないため、基本的にはこれを使う機会が多そうです。

実際の使用方法は以下のコードの通りです。

var Exp := TBindingExpressionDefault.Create;
try
  Exp.Source := 'Square(2) * 42.195'; // 評価したい式を文字列として代入する
  Exp.Compile(Scopes); // コンパイルする。Scope の配列を渡せる

  var ValueIntf := Exp.Evalute; // 評価が成功すると IValue が返ってくる
  var Value := ValueIntf.GetValue; // TValue が取れる

  Result := Value.AsExtended; // TValue を任意の型に変換して返す
finally
  Exp.Free;
end;

Σ関数を作る

上記までに紹介した機能を使ってΣ関数を作ります。

まずは簡単な↓これを

z = \sum_{i=1}^{5}(i+1)

↓こんな風に表すことにします。

var z := Σ('i', 1, 5, 'i + 1');
  • 関数名:Σ
  • 第1引数:変数の名前
  • 第2引数:開始値
  • 第3引数:終了値
  • 第4引数:評価する式(文字列形式)

次に、難しい↓これです。

z = \sum_{i=1}^{100}\frac{1}{(i + a)^2}

この式には a という定数と冪乗があります。
Object Pascal には冪乗演算子がないのでこれは Pow という関数を定義して、そこで計算するようにします。

z = \sum_{i=1}^{100}\frac{1}{Pow(i + a, 2)}

これらを含めて↓のように表すことにしました。

var z :=
  Σ(
    'i',
    1,
    100,
    '1 / Pow((i + a), 2)',
    [TConst.Create('a', 2)],
    [
      TUserFunc.Create(
        'Pow',
        function(Args: TArray<TValue>): TValue
        begin
          if Length(Args) = 2 then
            Result :=
              System.Math.Power(
                Args[0].AsExtended,
                Args[1].AsExtended
              )
          else
            Result := TValue.Empty;
        end
      )
    ]
  );

第4引数までは同じです。
新たに

  • 第5引数:定数の宣言
  • 第6引数:関数の宣言

が追加されました。

第5引数は、TConst というレコード(実装は後述)を利用して名前とその値を定義しています。
オープン配列パラメータにして、複数の定数を渡せるようにしてあります。

[TConst.Create('a', 2)] // 定数の名前, 定数の値

第6引数は、Pow 関数の定義です。
こちらも TUserFunc というレコード(実装は後述)を利用して、それを使って関数を定義しています。
こちらもオープン配列パラメータになっているので複数の関数を定義できます。

[
  TUserFunc.Create(
    'Pow', // 関数の名前
    function(Args: TArray<TValue>): TValue // 関数の実装
    begin
      if Length(Args) = 2 then
        Result :=
          System.Math.Power(
            Args[0].AsExtended,
            Args[1].AsExtended
          )
      else
        Result := TValue.Empty;
    end
  )
]

ではこれを踏まえて、Expression Engine を使ってΣ関数を定義すると、こんな風になりました。
TConst, TUserFunc の定義もここに記載しました。

unit uSigma;

interface

uses
  System.Rtti;

type
  TNamedValue<T> = record
  private var
    FName: String;
    FValue: T;
  public
    constructor Create(const AName: String; const AValue: T);
    property Name: String read FName;
    property Value: T read FValue;
  end;

  TConst = TNamedValue<TValue>;

  TFuncBody = reference to function(Args: TArray<TValue>): TValue;
  TUserFunc = TNamedValue<TFuncBody>;

function Σ(
  const AVarName: String;
  const AFrom, ATo: Integer;
  const AExpression: String;
  const AConstant: TArray<TConst> = [];
  const AMethods: TArray<TUserFunc> = []): Double;

implementation

uses
  System.Bindings.EvalProtocol
  , System.Bindings.EvalSys
  , System.Bindings.Evaluator
  , System.Bindings.Expression
  , System.Bindings.ExpressionDefaults
  , System.Bindings.Helper
  , System.Bindings.Methods
  ;

{ TNamedValue<T> }

constructor TNamedValue<T>.Create(const AName: String; const AValue: T);
begin
  FName := AName;
  FValue := AValue;
end;

function Σ(
  const AVarName: String;
  const AFrom, ATo: Integer;
  const AExpression: String;
  const AConstant: TArray<TConst> = [];
  const AMethods: TArray<TUserFunc> = []): Double;

  function FuncWrapper(const AFunc: TFuncBody): IInvokable;
  begin
    Result :=
      MakeInvokable(
        function(Args: TArray<IValue>): IValue
        begin
          var tmpArgs: TArray<TValue>;
          SetLength(tmpArgs, Length(Args));

          for var i := 0 to High(tmpArgs) do
            tmpArgs[i] := Args[i].GetValue;

          Result := TValueWrapper.Create(AFunc(tmpArgs));
        end
      );
  end;

begin
  Result := 0;

  var Scopes: TArray<IScope>;
  SetLength(Scopes, 1 + Length(AMethods));

  // 定数を Scope に追加
  var VarScope := TDictionaryScope.Create;
  for var C in AConstant do
    VarScope.Map.Add(C.FName, TValueWrapper.Create(C.FValue));

  Scopes[0] := VarScope;

  // ユーザー定義関数を Scope に追加
  for var i := 1 to High(Scopes) do
  begin
    var M := AMethods[i - 1];
    Scopes[i] := TPairScope.Create(M.FName, FuncWrapper(M.FValue));
  end;

  // 実行
  var Exp := TBindingExpressionDefault.Create;
  try
    Exp.Source := AExpression;

    for var i := AFrom to ATo do
    begin
      // for 文で値が更新されるたびに Scope に登録されている変数の値を変更する
      VarScope.Map.AddOrSetValue(AVarName, TValueWrapper.Create(i));
      Exp.Compile(Scopes);
      Result := Result + Exp.Evaluate.GetValue.AsExtended;
    end;
  finally
    Exp.Free;
  end;
end;

end.

Σ関数を呼び出す

ではまずは簡単な方を呼び出します。

var z := Σ('i', 1, 5, 'i + 1');
Writeln(z:1:2);

無事出力されました。
image.png

次に複雑な方

var z :=
  Σ(
    'i',
    1,
    100,
    '1 / Pow((i + a), 2)',
    [TConst.Create('a', 2)],
    [
      TUserFunc.Create(
        'Pow',
        function(Args: TArray<TValue>): TValue
        begin
          if Length(Args) = 2 then
            Result :=
              System.Math.Power(
                Args[0].AsExtended,
                Args[1].AsExtended
              )
          else
            Result := TValue.Empty;
        end
      )
    ]
  );

Writeln(z:1:2);

こちらも無事表示されました。
image.png

単純な式を評価させる簡単な方法

今回のΣ関数のように for ループの中で変数の値を変える必用がない場合は次のような簡単な呼び出し方があります。
必用な処理は TBindings.CreateExpression の中で全部やってくれます。

var Exp := TBindings.CreateExpression(Scopes, AExpression);
try
  Result := Exp.Evaluate.GetValue.AsExtended;
finally
  Exp.Free;
end;

まとめ

繰り返しになりますが、まとめると…

  • 必用なら Scope を作る
  • Expression.Source に式を設定し、Compile, Evalute する
  • 単純な式の評価なら TBindings.CreateExpession を使う

たったこれだけで、Eval 相当の物が作れてしまいます!
まれに Eval があったらな~と思う事があるので、そう言った機会にもわりと手軽に対応できます!

と、いうことで、ExpressionEngine を解説しました。
そのため純粋なΣ関数と違って式を文字列で渡すことになってしまったので、若干ルール違反かな~と思っています。

おまけ

演算子の定義について

TBindingsExpressionDefault のところで

このクラスは演算子(+,-,*,/ など)や代表的な定数(True,False,πなど) が定義された状態になっています

と書きました。
つまり代表的ではない演算子を定義できるのではないか、つまり ** や ^ といった記号を使って冪乗演算子を定義できるのでは??と思ったのですが、無理でした。
理由は、以下のように想定された記号だけが演算子として機能するようにハードコーディングされているためです。
ここに定義されていない記号が入っていた場合は例外があがってしまいます。

System.Bindings.Evaluator.pasから抜粋(458行~)
// single-character operators
'+': SetToken(Self, tkPlus, cp + 1);
'-': SetToken(Self, tkMinus, cp + 1);
'*': SetToken(Self, tkAsterisk, cp + 1);
'/': SetToken(Self, tkSlash, cp + 1);
以下略

ひとつ面白いのは、Pascal では使われない ! が非等価演算子として定義されていることです。
↓こんな風に使えます。

var Exp := TBindings.CreateExpression([], '1 ! 0'); //← 1 <> 0
try
  Result := Exp.Evaluate.GetValue.AsBoolean; // True が返る
finally
  Exp.Free;
end;

Pascal の普通の非等価演算子 <> も使えるので ! が用意されている理由は良くわかりません!(!= だったら解る)

Expression Eingine 記号一覧

演算子一覧

演算子 意味
+ 加算
- 減算, 単項マイナス
* 乗算
/ 除算
= 等価
! 非等価
<> 非等価
< 小なり
> 大なり
<= 以下
>= 以上
and 論理積
or 論理和
xor 排他的論理和
not 否定

その他の記号一覧

記号 意味
( 括弧左
) 括弧右
[ 角括弧左
] 角括弧右
. レコード、クラスのメンバー参照
, 引数のセパレータ

動作未定義記号一覧
記号の定義だけされていて動作が未定義のものです。
式としての評価はできません。

記号 意味
:= 代入
; 区切り
7
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?