Pull to refresh

Comments 45

Бежать по токенам в цикле и проверять что это за токены — это не очень быстро. JIT тут не сможет ничего соптимизировать. Самый быстрый метод — сгенерировать JS код и скормить его в new Function. На втором месте — для каждого подвыражения сформировать замыкание завязанное на конкретный набор подвыражений, тогда JIT сможет это неплохо оптимизировать. Пример второго подхода можно глянуть в библиотеке $mol_time, имеющей самый быстрый форматтер среди конкурентов:


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

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

Так я и говорю про множественные вычисления. На одноразовых вычислениях указанные мной оптимизации только замедлят исполнение.

Можете привести ссылки на реализацию вычислений, а не форматирования строк?

Форматирование времени так-то содержит в себе много нетривиальных вычислений. То, что на выходе строка, а не число, сути не меняет.


Ок, вот простой пример:


// Фабрики выражений
const Const = ( v )=> ()=> v
const Arg = ( index )=> x => x[ index ]
const Sum = ( a , b )=> x => a(x) + b(x)
const Pow = ( a , b )=> x => a(x) ** b(x)
const Square = ( a )=> Pow( a , Const(2) )
const Sqrt = ( a )=> Pow( a , Const(1/2) )

// Собираем формулу
const distance = Sqrt(
    Sum(
        Square( Arg('x') ),
        Square( Arg('y') ),
    )
)

// Вычисляем формулу
const result = distance({ x : 3 , y : 4 }) // 5

После прогрева JIT получаем ускорение в несколько раз:

Хотя, так ещё быстрее:


// Аргументы: F( X , Y )
const X = (x,y) => x
const Y = (x,y) => y

// Фабрики выражений
const Const = ( v )=> ()=> v
const Sum = ( a , b )=> (x,y) => a(x,y) + b(x,y)
const Pow = ( a , b )=> (x,y) => a(x,y) ** b(x,y)
const Square = ( a )=> Pow( a , Const(2) )
const Sqrt = ( a )=> Pow( a , Const(1/2) )

// Создание формулы
const distance = Sqrt(
    Sum(
        Square( X ),
        Square( Y ),
    )
)

// Вычисление формулы
const result = distance( 3 , 4 ) // 5

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

=round2((A1^2+A2^2)^0.5)

Проверить работу формулы при изменении исходных значений можно на bizcalc.ru (формулу нужно будет прописать вручную).
В том то и дело, что задачей движка является исключение возможности выполнения произвольного кода, а только в соответствии с формальным синтаксисом формул. При этом формулы должны быть понятны и привычны обычным пользователям Excel.

Excel между прочим поддерживает JScript.

На вивальди выдается странный результат:
image

Там влияние непредсказуемых факторов высокое из-за простоты тестируемого кода. Я привёл максимальные значения, что смог накликать.

Лучше всего кидать ошибку, чтобы пользователь не ломал голову, а чётко дал понять свои намерения. А так, первый вариант единообразен с остальными операторами, вычисляемыми обычно слева на право. С другой стороны он имеет мало смысла, ибо (4^3)^2 проще записать как 4^(3*2).

На самом деле не понимаю, почему «наука» не договорилась о едином подходе.
Другой вариант — убрать оператор и оставить функцию.
Как я понял у автора понятия ошибок нет, по крайней мере 1/0=0.
С ошибками могут быть проблемы, сейчас при вычислении if(1;2+2;3+3) считается 2+2 и 3+3, а затем if(1;4;6), если вводить понятие ошибок то какой должен быть результат при if(1;2+3;4/0), 5 или ошибка?
Прошу прощения, что забыл упомянуть про специфику расчетчика. Обработка выражения 1/0 эквивалентно созданию функции if ( B != 0; A/B; 0). Данную функцию приходится постоянно писать в Excel при выполнении различных отчетов, иначе таблица превращается в нагромождение сообщений об ошибках. А в специализированном экономическом программном обеспечении, используемом в нашей организации, данное умолчание используется более 10 лет, поэтому как-то и подзабыл.
Так как используется единственный оператор возведения в степень, то без скобок вычисления производятся слево направо. То есть верен первый вариант 4^3^2= (4^3)^2= 4096. Сработает также вариант со скобками 4^(3^2)= 262144. Можете потестировать на bizcalc.ru.
Вообще-то везде по разному направление.
В JS, к примеру, 4**3**2 = 4**(3**2), то есть как в «степенной башне».
Честно говоря, только сейчас об этом узнал. В результате в Википедии нашел следующую фразу: «Иногда в компьютерных системах и языках программирования значок возведения в степень имеет левую ассоциативность, в отличие от принятого в математике соглашения о правой ассоциативности возведения в степень».

С другой стороны, расчетчик предназначен для электронных таблиц и разбора Excel-подобных формул, где используется классический математический подход. Кроме того, скобки никогда лишними не бывают (в разумных количествах) — они если и не меняют последовательность расчетов, то могут повысить читаемость формул.

Это не обратная польская запись, в ней 2 + 3 записывается как 2 3 +, а скобки вообще отсутствуют.

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

хм, то есть если я возьму пример
formula1 = "round2(15.542 + 0.5)";
и запишу его как
formula1 = "round2(+ 15.542 0.5)";
то он все ещё будет работать?

Нет. Такое преобразование делает алгоритм, по ходу дела распихивая токены по стекам. На входе алгоритма должна быть классическая формула в инфиксной нотации.

Дорогой автор, а в чем смысл публикации? Ваш код демонстрирует нежизнеспособный подход и обратной польской нотацией тут не пахнет.


Ваш код это демонстрация как делать нельзя. Обычно, для решения подобных задач делают токенизатор, парсер и интерпретатор. Ваш код ломается на элементарных вещах вроде « max(1;2--2)» А когда вы начнёте его расширять, то будет вообще мрак. Чтобы не ломался нужно


  • Написать лексер, вполне сойдёт лексер на регулярках. На выходе лексера поток токенов (тип токена, значение). Из лексера вылетают лексемы, синтаксические примитивы вроде «имя», «число», «точка с запятой», «строка». На данном этапе никакой семерики нет. У вас есть тип токена — function, это не верно, может быть name, но точно не function.
  • Написать грамматику в виде bnf и сделать для неё LL парсер руками или сгенерировать. Сгенерировать сложнее, там нужно больше теории знать. Парсер разбирает поток токенов и выплевывает либо AST, либо код для стековой машины (та сама польская запись), либо сразу интерпретирует.

Кода будет побольше на пару экранов, но все будет работать как часы. Почитайте про LL парсеры, найдите примеры, из полно онлайн. Раз вы делаете что-то вроде Excel, вам без этого никуда.

Смысл публикации довольно прост — поделиться с сообществом своими мыслями и получить на них обратную связь. Про обратную польскую нотацию, честно говоря, не совсем понял. Вы уже второй, который не видит ее в алгоритме. Вот здесь довольно много примеров реализации обратной польской нотации на стеках: https://ru.wikiversity.org/wiki/Обратная_польская_запись:_примеры реализации.

Что касается обработки ошибок, то согласен, ее пока нет. На текущий момент это было сделано, чтобы упростить код для понимания, когда в него вписывал механизм работы с функциями. Технически же реализовать обработку ошибок довольно просто — в типах токенов добавляется массив ожидаемых типов токенов по ходу движения парсера, которые отслеживаются на стадии разбора формул. По скобкам еще проще — вводятся счетчики открытых и закрытых скобок.

Алгоритм, который Вы предлагаете, вполне имеет право на существование. Просто я реализовал на JS свой алгоритм 20-летней давности на Java, когда еще в Java еще не было нормальной поддержки регулярных выражений, а приходилось работать с отдельными символами строки. Тем не менее, этот алгоритм вроде бы достаточно быстр, но хотелось бы получить объективную сравнительную оценку со стороны, ради которой и сделал эту публикацию.
Обратная польская запись, (1+1), как у вас, это инфиксная запись, (1 1 +) это обратная польская запись.
1. Запись (1+1) превращается в два стека (в виде массивов) перед передачей ее расчетной функции calcExpression после обнаружения закрывающей скобки:
массив операндов: [1, 1]
массив операторов: ["(", "+"]
2. Функция calcExpression после вычисления возвращает следующие стеки:
массив операндов: [2]
массив операторов: ["("]
3. Функция calc после возврата значения функцией calcExpression выбрасывает из стека открывающую скобку, в результате чего стеки имеют следующий вид:
массив операндов: [2]
массив операторов: []
4. В завершении цикла обхода токенов функция calc снимает из стека операндов последний результат, то есть 2
Это все прекрасно и никакого отношения к обратной польской записи не имеет. ОПЗ она про запись, то есть синтаксис, а не про стеки. При разборе ОПЗ используется один стек, а у вас их уже на шаге №1 два.
Ошибаетесь. Обратная польская нотация — это именно про стеки. В свое время даже выпускались стековые калькуляторы, такие как ru.wikipedia.org/wiki/Электроника_МК-61, где в инструкциях примеры ввода и последующего расчета проводились именно в обратной польской нотации.
То есть выражение 1+2-3 будет передано на вычисление в виде двух стеков:
массив операндов: [1, 2, 3]
массив операторов: ["+", "-"]
Чем не обратная польская запись?

Наверное, тем что в обратной польской записи операции идут за операндами?


1 + 2 - 3 в RPN будет 1 2 + 3 - а вовсе не "массив операндов" и "массив операторов".


Вы упорно продолжаете путать запись и алгоритм — первое это то с чем имеет дело пользователь, второе (ваши массивы и прочие детали) он вообще не видит (и ему всё равно).


Если бы вам нужно было реализовать обработку выражений именно в RPN — то реализация алгоритма уместилась бы в двух десятках строк (но пользователи вас бы проклинали).

В самом начале сказано же — "форма записи математических и логических выражений" — причём тут вообще алгоритмы и способы обработки?


производящую вычисления по обратной польской записи

Это значит — "производит вычисления имея на входе обратную польскую запись" — это не название алгоритма.


Почему важно называть вещи правильно? Да потому что любой, знакомый с RPN и прочитав название вашей статьи решит что речь о том что выражения записаны в RPN (что, собственно, несколько человек уже и отметило).

Вы уже второй, который не видит ее в алгоритме.

Я буду третий (наверное). Потому что, как уже чуть выше сказали, речь про "запись", да и ваша ссылка говорит о способе записи выражений, а у вас обычный инфикс. Как это реализовано внутри (стеки, регистры etc) совершенно не имеет значения с точки зрения нотации.


Кстати, есть неплохая реализация разбора и вычисления выражений на JS, может вам пригодится в качестве источника идей, раз уж интересуетесь этой темой.

За ссылку спасибо. А вот на тему обратной польской нотации просто задайте себе вопрос — а для чего она вообще нужна, если не для обслуживания определенных структур данных. Повторюсь, но в свое время выпускались стековые калькуляторы, такие как ru.wikipedia.org/wiki/Электроника_МК-61, у которых в инструкциях для пользователей примеры вычислений приводились именно в обратной польской нотации, которая сама по себе, вне контекста стекового калькулятора, выглядела жутко неудобной и непонятной. Но вот после обуздания стекового калькулятора, на котором можно было еще и программировать, привычные всем бытовые калькуляторы выглядели палкой-копалкой на фоне экскаватора.

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


В калькуляторах (старых) были очень ограниченные ресурсы, именно по этой причине старались всё упростить — вот собственно и вся история.


Вы же пытаетесь называть стековую (и то не совсем — у вас массивы, которые их эмулируют) обработку "обратной польской нотацией" — хотя стек он и есть стек, он появился до того как появилась RPN, более того, она появилась как раз по той простой причине что прекрасно "ложилась" на стековую архитектуру, а не наоборот.

Ну про массивы все просто. В JS в отличие от, например, Java или C#, нет стека как специально выделенной структуры данных, а его интерфейс в виде функций push и pop реализован на массиве.

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

При этом не понятно, для чего человек должен сам переводить запись формул из инфиксной в постфиксную форму, если машина на стеках это делает просто не задумываясь?
функция evaluate также выполняется на стеке в виде массива

Да, безусловно, почти все разборщики выражений используют стеки в том или ином виде (прямо или через массивы), но это всё не имеет ровно никакого отношения к RPN, в принципе, по причинам которые я уже описал несколько раз в других ответах.


для чего человек должен сам переводить запись формул из инфиксной в постфиксную форму

Потому что когда-то были калькуляторы (и другие вычислительные устройства) которые не понимали инфиксной формы. На самом деле они всё ещё есть, в ряде очень специфичных областей, и есть даже языки программирования с RPN записью и стековой архитектурой, к которым относится и PostScript.

Кажется это вариация алгоритма сортировочной станции:
"… который применяется для разбора математических выражений, представленных в инфиксной нотации. Может быть использован для получения вывода в виде обратной польской нотации или в виде абстрактного синтаксического дерева.."

Только у вас на два стека, данных и команд, хотя можно обойтись одним.
Тоже делал на два стека, тогда казалось, что так логичнее.

Все-же я не понял, это просто эксперимент или вы делаете рабочий продукт? если последнее почему не взять что-нибудь готовое и куда более мощное, хотя-бы math-js?
Спасибо за наводку на алгоритм сортировочной станции. Что касается библиотек типа указанной Вами, то меня сильно напрягают их размеры, а также зависимости от других библиотек. С этой точки зрения работу можно назвать экспериментом на тему того, можно ли писать полнофункциональные приложения на современном чистом JS без использования сторонних библиотек. Конечно же при допущении, что поддержка старых браузеров не нужна.

С другой стороны, в рамках проекта BizCalc также происходит переосмысление некоторых концепций, которые сейчас реализованы в проекте JetCalc, в котором я участвую только в части постановки и тестирования по предметной области и в части организации структуры базы данных.
Есть еще одно соображение. При создании библиотек для широкого круга пользователей волей-неволей приходится реализовывать дополнительные уровни абстракций, которые для интерпретируемых языков являются довольно-таки узким местом, которые сказываются на производительности на больших объемах данных либо при интенсивных режимах работы.

Ну и не следует упускать из внимания тот фактор, что большой объем кода потенциально может нести и большое количество уязвимостей. И то, что код открыт, совершенно не означает, что даже крутой профессионал способен быстро найти потенциальные уязвимости в таком коде.
1. думаю найдется немало куда более быстрых готовых библиотек;
2. просто признайтесь — вам было интересно это писать, это самая важная причина из всех, какие могут быть :)
Кайф и коронавирус рулят.
Sign up to leave a comment.

Articles