発端
DEKO さんのこの記事
総和 (Σ) 関数を書く
https://qiita.com/ht_deko/items/52b76827682e28403f46
事前に
とあったので!Delphi Advent Calendar 2021、5 日目のネタ予告。
— DEKO (@ht_deko) December 2, 2021
■ 総和 (Σ) 関数を書く
さぁ、みんなで考えよ~! pic.twitter.com/jWqBEcpe7V
今回は Expression Engine の詳解も兼ねて書いてみました。
方針
Delphi RTL に搭載されている LiveBindings を裏で支える仕組み System.Bindings.* を使って、文字列を式として評価させます。
文字列を式として評価させる関数を一般的に Eval といいます。
System.Bindings にはまんま System.Bindings.EvalSys や System.Bindings.Evaluator といったユニットがあります。
Expression Engine
では、この Bindings の仕組みを使って文字列を計算式として評価させてみます。
基本的には以下の2ステップで評価できます。
- Scope を作り変数や定数、関数を登録する
- 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);
次に複雑な方
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);
単純な式を評価させる簡単な方法
今回のΣ関数のように 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,πなど) が定義された状態になっています
と書きました。
つまり代表的ではない演算子を定義できるのではないか、つまり ** や ^ といった記号を使って冪乗演算子を定義できるのでは??と思ったのですが、無理でした。
理由は、以下のように想定された記号だけが演算子として機能するようにハードコーディングされているためです。
ここに定義されていない記号が入っていた場合は例外があがってしまいます。
// 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 | 否定 |
その他の記号一覧
記号 | 意味 |
---|---|
( | 括弧左 |
) | 括弧右 |
[ | 角括弧左 |
] | 角括弧右 |
. | レコード、クラスのメンバー参照 |
, | 引数のセパレータ |
動作未定義記号一覧
記号の定義だけされていて動作が未定義のものです。
式としての評価はできません。
記号 | 意味 |
---|---|
:= | 代入 |
; | 区切り |