Что есть "Expression evaluator" ?
Expression evaluator позволяет вычислять указанные выражения, например:
12*3
это 36[1,2,3].reverse()
это массив [3,2,1]'Kate'.reverse()
это "etaK"
Выражения могут зависеть от входных переменных:
10*x + 4
зависит от значенияx
.'My name is {userName}. Age is {2022-birthYear}'
зависит от значенийuserName
иbirthYear
.
Скрипт может содержать несколько таких выражений:
x = Vx*t
y = Vy*t
distance = sqrt(x**2 + y**2)
average = (x+y)/2
В общем случае, вы можете использовать Nfun везде, где раньше вы хранили, передавали или настраивали константы. В примере ниже, мы формульно задаем правила начисления бонусов
// ...`settings.json`...
{
"offset": "25",
"timeOffset": "3* 60 * 60 * 24 #sec",
"condition": "if (age>18) isEmpty(orders) else isEmpty(parent.orders)",
"bonus": "(min(max(order.price, 20.0), 100) + prevBonus)/ordersCount"
}
Вот несколько примеров использования:
Backend: фильтры входящих запросов;
Embeded: настройка обработки сигналов;
Система лояльности: настройки бонусной программы;
Робототехника: кинематическая модель. Описания траекторий;
Low-code решения.
Что умеет NFun
Nfun продолжает идею библиотеки Ncalc, но для богатой системы типов
Примитивные типы -
byte
,u16
,u32
,u64
,i16
,i32
,i64
,real
,bool
,ip
,char
,text
,any
Массивы, структуры, лямбда выражения и Linq.
Арифметические, бинарные, дискретные операторы, операторы по работе с массивами
Условный оператор
if
.Интерполяция строк.
Именованные выражения и пользовательские функции.
Строгая типизация с выведением типов.
Встроенные функции.
Кастомизация семантики.
Playground
Установите nuget пакет NFun
PM> Install-Package NFun
Начнем с классики!
var a = Funny.Calc("'Hello world'");
Console.WriteLine(a);
Посчитаем константы
bool b = Funny.Calc<bool>("false and (2 > 1)"); // false of bool
double d = Funny.Calc<double>(" 2 * 10 + 1 "); // 21 of double
int i = Funny.Calc<int>(" 2 * 10 + 1 "); // 21 of int
Посчитаем выходные данные
class User { public string Age {get;set;} public string Name {get;set;} }
var inputUser = new User{ Age = 42; Name = "Ivan"; }
string userAlias =
Funny.Calc<User,string> (
"if(age < 18) name else 'Mr. {name}' ",
inputUser);
А теперь перейдем в режим хардкора. Этот режим предоставляет доступ ко всем переменным, и к контролю исполнения на низком уровне
var runtime = Funny.Hardcore.Build(
"y = a-b; " +
"out = 2*y/(a+b)"
);
// Set inputs
runtime["a"].Value = 30;
runtime["b"].Value = 20;
// Run script
runtime.Run();
// Get outputs
Assert.AreEqual(0.4, runtime["out"].Value);
Мы можем продолжить ...
Вычисление нескольких значений, на основании входных переменных
// Assume we have some С# model
/*
class SomeModel {
public SomeModel(int age, Car[] cars) {
Age = age;
Cars = cars;
}
public int Age { get; } //Used as input
public Car[] Cars { get; } //Used as input
public bool Adult { get; set; } //Used as output
public double Price { get; set; } //Used as output
}
*/
var context = new SomeModel(
age:42,
cars: new []{ new Car{ Price = 6000 }, new Car{ Price = 6200 }}
);
// then we can set the 'Adult' and 'Price' properties based on the value of the 'Age' and 'Cars' properties
Funny.CalcContext(
@"
adult = age>18
price = cars.sum(rule it.price)
",
context);
Assert.AreEqual(true, context.Adult);
Assert.AreEqual(12200, context.Price);
// So input values and output values are properties of the same object
Кастомизация
Nfun предоставляет кастомизацию синтаксиса и семантики под ваши нужды
- Запрет или разрешение if-выражений
- Decimal или Double арифметика
- Integer overflow поведения
- Запрет или разрешение пользовательских функций
- Тип по умолчанию для целочисленных констант
var uintResult = Funny
.WithDialect(integerOverflow: IntegerOverflow.Unchecked)
.Calc<uint>("0xFFFF_FFFF + 1");
Assert.AreEqual((uint)0, uintResult);
//now you cannot launch script with such an expression
var builder = Funny.WithDialect(IfExpressionSetup.Deny);
Assert.Throws<FunnyParseException>(
() => builder.Calc("if(2<1) true else false"));
Добавление функций и констант
//assume we have custom function (method or Func<...>)
Func<int, int, int> myFunctionMin = (i1, i2) => Math.Min(i1, i2);
object a = Funny
.WithConstant("foo", 42)
.WithFunction("myMin", myFunctionMin)
// now you can use 'myMin' function and 'foo' constant in script!
.Calc("myMin(foo,123) == foo");
Assert.AreEqual(true, a);
Синтаксис
Nfun поддерживает однострочные выражения:
12 * x**3 - 3
Многострочные именованные выражения:
nameStr = 'My name is: "{name}"'
ageStr = 'My age is {age}'
result = '{nameStr}. {ageStr}'
И пользовательские функции:
maxOf3(a,b,c) = max(max(a,b),c)
y = maxOf3(1,2,3) # 3
В зависимости от задачи, вы можете включать и отключать эти возможности.
Подробнее про синтаксис
Операторы
# Arithmetic operators: + - * / % // **
y1 = 2*(x//2 + 1) / (x % 3 -1)**0.5
# Bitwise: ~ | & ^ << >>
y2 = (x | y & 0xF0FF << 2) ^ 0x1234
# Discreet: and or not > >= < <= == !=
y3 = x and false or not (y>0)
If-выражение
simple = if (x>0) x else if (x==0) 0 else -1
complex = if (age>18)
if (weight>100) 1
if (weight>50) 2
else 3
if (age>16) 0
else -1
Пользовательские функции и обобщенная арифметика
sum3(a,b,c) = a+b+c #define generic user function sum3
r:real = sum3(1,2,3)
i:int = sum3(1,2,3)
Массивы
# Инициализация
a:int[] = [1,2,3,4] # [1,2,3,4] type: int[]
b = ['a','b','foo']# ['a','b','foo'] type: text[]
c = [1..4] # [1,2,3,4] type: int[]
d = [1..7 step 2] # [1,3,5,7] type: int[]
# Оператор In
a = 1 in [1,2,3,4] # true
# Чтение
c = (x[5]+ x[4])/3
# Срезы
y = [0..10][1:3] #[1,2,3]
y = [0..10][7:] #[7,8,9,10]
y = [0..10][:2] #[0,1,2]
y = [0..10][1:5 step 2] #[1,3,5]
# Функции
# concat, intersect, except, unite, unique, find, max, min, avg, median, sum, count, any, sort, reverse, chunk, fold, repeat
Структуры
# initialization
user = {
age = 12,
name = 'Kate'
cars = [ # array of structures
{ name = 'Creta', id = 112, power = 140, price = 5000},
{ name = 'Camaro', id = 113, power = 353, price = 10000}
]
}
userName = user.name # field access
Строки
a = ['one', 'two', 'three'].join(', ') # "one, two, three" of String
# Interpolation:
x = 42
out = '21*2= {x}, arr = {[1,2,x]}'
#"21*2= 42, arr = [1,2,42]" of String
Linq (лямбды)
[1,2,3,4]
.filter(rule it>2)
.map(rule it**3)
.max() # 64
Семантика
Nfun строго типизирован - это было основным вызовом, и ключевой особенность для гармоничной интеграции в C#, а так же защиты от ошибок. Однако, синтаксис языков со строгой типизацией всегда сложнее.
Что бы решить эту проблему я опирался на постулат:
Все, что выглядит как правильный скрипт (в рамках синтаксиса/семантики) - должно запуститься
Или, более формализовано:
Если код выглядит как слабо-типизированный скрипт, но при этом запускается без ошибок, значит для него можно однозначно вывести типы.
Либо показать, что такой код не может быть выполнен без ошибок.
Это потребовало разработки сложной системы выведения типов, от которой и отталкивается вся семантика языка. Результатом этого является повсеместное использование Generic-ов.
В примере ниже - функции, вычисления, и даже константы - являются обобщенными типами (из списка int32
, uint32
, int64
, uint64
, real
). Однако, пользователь не должен задумываться об этом:
var expr = @"
# generic function
inc(a) = a + 1
# generic calculation
out = 42.inc().inc()
";
double d = Funny.Calc<double>(expr); // 44 of double
int i = Funny.Calc<int>(expr); // 44 of int
Таким образом, удалось собрать все преимущества строгой типизации:
Если типы выражения не сходятся - вы получаете ошибку на этапе интерпретации.
Если типы не сходятся с ожидаемыми C# типами - вы получаете ошибку на этапе интерпретации.
Производительность.
При этом, синтаксис остался максимально простым для неподготовленного пользователя!
Технические детали
Так как Nfun под капотом это - "почти язык программирования", то его архитектура достаточно стандартна. Не буду описывать ее здесь подробно, об этом уже есть много классных статей.
Интерпритацию кода можно разделить на несколько этапов:
1. Токенизация (лексер).
Самописный лексер разбирает входную строку на токены, учитывается возможность интерполяции строк.
2. Парсинг.
Разбор токенов в дерево AST. Используется самописный парсер. Спецификация синтаксиса опирается на спецификацию языка (и очень много тестов). Формальная грамматика не описана.
3. Выведение типов.
Кастомный, графовый алгоритм выведения типов, с поддержкой неявных преобразований между типами.
Нюансы:
Целочисленные константы являются "обобщенными константами"
one() = 1 # returns T, where byte -> T -> real
y:int = one() # 1 of int
z:real = one() # 1.0 of real
Тип узла может зависеть, как от предыдущего, так и от последующего кода
y = 1 # y:real, так как используется в знаменателе на следующей строчке
x:real = 1/y
Ограничения на дженерик-переменные могут быть заданы, как сверху (наследование, тип к которому можно привести данный), так и снизу ( потомок, тип, которой может быть приведен к данному)
sum(a,b) = a+b
x:int = 1 + 2 # x = 3 of int
y:real = 1 + 2 # y = 3.0 of real
z:byte = 1 + 2 # error! operator '+' is defined for (uint16|int16)-> T -> real
# so it cannot return 'byte'
4. Сборка выражений (построение рантайма)
Из результатов решения типов и Ast-дерева собирается самописное дерево вычисляемых выражений.
Конвертация CLR-значений в Nfun.
Исполнение выражений.
Конвертация результатов в CLR значения.
Я сознательно не использовал Csharp-expression-tree, так как, одним из важнейших критериев была скорость "one-time-shot". То есть запуска, от момента получения строки со скриптом и до момента получения результатов выполнения.
Состояние проекта
Проект готов к использованию в продакшене.
Используется в течении года в составе scada-системы, покрыт 6000+ тестами, к нему написана спецификация, и.. даже есть несколько звездочек на гитхабе (да-да, это call to action!). Почти успех!
Заключение
Когда я начинал Nfun, я мечтал создать простой, надежный и интуитивный опенсорс-инструмент. Я хотел реализовать синтаксические идеи и поэкспериментировать с системами выведения типов, попробовать для себя что-то новое...
По итогу, Nfun стоил огромного количества сил, времени и инвестигаций. Я впервые столкнулся с подобной задачей. Теперь мне хочется, чтобы люди пользовались этим инструментом! И писали тикеты, да реквесты на гитхаб. Ну, или комментарии под этот пост ;)