Что есть "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 стоил огромного количества сил, времени и инвестигаций. Я впервые столкнулся с подобной задачей. Теперь мне хочется, чтобы люди пользовались этим инструментом! И писали тикеты, да реквесты на гитхаб. Ну, или комментарии под этот пост ;)
Комментарии (21)
AuDim
01.12.2022 15:05В последнее время в большинстве статей по C# идет описание "Как это сделать"
Но я почти всегда перестаю понимать "Зачем это делать"?Так и здесь - объясните зачем это делать?
Можете описать прикладное пример этого?codecity
01.12.2022 15:31+1В админке для вычисления курсов валют задавать формулу, а не просто коэффициенты.
tmteam Автор
01.12.2022 15:59Это гибкий инструмент, а значит ты понимаешь что он тебе нужен только когда ты сталкиваешься с практической задачей. Я знаю что многие ребята сталкивались с подобными задачами но не находя инструмента городили костыли
Запись выражений в скада системах и или лоукод решениях (смотрите скриншоты в статье)
Хранение выражений вместо констант в конфигах (пример с Settings.json в статье). Можно сказать что это сериализация лямбд.
Web UI - например фильтры запросов, правила начисления бонусов и так далее (скриншоты в статье)
Тонкая настройка фильтров изображений
Верстка, в том числе email. Например если раньше вы писали
Дорого пользователь. Ваш баланс низкий и скоро мы вас заболим
то теперь можно писать:
{if (user.gender==m} 'Дорогой' else 'Дорогая'} {user.name}. Ваш баланс равен {user.balance} $ и скоро мы вас заблочим
Раньше для такого нужно было напрягать программистов. Теперь это можно писать просто в вашей CRM
Это примеры которые были в практике или сходу пришли мне в голову.
Примеры:
ka4ep
01.12.2022 15:38+2Можете попробовать перевести слово "инвестигация" на русский, это не сложно.
tmteam Автор
01.12.2022 15:44-1Это действительно не сложно - но у этих слов сейчас различные оттенки:
Исследования - это что то связаное с формальной наукой. "Делал дома исследования" - звучит кустарно
Изучение - это больше про учебу и не смотрится в оригинальном тексте
Инвестигации - это именно кустарное, айтишное исследование
А так как слово "Инвестигация" - прекрасно склоняется и встраивается в язык - не вижу проблем. Мы же не боимся слова "инфа" или "комп" чьи корени так же заимствованы.
Процесс обогащения языка заимствованными корнями, при условии склоняемости слов - это не плохо. Даже хорошо
ka4ep
01.12.2022 15:57+1Наверное это только мне глаза режет, потому и занудствую. Но какое у этого слова, обозначающее процесс, есть существительное? Климатизация - климат, диспетчеризация - диспетчер, инвестигация - ...?
DimonSmart
01.12.2022 17:30+1Спасибо за статью! Проделана огромная работа!
Добавлю свои 5 копеек про шуточный калькулятор на регулярках! Fun with C# Regex based Expression calculator - DEV Community ????????????????
kemsky
01.12.2022 20:47Недавно тоже размышлял над похожей штукой, чтобы дать пользователям возможность описать, например, график платежей в обычном тестовом поле языком близким к естественному. Я правда смотрел в сторону gherkin.
Нужна подсветка синтаксиса или хотя бы минимальный валидатор на js.
ARad
Почему вы не использовали стандартный компилятор C#? Зачем такие сложности?
Bonart
Скорость компиляции
a-tk
Компилятор C# компилирует C#. Здесь же грамматика явно отличается.
ARad
Я понял что отличается, но зачем это, если как раз можно использовать и стандартный C# и не писать компилятор...
CrazyMaks
Потому что скрипт задает пользователь, а не программист хардкодит его в проект.
mayorovp
Нельзя сувать непроверенный ввод пользователя в компилятор C#. А проверка ввода тут сама по себе по сложности как компиляция.
tmteam Автор
C# и F# не подходят для этих задач:
Пользователям (бугалтерам, редакторам, пусконаладчикам, проектантам) очень сложно (а скорее невозможно) писать код с явной типизацией (пускай даже простой)
Плохой синтаксис для этих задач, что выглядит чужеродно и пугающе будучи встроенным внутрь интерфейса или конфиг файла.
Я встречал людей которые отказывались пользоваться подобной штукой как только увидели символ стрелки.
Сравните с точки зрения не программиста эти две клинописи:
и
Скорость компиляции у C#/F# - отстой. Это критично как для embedded -сценариев,так и для одиночного запуска
Кол-во звездочек у пакета Ncalc
Потому что я могу написать свой мини-ЯП !
mayorovp
Только вот у вас во втором пункте две разные "клинописи". Ни один пример не будет понятным бухгалтеру или редактору.
tmteam Автор
Сегодня как раз на стриме будем обсуждать.
Проверяли на практике (проэктанты Scada-системы Sonica). Там был питон/С# и Nfun.
По итогу - проектанты:
При касании C# - сразу говорили "Нет, мы это делать не будем, лучше на костылях".
На питоне плакали кололись и допускали очень много ошибок.
На Nfun спокойно пишут, ошибок не допускают (но лямбды не используют).
Более того - многое из синтаксиса Nfun строилось именно по реакция проэктантов. Наверняка можно было сделать лучше, я сделал лучшее что мог. Вы можете предложить синтаксис - и можно его впилить в NFun. И это не троллинг, это действительное предложение. Накидать парсер для синтаксиса не есть сложно.
А по факту - действительно "идеального" решения пока нет. Но сложность например SQL дается большинству. Python уже сложнее. C# это ппц для обычных ребят. Но так как мы идем в мир где минимальное знание ЯП становится таким же естественным как умение решать простые уравнения - то это вселяет в меня оптимизм
ARad
Я точно не уверен насколько это просто, но рассматривали ли вы интеграцию с
.NET Interactive
как альтернативу делать свое решение?.NET Interactive
поддерживает скрипты на C#, F#, PowerShell, JavaScript. Я думаю что F# и JavaScript достаточно просты. Жаль конечно что нет поддержки Python. Это сейчас один из самых популярных скриптовых языков. И мне безусловно интересно как в.NET Interactive
реализованна защита песочницы, и есть ли она там...Рассматривали ли вы Python или JavaScript как языки для написания скриптов? Их достаточно часто применяют в похожих сценариях. Я не искал готовые для этого библиотеки в .NET, но возможно они есть. Тут конечно надо серьёзно взвешивать все за и против, особенно в части передачи данных между .NET и скриптом.
По 4 пункту — если вы будете сохранять результат компиляции, то скорость компиляции C#, F# оригинальным компилятором не должна стать проблемой. Тут конечно возникает вопрос защиты песочницы. Насколько просто сделать защиту песочницы, при использовании оригинального компилятора.
По 2 пункту, если честно синтаксис спорный в том плане что он нестандартный, опять напомню про синтаксис Python или JavaScript как альтернативу придумывать свой. Ваш мне очень напоминает смесь C# и JavaScript с расширениями. Особенно нестандартно и крипово выглядит
(rule it**2)
.Я осознаю что вам уже поздно все переделывать, когда вы уже использовали свое решение в
Проде
. И нисколько вас к этому не призываю.GbrtR
Выглядит круто.
Как решается проблема с ошибками которые пользователи совершают? Насколько детальны сообщения об ошибках? Мне кажется что для непрофессиональных пользователей это очень важно, чтобы упростить порог входа.Есть ли подобные библиотеки или прямые конкуренты?
Написали ли language server (и/или плагин для редактора кода на веб страницах, чтобы синтаксис подсвечивался) чтобы пользователи могли проще видеть что они делают?
Можно ли отключать поддерживаемые фичи в рантайм? Например мы не хотим переусложнять синтаксис и разрешать сложные выражения, было бы полезно иметь разные поддерживаемой уровни сложности, типа basic, advanced, prof.