Skip to content

Expr и Eval - вычисление выражений в разных контекстах

edited March 18 in Tutorials

Функция Expr возвращает выражение, а Eval выполняет его в текущем контексте, т.е. с переменными, видимыми из места вызова. Сначала простой пример:

int a, b;
object e=Expr(a+b); //a+b - это не строка, как в eval в JavaScript, а реальное выражение 
a=2; b=3;
int c=e.Eval(int); //в Eval можно задать, какого типа вернуть значение (по умолчанию возвращает null)
//e.Eval(int) - это то же самое, что Eval(e, int). 
WriteLine(c); //5
a=5; b=10;
string str=e.Eval(string);
WriteLine(str); //15

ReadKey();

Выражения в Expr могут быть любой сложности, например:

object e=Expr((5*10-Ceiling(2.55))/2);
WriteLine(e.Eval(string)); //23,5

А с помощью специальных функций можно делать выражения из нескольких последовательных действий:

int a, b, c;
object e=Expr(@(a=1, b=a+2, c=a+b)); //функция @(arg0, arg1, arg2) просто выполняет все действия и возвращает результат последнего (тип object)
WriteLine(e.Eval(string)); //4
WriteLine($"a={a}, b={b}, c={c}"); //a=1, b=3, c=4

e=Expr(_(a=1, b=a+2, c=a+b)); //функция _(arg0, arg1, arg2) выполняет все аргументы и возвращает значение первого
WriteLine(e.Eval(string)); //1

e=Expr(__(c=a+b, a=1, b=a+2)); //функция __(arg0, arg1, arg2) выполняет все аргументы, начиная со второго, и возвращает значение первого (вычисляется последним) 
WriteLine(e.Eval(string)); //4 

Теперь более сложный пример:

int[] arr=new int[]{5, 6, 7};
object someExpr=GetExpr(); //получаем выражение
ForAllElements(arr, someExpr); //передаём в функцию массив, который нужно изменить, и выражение
WriteLine(Join(' ', arr)); //15 16 17 - к каждому элементу прибавилось 10
ReadKey();

int[] ForAllElements(int[] arr, object e){
    //эта функция для каждого индекса элемента массива выполняет выражение, которое меняет значение элемента
    foreach(int index in Range(arr.Length()))
        e.Eval(); //выражение arr[index]+=10 выполняется в контексте функции ForAllElements, т.е. с локальными переменными arr и index

}

object GetExpr(){
    //эта функция создаёт выражение с массивом и индексом
    int[] arr;
    int index;
    return Expr(arr[index]+=10); //суть выражения - прибавление к элементу числа 10
}
ReadKey();

Теперь пример с ошибкой:

int a, b;
object e=Expr(a+b);
Test(e);
ReadKey();

Test(object e){
    int a=2, b=3;
    WriteLine(e.Eval(int)); //0
}

Вместо 5 получили 0! Это потому, что складывались не локальные переменные функции Test, а переменные a и b, которые в первой строке (равны 0). Переменные вне функций - это переменные экземпляра, в данном случае главного класса App, в который оборачивается весь код. Функция Expr взяла выражение, в котором a и b являются экземплярными. Поэтому Eval выполнил 0+0, а не 2+3.
Если выражение должно работать с ref-переменной, то и при получении выражения переменная в нём должна быть переданной по ссылке:

//код выше пропущен
object GetExpr(ref int[] arr){ //при вызове GetExpr надо будет передать какой-нибудь массив (чисто формально)
    int index;
    return Expr(arr[index]+=10); //суть выражения - прибавление к элементу числа 10
}

Конечно, это неудобно, поэтому я сделал функцию SetVarScopeKind, которой можно задавать вид (статическая, экземплярная, локальная, ссылка) переменных в выражении. О ней будет ниже.

Вот ещё пример, в котором выражение для изменения экземплярной переменной формируется в другом классе:

object e=new Bar().GetExpr();
Foo f=new Foo();
f.SetX(e);
f.PrintX(); //555
ReadKey();

class Foo{
    int X;
    public PrintX(){WriteLine(X);}
    public SetX(object e){e.Eval();}
}

class Bar{
    int X;
    public object GetExpr(){return Expr(X=555);} // вместо X=555 можно написать this.X=555
}

Функция GetExpr находится в классе Bar, но для Expr(X=555) это не важно. Главное, что в выражении X будет значиться как экземплярная. И когда выражение выполняется в классе Foo, то X будет ссылаться именно на X в Foo, а не в Bar.
Перепишем для статических переменных:

object e=Bar.Expression;
Foo f=new Foo();
f.SetX(e);
f.PrintX(); //555
ReadKey();

class Foo{
    public static int X; //нужно, чтобы X была публичной, чтобы в Bar можно было создать выражение 
    public PrintX(){WriteLine(X);}
    public SetX(object e){e.Eval();}
}

static class Bar{
    static int X;
    public static object Expression=Expr(Foo.X=555); //если вместо Foo.X=555 написать X=555, то X будет ссылаться на X в Bar
}

На самом деле в этих двух случаях проще создавать выражения прямо в классе Foo, а не в Bar.

Теперь о функции SetVarScopeKind

Эта функция позволяет менять вид (область видимости) переменных в выражении.

Test();

Test(){
    int A, B, C, x;
    object e=Expr(x=A+B+C); //все переменные локальные, а в Foo A и B статические, C экземплярная, а x - ссылка
    e.SetVarScopeKind("Static", A, B); //делаем A и B статическими
    e.SetVarScopeKind("This", C); //делаем C экземплярной
    e.SetVarScopeKind("Ref", x); //делаем x ссылкой
    //Можно одной строкой: e.SetVarScopeKind("Static", A, B).SetVarScopeKind("This", C).SetVarScopeKind("Ref", x);
    int v=5;
    Foo f=new Foo();
    f.Go(e, v);
    WriteLine(v); //9
    e.SetVarScopeKind("Local", C); //делаем C локальной переменной функции
    f.Go(e, v);
    WriteLine(v); //107
}

class Foo{
    static int A=3, B=4;
    int C=2;
    public Go(object e, ref int x){
        int C=100;
        e.Eval();
    }
}

Если меняете тип на статический для уже статической переменной, то привязка её к конкретному классу
сбрасывается.

static int X=123;
object e=Expr(WriteLine(Foo.X));
e.Eval(); //5
e.SetVarScopeKind("Static", X); //X и так статическая, но теперь она не будет привязана к Foo, а будет браться из класса, в котором выполняется выражение
e.Eval(); //123
Bar.EvalExpr(e); //555

class Foo{
    public static int X=5;
}
class Bar{
    public static int X=555;
    public static EvalExpr(object e){
        e.Eval();
    }
}

Вовлечение переменных

В Expr можно указать, какие переменные вовлечь в выражение:

Test();

Test(){
    int x=2, y=3;
    object e=Expr(x+y, x, y); //переменные указываются после выражения
    //ещё можно вовлекать переменные так: e.Involve(x, y);
    EvalAndPrint(e);
}

EvalAndPrint(object e){
    WriteLine(e.Eval(int)); //5 - сработало потому, что были вовлечены переменные из Test()
}

Я специально не использую термины "захват" и "замыкания" потому, что в OverScript это работает немного иначе, и термин "вовлечение" лучше отражает суть. С экземплярами переменными всё как обычно: экземпляры с вовлечёнными в выражение переменными будут существовать, пока существует само выражение. Локальные переменные тоже можно вовлечь, но они не удерживаются выражением, а значит, если выражение создано в функции с её локальными переменными, а потом функция завершила работу, то переменные в выражении станут указывать на неактуальную область памяти.
Давайте посмотрим на пример неправильного вовлечения:

object e = GetExpr();
//в e выражение x+y, но на что ссылаются его переменные, если GetExpr завершила работу и освободила занятую область памяти, в которой находятся x и y?!
EvalAndPrint(e);

object GetExpr(){
    int x=2, y=3;
    return Expr(x+y, x, y); //вовлекаются локальные переменные
}

EvalAndPrint(object e){
    WriteLine(e.Eval(int)); //5 - сработало, хотя не должно было. Как так-то?!
}

Этот пример не должен работать корректно, но дело в том, что по завершении работы функций очищаются только ссылочные переменные, а значимые остаются и потом перезаписываются другими значениями. Это сделано, чтобы не тратить лишнее время на очистку. Вот пример, демонстрирующий перезапись использованных переменных:

object e = GetExpr(); 
//после завершения GetExpr в памяти остаются x=2 и y=3.
SomeFunc(); //SomeFunc будет запущена в той же области памяти, что и GetExpr, а значит перезапишет x и y!
//в e выражение x+y, в котором переменные x и y ссылаются на новые значения
EvalAndPrint(e);

object GetExpr(){
    int x=2, y=3;
    return Expr(x+y, x, y); 
}

EvalAndPrint(object e){
    WriteLine(e.Eval(int)); //25!!! Это 10+15 заданные функцией SomeFunc. Теперь вы видели всё!
}
SomeFunc(){
    int x=10, y=15;
}

Очень важно понять, что происходит в этом примере! Это поможет избежать критических ошибок. Вовлекать локальные переменные можно только тогда, когда Eval будет выполнена до завершения функции, в которой создано выражение.

Ещё один уже правильный пример:

Foo f=new Foo(2, 3);
object e=f.GetSumExpr(); //получаем выражение x+y, где x и y - переменные из Foo 
int v=e.Eval(int);
WriteLine($"{f}, {e}={v}"); //x=2, y=3, x+y=5
f.SetXY(5, 2);
v=e.Eval(int);
WriteLine($"{f}, {e}={v}"); //x=5, y=2, x+y=7

class Foo{
    int x, y;

    New(int x, int y){ //конструктор
        SetXY(x, y);
    }
    public object GetSumExpr(){ //возвращает выражение x+y
        return Expr(x+y, x, y); //указываем не только выражение, а и вовлечённые в него переменные (экземплярные x и y)
    }
    public SetXY(int x, int y){ //устанавливает значения x и y
        this.x=x; 
        this.y=y;
    }
    string ToString(){ //возвращает строковое представление объекта (вызывается автоматически при преобразовании в строку)
        return $"x={x}, y={y}";
    }
}

Экземпляр Foo будет удерживаться от удаления пока существует ссылка на него в выражении.

Статические переменные не подлежат вовлечению, т.к. они и так ссылаются на конкретные места. А ref-переменные нельзя т.к. они являются ссылками (как и в C#).

Упаковка (прикрепление) переменных

Функцией Box можно упаковать в выражение текущие значения указанных переменных.

int x=10;
object[] arr=new object[3];
int count=arr.Length();
for(int i=0; i<count; i++)
    arr[i]=Expr(WriteLine("sum="+(i+x))).Box(i, x); //получение выражения и упаковка в него значений i и x

//в arr теперь следующие выражения:
//WriteLine("i="+(0+10))
//WriteLine("i="+(1+10))
//WriteLine("i="+(2+10))

x=20; //это не повлияет на выражения потому, что Box упаковывает не ссылки, а значения
Test(arr);
//sum=10
//sum=11
//sum=12

Test(object[] arr){
    foreach(object e in arr)
        e.Eval();
}

Можно упаковывать сразу все переменные или только локальные функцией BoxAll:

int X=5;
Test();
ReadKey();

Test(){
    string s="Hello!";
    var e=Expr(WriteLine("s="+s+"; X="+X)).BoxAll(); //BoxAll упаковывает все значения (локальные, ref-переменные, экземплярные и статические)
    s="test"; X=10;
    e.Eval(); //s=Hello!; X=5
    //если написали BoxAll(true), то результат был бы: s=Hello!; X=10
}

Упакованные значения существуют пока существует ссылка на выражение, в котором они упакованы. Потом все задействованные объекты уничтожаются сборщиком мусора.

Переупаковка и перевовлечение
Можно перезаписывать ранее заданные значения. Пример с переупаковкой:

int a=2, b=3;
object e=Expr(WriteLine(a+b)).Box(a, b);
e.Eval(); //5
a=4;
e.Box(a, b).Eval(); //7
b=5;
e.Box(a, b).Eval(); //9

Пример с перевовлечением:

int[] arr;
int index;
object e=Expr(WriteLine(arr[index])); //получение выражения, в котором arr и index экземплярные

new Foo().InvolveVars(e); //в этой функции вовлечение делается в первый раз
//в e будет WriteLine(arr[index]), где arr и index из экземпляра Foo, созданного ранее
Test(e); //30 

new Bar().InvolveVars(e); //а в этой второй раз, т.е. ранее вовлечённые переменные будут перезаданы
//теперь arr и index из экземпляра Bar
Test(e); //200
//экземпляры Foo и Bar будут существовать, пока существует выражение в e


Test(object e){
    e.Eval();
}

class Foo{
    int[] arr=new int[]{10, 20, 30};
    int index=2;
    public InvolveVars(object e){
        e.Involve(arr, index); //вовлечение в выражение arr и index из этого экземпляра Foo
    }
}

class Bar{
    int[] arr=new int[]{100, 200, 300};
    int index=1;
    public InvolveVars(object e){
        e.Involve(arr, index); //вовлечение в выражение arr и index из этого экземпляра Bar
    }
} 

Функции Involve и Box изменяют данные в выражении, и если выражение уже выполняется в другом потоке, то эти изменения отразятся на результате.

int x=10;
object e=GetExpr().Box(x); //сразу упаковываем x=10
object timer=LocalTimer(e.Eval(), 1000, true); //создаётся и запускается таймер, выполняющий выражение
Sleep(5000);
x=15;
e.Box(x); //а теперь упаковываем 15 вместо 10
Sleep(5000);
timer.StopTimer();
//10 10 10 10 10 15 15 15 15 15

object GetExpr(){
    int x;
    return Expr(Write(x+" "));
}

Если вам такое поведение не нужно, то нужно сначала запомнить оригинал выражения функцией FixOrigExpr, а потом на основе копии оригинала, которая возвращается функцией OrigExpr, создавать новые выражения.

int i=5; 
int x=10;
object e=Expr(WriteLine("sum="+(i+x))).Box(i); //получение выражения и упаковка в него значения i
//обратите внимание, что i и x экземплярные, но менять их тип на локальный не обязательно т.к. после упаковки они будут конкретными значениями
e.FixOrigExpr(); //запоминаем текущую версию выражения как оригинал (WriteLine("sum="+(5+x)))
e.Box(x); //упаковка x, но она не затрагивает оригинал
//выражение теперь: WriteLine("sum="+(5+10))

Test(e); //sum=15

//делее выполним выражение, но с новым значением x
x=20; 
e=e.OrigExpr().Box(x); //OrigExpr возвращает копию оригинала, т.е. WriteLine("sum="+(5+x)), а после в неё упаковывается x
//выражение теперь: WriteLine("sum="+(5+20))
Test(e); //sum=25

//повторим с новым x:
x=30; 
e=e.OrigExpr().Box(x);
//выражение теперь: WriteLine("sum="+(5+30))
Test(e); //sum=35

Test(object e){
    e.Eval();
}

В функцию OrigExpr можно передать переменные для вовлечения:

int x=1, y=2;
object e=Expr(WriteLine(x+y)).FixOrigExpr();
e.Box(x, y);
e.Eval(); //3

object e2=new Foo().GetExpr(e);
e2.Eval(); //7 //складываются 4 и 3 из экземпляра Foo

e.Box(x, y);
e.Eval(); //3 // выражение в e осталось прежним

class Foo{
    int x=4, y=3;
    public object GetExpr(object e){
        return e.OrigExpr(x, y); //OrigExpr вернёт копию выражения на момент вызова FixOrigExpr, а также вовлечёт в это новое выражение переменные x и y текущего экземпляра Foo.
        //OrigExpr не затрагивает исходное выражение
    }
}

Нужно понимать, что Box, Involve и SetVarScopeKind изменяют выражение не моментально, и может получиться, что это выражение, выполняемое другим потоком, может повести себя не так как надо из-за того, что одни его члены изменены, а другие нет. Например, в выражении x+x в операндах могут оказаться разные значения.

Есть ещё функция Renew, которая обновляет (заменяет) выражение другим. При этом в выражении остаётся оригинал, если он был зафиксирован функцией FixOrigExpr. Поясню: экспрешн - это объект ссылочного типа с двумя полями (не путать с System.Linq.Expressions.Expression). В одном лежит главное выражение, а в другом оригинальное выражение. Renew заменяет только главное.

object e=Expr(Write("Hello! ")); 
object timer=LocalTimerByExpr(e, 1000, true); 
Sleep(3000);
e.Renew(Expr(Write("Test! ")), true); //true указывает, что нужно сделать горячее обновление
Sleep(3000);
timer.StopTimer();
//Hello! Hello! Hello! Test! Test! Test!

Горячее обновление - замена членов выражения (главное выражение) членами из указанного выражения, а не присваивание ссылки. При создании локального таймера захватывается ссылка не на весь экспрешн, а только ссылка на главное выражение в нём. Поэтому, если написать просто e.Renew(Expr(Write("Test! "))); на таймер это никак не повлияет, т.к. в нём останется ссылка на прежнее выражение. То же самое с локальными задачами и потоками, создаваемыми функциями LocalTaskByExpr, LocalThreadByExpr.
Если вы используете инструкцию apply$, которая выполняет выражение с кешированием ссылки на него, то вот ещё пример с горячим обновлением выражения.

object e=Expr(Write("Hello! ")); 
object t=RunLocalTask(Test(e)); 
Sleep(3000);
e.Renew(Expr(Write("Test! ")), true); 
t.Wait();
//Hello! Hello! Hello! Test! Test! Test! Test!
ReadKey();

Test(object e){
    foreach(int i in Range(7)){
        apply$ e;
        Sleep(1000);
    }
}

Renew не является потокобезопасной. Если обновление происходит в момент выполнения выражения в другом потоке, то может возникнуть ошибка или результат будет неверный.

Резюмируем
Expr получает выражение, но только статические переменные в нём указывают на конкретные области памяти. Остальные - это просто имя + вид (статическая, локальная и т.д.). Вид можно поменять функцией SetVarScopeKind. Если поменять вид на статический, то значение переменной будет браться из класса в котором выполняется выражение.
Переменные можно вовлекать, а можно упаковывать. В выражении вовлеченным переменным можно присваивать новые значения, а упакованным нет, т.к. они уже не переменные, а постоянные значения.

Sign In or Register to comment.