Двойная диспетчеризация для динамически типизированных операторов в JavaScript

Я хочу сформулировать алгебраические выражения таким образом, чтобы можно было заменить основные типы чисел. Если хотите, подумайте о комплексных числах, больших целых числах, матрицах и тому подобном. По этой причине я бы написал либо add(a, b) или же a.add(b) вместо a + b, В статически типизированном языке я бы просто использовал перегрузку функции на основе типов add реализовать различные альтернативы. Но для JavaScript это не работает, поэтому я ищу альтернативы. Выполненный метод зависит от типа обоих операндов.

Одним из способов, который я придумал, был бы следующий механизм двойной отправки:

  1. Напишите выражение как a.add(b),

  2. Реализуйте этот метод для данного типа (например, мой собственный Complex тип, или встроенный Number типа) следующим образом:

    add: function(that) { that.addComplex(this); }
    

    Таким образом, имя метода второго вызова кодирует тип одного из операндов.

  3. Реализуйте специализированные методы для работы со всеми комбинациями Например, установить

    Number.prototype.addComplex = function(that)
      { return newComplex(that.real + this, that.imaginary); }
    

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

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

Другой подход, который я попробовал в этом jsperf, заключался бы не в использовании прототипа, а в хранении виртуального метода как свойства каждого отдельного экземпляра объекта. Работает довольно быстро на практически всех протестированных браузерах, но здесь я беспокоюсь о размере объектов. Меня беспокоит наличие объектов с двумя фактическими значениями с плавающей запятой, но, возможно, целых 50 различных функций-членов просто для обработки всех пар перегрузки операторов.

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

Есть ли какой-нибудь способ обмануть текущие реализации JavaScript, чтобы сделать правильную оптимизированную двойную диспетчеризацию с объектами, которые дешевы в создании и не занимают слишком много памяти?

1 ответ

Третий подход выглядит вполне жизнеспособным:

function Complex(re, im) {
    return {type:'c', re:re, im:im }
}
function Real(n) {
    return {type:'r', n:n }
}

funcs = {
    add_c_r: function(a, b) {
        console.log('add compl to real')
    },
    add_r_c: function(a, b) {
        console.log('add real to compl')
    }
}

function add(a, b) {
    return funcs["add_" + a.type + "_" + b.type](a, b);
}

add(Complex(1, 2), Real(5))
add(Real(5), Complex(1, 2))

Одно дополнительное поле + одно косвенное обращение - разумная цена.

Другие вопросы по тегам