Сделать Неизменным.js или ленивый.js выполняет короткий синтез?

во-первых, позвольте мне определить, что это короткий синтез для тех из вас, кто не знает. Рассмотрим следующее преобразование массива в JavaScript:

var a = [1,2,3,4,5].map(square).map(increment);

console.log(a);

function square(x) {
    return x * x;
}

function increment(x) {
    return x + 1;
}

здесь мы имеем массив,[1,2,3,4,5], чьи элементы сначала в квадрате,[1,4,9,16,25], а затем увеличивается [2,5,10,17,26]. Следовательно, хотя нам не нужен промежуточный массив [1,4,9,16,25], мы все еще создаем его.

Short-cut fusion-это метод оптимизации, который может получить избавление от промежуточных структур данных путем объединения некоторых вызовов функций в одну. Например, short-cut fusion можно применить к вышеуказанному коду для получения:

var a = [1,2,3,4,5].map(compose(square, increment));

console.log(a);

function square(x) {
    return x * x;
}

function increment(x) {
    return x + 1;
}

function compose(g, f) {
    return function (x) {
        return f(g(x));
    };
}

как вы можете видеть, два отдельных map вызовы были объединены в один map вызов, составляя square и increment функции. Следовательно, промежуточный массив не создается.


теперь, я понимаю, что библиотеки, как незыблемыми.js и ленивый.js эмулировать ленивую оценку в JavaScript. Ленивая оценка означает, что результаты вычисляются только при необходимости.

например, рассмотрим приведенный выше код. Хотя мы square и increment каждый элемент массива, но нам могут не понадобиться все результаты.

Предположим, нам нужны только первые 3 результата. Использование Неизменяемого.js или ленивый.js мы можем получить первые 3 результата,[2,5,10], без расчета последних 2 результаты, [17,26], потому что они не нужны.

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

чтобы прояснить этот момент, рассмотрим следующий код, который эмулирует ленивую оценку:

var List = defclass({
    constructor: function (head, tail) {
        if (typeof head !== "function" || head.length > 0)
            Object.defineProperty(this, "head", { value: head });
        else Object.defineProperty(this, "head", { get: head });

        if (typeof tail !== "function" || tail.length > 0)
            Object.defineProperty(this, "tail", { value: tail });
        else Object.defineProperty(this, "tail", { get: tail });
    },
    map: function (f) {
        var l = this;

        if (l === nil) return nil;

        return cons(function () {
            return f(l.head);
        }, function () {
            return l.tail.map(f);
        });
    },
    take: function (n) {
        var l = this;

        if (l === nil || n === 0) return nil;

        return cons(function () {
            return l.head;
        }, function () {
            return l.tail.take(n - 1);
        });
    },
    mapSeq: function (f) {
        var l = this;
        if (l === nil) return nil;
        return cons(f(l.head), l.tail.mapSeq(f));
    }
});

var nil = Object.create(List.prototype);

list([1,2,3,4,5])
    .map(trace(square))
    .map(trace(increment))
    .take(3)
    .mapSeq(log);

function cons(head, tail) {
    return new List(head, tail);
}

function list(a) {
    return toList(a, a.length, 0);
}

function toList(a, length, i) {
    if (i >= length) return nil;

    return cons(a[i], function () {
        return toList(a, length, i + 1);
    });
}

function square(x) {
    return x * x;
}

function increment(x) {
    return x + 1;
}

function log(a) {
    console.log(a);
}

function trace(f) {
    return function () {
        var result = f.apply(this, arguments);
        console.log(f.name, JSON.stringify([...arguments]), result);
        return result;
    };
}

function defclass(prototype) {
    var constructor = prototype.constructor;
    constructor.prototype = prototype;
    return constructor;
}

как вы можете видеть, вызовы функций чередуются и обрабатываются только первые три элемента массива, доказывая, что результаты действительно вычисляются лениво:

square [1] 1
increment [1] 2
2
square [2] 4
increment [4] 5
5
square [3] 9
increment [9] 10
10

если ленивая оценка не используется, то результат будет:

square [1] 1
square [2] 4
square [3] 9
square [4] 16
square [5] 25
increment [1] 2
increment [4] 5
increment [9] 10
increment [16] 17
increment [25] 26
2
5
10
, если вы видите исходный код, то каждая функция list, map, take и mapSeq возвращает промежуточный List структуры данных. Не выполняется короткое слияние.

это подводит меня к моему главному вопросу: делают ли библиотеки неизменяемыми.js и ленивый.js выполняет короткий синтез?

в причина, по которой я спрашиваю, заключается в том, что, согласно документации, они "по-видимому" делают. Однако я настроен скептически. У меня есть сомнения, действительно ли они выполняют короткий синтез.

например, это взято из README.md файл неизменяемый.js:

Immutable также предоставляет лень Seq, что позволяет эффективно связывать методы сбора, такие как map и filter без создания промежуточных представлений. Создать какой-нибудь Seq С Range и Repeat.

поэтому разработчики неизменяемы.js утверждают, что их Seq структура данных позволяет эффективно связывать методы сбора, такие как map и filter без создания промежуточных представлений (т. е. они выполняют короткое слияние).

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

кроме того, в их документации Ленивый Seq они говорят:

Seq описывает ленивую операцию, позволяя им эффективно использовать цепочку всех итерационных методов (таких как map и filter).

Seq является неизменяемым - после создания Seq он не может быть изменен, добавлен, изменен или иным образом изменен. Вместо этого любой мутативный метод, вызываемый Seq, будет вернуть новый след.

Seq ленив - Seq делает так мало работы, как необходимо, чтобы ответить на любой вызов метода.

так установлено, что Seq действительно ленивый. Однако нет примеров, чтобы показать, что промежуточные представления действительно не создаются (что они утверждают, что делают).


переход к ленивым.js у нас такая же ситуация. К счастью, Даниэль Тао написал блог пост о том, как ленивый.JS работает, в котором он упоминает, что в его сердце ленивый.js просто выполняет функциональную композицию. Он приводит следующий пример:--52-->

Lazy.range(1, 1000)
    .map(square)
    .filter(multipleOf3)
    .take(10)
    .each(log);

function square(x) {
    return x * x;
}

function multipleOf3(x) {
    return x % 3 === 0;
}

function log(a) {
    console.log(a);
}
<script src="https://rawgit.com/dtao/lazy.js/master/lazy.min.js"></script>
на map, filter и take функции производят промежуточную MappedSequence, FilteredSequence и TakeSequence объекты. Эти Sequence объекты по существу являются итераторами, которые устраняют необходимость промежуточных массивов.

однако, из того, что я понимаю, есть по-прежнему не происходит короткого слияния. Промежуточные структуры массива просто заменяются на intermediate Sequence структуры, которые не срастается.

я могу ошибаться, но я считаю, что такие выражения, как Lazy(array).map(f).map(g) произвести два отдельных MappedSequence объекты, в которых первый MappedSequence объект передает свои значения второму, а не второму, заменяя первый, выполняя работу обоих (через композицию функций).

TLDR: Do Неизменный.js и ленивый.js действительно выполняет короткий синтез? Насколько я знаю, они избавляются от промежуточных массивов, эмулируя ленивую оценку через объекты последовательности (т. е. итераторы). Однако я считаю, что эти итераторы прикованы: один итератор лениво передает свои значения следующему. Они не объединяются в один итератор. Следовательно, они не "устраняют промежуточные представления". Они только преобразуют массивы в объекты постоянной последовательности пространства.

1 ответов


Я автор неизменными.js (и поклонник ленивых.в JS).

Совсем Ленивый.js и неизменный.JS Seq использует короткий синтез? Нет, не совсем. Но они удаляют промежуточное представление результатов операции.

Short-cut fusion-это метод компиляции/транспиляции кода. Ваш пример хорош:

var a = [1,2,3,4,5].map(square).map(increment);

Transpiled:

var a = [1,2,3,4,5].map(compose(square, increment));

ленивый.js и неизменный.Яш не transpilers и не переписывать код. Они во время выполнения библиотеки. Поэтому вместо того, чтобы срезать синтеза (метод компилятора) они используют состав итерируемый (техника выполнения).

вы отвечаете на это в своем TLDR:

насколько я знаю, они избавляются от промежуточных массивов, эмулируя lazy оценка через объекты последовательности (т. е. итераторы). Тем не менее, я верю что эти итераторы связаны: один итератор, подающий свои значения лениво к следующему. Они не объединяются в один итератор. Следовательно они не "исключить промежуточные представления". Они только преобразуйте массивы в объекты постоянной пространственной последовательности.

это совершенно верно.

давайте:

массивы хранят промежуточные результаты при цепочке:

var a = [1,2,3,4,5];
var b = a.map(square); // b: [1,4,6,8,10] created in O(n)
var c = b.map(increment); // c: [2,5,7,9,11] created in O(n)

короткая транспиляция слияния создает промежуточные функции:

var a = [1,2,3,4,5];
var f = compose(square, increment); // f: Function created in O(1)
var c = a.map(f); // c: [2,5,7,9,11] created in O(n)

состав итерируемый создает промежуточные итерируемых:

var a = [1,2,3,4,5];
var i = lazyMap(a, square); // i: Iterable created in O(1)
var j = lazyMap(i, increment); // j: Iterable created in O(1)
var c = Array.from(j); // c: [2,5,7,9,11] created in O(n)

обратите внимание, что использование итерационной композиции, мы не создали запас промежуточных результатов. Когда эти библиотеки говорят, что они не создают промежуточных представлений, они имеют в виду именно то, что описано в этом примере. Не создается структура данных, содержащая значения [1,4,6,8,10].

конечно некоторые промежуточное представление сделан. Каждая "ленивая" операция должна что-то возвращать. Они возвращают итерируемые. Создание этих данных чрезвычайно дешево и не связано с размером данных прооперирован. Обратите внимание, что в короткой транспиляции слияния также выполняется промежуточное представление. Результат compose новая функция. Функциональная композиция (написанная от руки или результат компилятора short-cut fusion) очень связана с Итерабельной композицией.

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


Appx:

это то, что простая реализация lazyMap может выглядеть так:

function lazyMap(iterable, mapper) {
  return {
    "@@iterator": function() {
      var iterator = iterable["@@iterator"]();
      return {
        next: function() {
          var step = iterator.next();
          return step.done ? step : { done: false, value: mapper(step.value) }
        }
      };
    }
  };
}