Статьи

Оптимизация вашего JavaScript с помощью функционального программирования

обзор

Вообще говоря, есть много способов оптимизировать ваш код. Например, вы можете пересмотреть ваши основные алгоритмы и, возможно, попытаться ускорить выполнение некоторых циклов. Кроме того, вы можете воспользоваться некоторыми рекомендациями, такими как избегать конкатенации больших строк или чрезмерного использования замыканий. Но вместо того, чтобы сократить время выполнения ваших функций, что, если бы вы могли избежать вызова ненужных функций? Поскольку JavaScript является функциональным языком программирования, существует несколько методов, которые мы можем использовать для сокращения времени выполнения ваших программ:

  1. Ленивая оценка
  2. мемоизации

Прежде чем мы начнем, вот некоторые концепции, которые вам необходимо понять, чтобы понять смысл остальных: 

Чистые функции:  функция чистая, когда у нее нет побочных эффектов, то есть не читает / не изменяет никакие внешние данные.

Ссылочная прозрачность (RT):  поддержка чистых функций, функции RT — это те, которые вычисляют свои возвращаемые значения исключительно на основе предоставленного ввода. Это самая близкая вещь к математической функции. Поскольку его результат согласуется с вводом, функции RT очень легко рассуждать и тестировать. 

Теперь давайте перейдем к: 

Ленивая оценка

В JavaScript композиция функций играет важную роль в реализации отложенных вычислений. Ленивая оценка — это стратегия выполнения «по требованию», при которой функция вызывается только при необходимости. Нестрогие языки, такие как Haskell, имеют встроенную ленивую оценку в каждом вызове; тем не менее, JavaScript является строгим языком, что означает, что все функции оцениваются с нетерпением. Рассмотрим следующий пример. Предположим, что мы хотим извлечь конечное подмножество чисел из бесконечного множества. Какая? Потерпите меня. Мы можем создать функцию под названием  range (начало, конец),  которая создает массив целых чисел от указанного начала до конца:

var firstTenItems = range(1, Infinity).take(10);

firstTenItems();//-> 1, 2, 3, 4....10?

Вы можете задаться вопросом: как может завершиться этот вызов функции, если мы генерируем бесконечный список чисел? На нестрогом языке, таком как Haskell, мы могли бы написать эквивалентную программу, которая будет фактически завершать работу и эффективно брать первые 10 элементов. В JavaScript эта похожая программа будет генерировать бесконечный цикл, так как она будет стараться сначала оценить функцию диапазона. Поскольку отложенная оценка не является встроенной в JavaScript, единственной альтернативой является ее эмуляция с помощью API, который основан на функциональной композиции, чтобы обойти механизм активной оценки JavaScript в своей основе с помощью функциональной библиотеки Lazy.js. 

 var evenNumbers = Lazy.generate(function (x) {           
        return 2 * x;
    }); //-> 2, 4, 6, 8,...

    evenNumbers.take(300).each(doWork);                      

Функция  Lazy.generate  по сути является структурой потоковых данных. Поток — это массив данных, похожий на массив, который очень тесно связан со связанным списком (на языке, подобном Java). Он может быть использован для генерации бесконечного количества элементов. Хотя это звучит странно, за кадром это — сила ленивой оценки посредством композиции функций, которая позволяет этому случиться. Ранее показанный код представляет собой классическую проблему разделения функции, которая может генерировать бесконечное число возможных значений, и селектора, который выбирает подходящие.

Использование Lazy.js позволяет нам эффективно модулировать эту программу, поскольку инициализация потока номеров откладывается до тех пор, пока не будет вызвана каждая функция. Без этого вам пришлось бы писать код, похожий на следующий: 

var threshold = 10;
for (var i = 0; i < Infinity; i+=2) {
  if (i >= threshold) {
      break;
  }
  else {
    var item = items[i]; 

    // do work on item
  }
}

Этот одноразовый код не используется повторно, его написание занимает больше времени, и его сложнее поддерживать. Используя Lazy.js, мы избегаем написания такого кода в пользу более декларативного подхода, который использует преимущества ленивых вычислений. Поскольку это позволяет избежать ненужных вычислений (или, по крайней мере, откладывать только при необходимости), ленивая оценка также показала ускорение выполнения программ JavaScript. 

мемоизации

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

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

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

function generateSecureHash(username, password) {
 // run expensive algorithm

 return hash;
}
generateSecureHash('jim', 'Jim@Pwd!');

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

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

var computeFactors = (function factors(num) {

// run algorithm

 return factors;
}).memoize();

computeFactors(100); //-> [1,2,4,8,16,32,64]

Опять же, последующие вызовы со 100 будут ленивыми и просто вернут запомненный результат. Прелесть этого уровня кэширования в функциональных языках и в JavaScript может быть реализована без добавления какого-либо стандартного кода размещения / проверки кеша во всех наших функциях. Вот посмотрите на реализацию памятки путем расширения   объекта Function

Function.prototype.memoized = function (key) {

        if (this.length === 0) {
            console.log("Cannot memoize function call with empty arguments");
            return;
        }
        else {
            // Handle identifiable objects
            if (key.getId && (typeof key.getId === 'function')) {
                // Extract objs ID
                key = key.getId();
            }
            else {
                // Last resort, handle objects by JSON-stringifying them
                if (!isNumeric(key) && !isString(key)) {
                    // stringify key
                    key = JSON.stringify(key);
                }
            }
        }

        // Create a local cache
        this._cache = this._cache || {};
        var val = this._cache[key];

        if (val === undefined || val === null) {
            //alert("Cache miss, apply function");
            console.log("Cache miss");
            val = this.apply(this, arguments);
            this._cache[key] = val;
        }
        else {
            console.log("Cache hit");
        }
        return val;
    };

    // Enhance Function prototypes

    // Provide ability for functions to memoize (or be proxied)
    Function.prototype.memoize = function () {

        var fn = this;

        if (fn.length === 0 || fn.length > 1)
            return fn;  // only handle functions with a single formal argument

        return function () {
            return fn.memoized.apply(fn, arguments);
        };
    };

Функциональные языки уступают механизмам ленивой оценки языковой платформе и освобождают разработчиков от необходимости беспокоиться об их реализации и тестировании. В JavaScript мы можем сделать то же самое, позволяя высокоуровневым конструкциям, таким как compose и memoize, обрабатывать любые оптимизации в нашем коде. Это предпочтительно, потому что это означает, что мы можем тестировать наши функции независимо от какого-либо механизма оптимизации. Надеемся, что нам понравился этот пост, и мы начинаем понимать, что функциональное программирование — это ваша парадигма программирования! Оставайтесь с нами, чтобы узнать больше! 

Ресурсы

  1. http://danieltao.com/lazy.js/