Настоящая* перегрузка операторов в JavaScript
- 2022-04-01
- ru
- CC BY-SA 4.0
- habr
Одна из активно реквестируемых фич в JavaScript и в TypeScript — перегрузка операторов. Без инфиксной записи, к примеру, получаются очень громоздкими вычисления с векторами или множествами. Тем не менее, используя сильное колдунство некоторые знания о том, как сейчас работают операторы в JavaScript, мы можем реализовать все самостоятельно.
Источник наиболее полной и поднобной информации о семантике операторов — это текст стандарта ECMA-262. Он формально описывает алгоритмы исполнения JavaScript похожим на псевдокод языком. Но он достаточно сложен для понимания неподготовленным читателем. Поэтому давайте пойдем другим путем — вспомним, к каким объектам JavaScript, не являющимся числами, часто применяют арифметические операторы.
Первый из очевидных вариантов — строки. Оператор +
для них означает конкатенацию:
> 'foo' + 'bar'
'foobar'
К сожалению, никакие другие операторы для них не перегружены. Для нашего волшебного MagicSet
этого маловато. Есть ли какие-то встроенные объекты, помимо чисел, к которым можно применить, например, вычитание? Если мы почитаем, например, MDN, то можем наткнуться вот на такой пример кода:
// Using Date objects
;
// The event to time goes here:
;
;
; // elapsed time in milliseconds
Как это работает при условии отсутствия в языке настоящих перегрузок операторов? Подсказку может дать результат выполнения похожего кода:
> new Date'2022-04-01 11:00' - new Date'2022-04-01 10:00'
3600000
Как видим, результат вычитания — число, не объект Date
. На самом деле, и вычитались тоже числа. При вычислении арифметических операторов с объектами JavaScript первым делом пытается превратить их в значения примитивного типа, используя их метод valueOf()
. Для объектов Date он переопределен, и возвращает количество миллисекунд с полуночи 1 января 1970 года.
Таким образом, мы можем преобразовать наши волшебные множества в числа, дать рантайму выполнить над ними операции по обычным правилам, а в методе overload()
как-то преобразовать обратно. Но в этом "как-то" и кроется вся сложность. В отличие от, например, дат, множества трудно представить как числа так, чтобы семантика операций над ними была чем-то полезным. Остается, разве что, подобрать подходящий MagicSet
перебором.
На самом деле, последний вариант не так страшен, как кажется — ведь у нас есть больше информации о вычисленном выражении, чем просто его результат! Для каждого из операндов вызвался метод valueOf()
в том порядке, в котором эти операнды были вычислены. Мы можем сохранить эту последовательность, и использовать ее при нахождении решения.
Давайте сформулируем задачу чуть подробнее. Нам дан массив "идентификаторов" операндов — значений их valueOf()
— в том порядке, в котором они были вызваны. Также известен результат выражения, также являющийся числом. Необходимо по этим данным восстановить вычисленное выражение — фактически, расставить символы операций между "идентификаторами", чтобы получить нужный ответ. Для того, чтобы не заморачиваться со скобками, будем пользоваться обратной польской нотацией и стековыми вычислениями.
Задачу можно решить поиском в ширину. Начнем с состояния, когда в стеке лежит первый операнд, положим это состояние в очередь. Затем для каждого элемента этой очереди посмотрим, какие состояния мы могли получить из него. Учитывая, что каждый операнд был использован заранее известное число раз, алгоритм не будет работать бесконечно.
;
Далее нужно вычислить полученное выражение, используя настоящие операндов. Для этого будем хранить для каждого соответствие "идентификаторов" и самих объектов MagicSet
.
Дело осталось за малым — реализовать сам MagicSet
и операции над ним.
Вот и готов наш первоапрельский розыгрыш - настоящая* перегрузка операторов в JavaScript!
*черную магию вне Хогвартса использовать в продакшене запрещено