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

# 発端

DEKO さんのこの記事
> 総和 (Σ) 関数を書く
> https://qiita.com/ht_deko/items/52b76827682e28403f46

事前に
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">Delphi Advent Calendar 2021、5 日目のネタ予告。<br><br>■ 総和 (Σ) 関数を書く<br><br>さぁ、みんなで考えよ～！ <a href="https://t.co/jWqBEcpe7V">pic.twitter.com/jWqBEcpe7V</a></p>&mdash; DEKO (@ht_deko) <a href="https://twitter.com/ht_deko/status/1466327351562625031?ref_src=twsrc%5Etfw">December 2, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
とあったので！

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

# 方針

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

# Expression Engine 

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

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

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

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

**IScope 実装クラス**

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

**IScope のコレクション**

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

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

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

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

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

こんな感じになります。
TDictionaryScope の Key は String, Value は IInterface 型(Interface の基底型)です。
[TValueWrapper](https://docwiki.embarcadero.com/Libraries/Sydney/ja/System.Bindings.EvalProtocol.TValueWrapper) を使うと通常の値を IValue として表せるので、これを使って Value の値を設定します。

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

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

```pascal
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](https://docwiki.embarcadero.com/Libraries/Sydney/ja/System.Bindings.Expression.TBindingExpression) が実際に式を評価するクラスです。
今回はこれを継承した [TBindingExpressionDefault](https://docwiki.embarcadero.com/Libraries/ja/System.Bindings.ExpressionDefaults.TBindingExpressionDefault) を使います。
このクラスは演算子(+,-,*,/ など)や代表的な定数(True,False,πなど) が定義された状態になっています。
こちらで演算子を定義する必用がないため、基本的にはこれを使う機会が多そうです。

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

```pascal
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;
```

# Σ関数を作る

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

まずは簡単な↓これを

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

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

```pascal
var z := Σ('i', 1, 5, 'i + 1');
```

* 関数名：Σ
* 第１引数：変数の名前
* 第２引数：開始値
* 第３引数：終了値
* 第４引数：評価する式（文字列形式）

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

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

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

```math
z = \sum_{i=1}^{100}\frac{1}{Pow(i + a, 2)}
```
これらを含めて↓のように表すことにしました。

```pascal
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
      )
    ]
  );
```

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

* 第５引数：定数の宣言
* 第６引数：関数の宣言

が追加されました。

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

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

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

```pascal
[
  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 の定義もここに記載しました。

```pascal
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.
```

# Σ関数を呼び出す

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

```pascal
var z := Σ('i', 1, 5, 'i + 1');
Writeln(z:1:2);
```
無事出力されました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12977/35c39803-42ed-229e-2514-f9d560902f71.png)

次に複雑な方

```pascal
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12977/9522319b-f966-8902-ddeb-f5712291001c.png)

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

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

```pascal
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,πなど) が定義された状態になっています

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

```pascal: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 では使われない ```!``` が非等価演算子として定義されていることです。
↓こんな風に使えます。

```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|否定|

**その他の記号一覧**

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

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

|記号|意味|
|:----|:----|
|:=|代入|
|;|区切り|