Функциональный JavaScript

функциональный javascript

Draft!!!

Осторожно! Слабонервным, холерикам и беременным не читать - выносит мозг, меняет сознание!

Переменные

function func(fn, arg1, arg2) {
    return function () {
        return fn.call(null, arg1, arg2);
    };
}

Переменные - зло. Основная причина - это то, что они переменные т.е. меняют своё состояние во времени самым непредсказуемым образом. Проблемы начинаются когда переменных появляется много то за ними трудно уследить, а так же количество кода, логика которого зависит он них, и к статиб автор переменной никогда не может быть уверен как ее используют в будущем. Другими словами, закон Мерфи("Что может произойти то обязательно произойдет") в действии, любой кусок кода который имеет доступ к переменной может изменить самым непредсказуемым образом.

С появлением переменной появляется зависимость во времени до и после присваивания. Проблема в том что переменные приносят в систему понятие состояния. Как правило каждое состояние приносит нам по два метода для роботы с ним, классический пример, malloc и free, open и close, getCanvas и freeCanvas и т.д.



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

Пример с сменой состояния:

var s = 8,
    q = 3;

function badFunction() {
    // ...
    s = s - 2;
    // ...
    q = 1/s;
    // ...
    return q + s;
}
badFunction();

Во-первых, в примере видно плохие имена переменных 🙂 во-вторых, видно, как функция обращается к переменной s и меняет её состояние. Так что будет, если переменная s будет иметь значение 2? Да, верно программа "вылетит" с исключением, но только в текущем event loop. Следующий код, который будет читать значения s и q, получит значения в не консистентном состоянии, т.е. другие части программы могут не ожидать таких значений и могут работать неверно.

Поэтому желательно не использовать переменные или использовать их в наименьшей области видимости, насколько это возможно - чем меньше кода имеет доступ к переменной, тем лучше. В идеале время жизни переменной должно быть в пределах 5-15 линий кода. Время жизни переменной это "путь" от её создания до последнего использования. Но так как эта запись о функциях, то вернёмся к ним.

Объявление функций

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

Объявление функции сродное объявлению обычной переменной:

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

Здесь переменной square присваивается функция, которую можно вызвать в любом другом месте, передать ей параметр x и она вернёт квадрат этого значения. Прошу обратить внимание на то, что функция не меняет состояние никаких внешних переменных (чистая функция, без побочных эффектов), а также является детерминированной, т.е. для одинаковых входных данных будет возвращать одинаковый результат. Ещё один вариант объявления:

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

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

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

Вызов функций

var add = function (x, y) {
    return x + y;
}

add(1, 2);

Имея функцию, сохранённую в переменной или просто определенную в текущей или доступной области видимости, её можно использовать собственно для чего она и предназначалась, иными словами вызвать её с необходимыми параметрами и получить ожидаемый результат. Есть несколько способов, как это можно сделать. Первый метод собственно классический: используя скобки funcName(x, y), где funcName имя функции а x и y передаваемые параметры (аргументы). Собственно результат своей работы функция возвращает "левее", и если его не присвоить переменной или сразу передать как аргумент другой функции, то он будет утрачен.

var result = add(3, 6); // => result = 9;

В JavaScript функция является по сути объектом и имеет некоторые дополнительные методы. Первый из них это метод call.

var result = add.call(null, 3, 6); // => result = 9;

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

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

var result = add.apply(null, [3, 6]); // => result = 9;

Есть ещё один метод, но он не совсем связан з вызовом функций, скорее с созданием новой функции. Именуется этот метод bind и он также позволяет "мертво" привязать контекст и параметры к функции. Если указаны параметры — они будут прибавлены к каждому вызову новой функции, причём встанут перед теми, которые указаны при вызове. Например:

var add1 = add.bind(null, 1);
var result = add1(2); // => result = 3;

В даном примере видно как при помощи метода bind была создана новая функция, привязанная к null объекту и также продемонстрировано, как можно сделать частичную передачу параметров. Т.е. привязали аргумент 1 к функции.

Еще один довольно часто используемый фокус, когда функция сразу же после обьявления исполняется. Это позволяет скрыть переменные и логику которые не нужны в других частях программы, а также создавать объекты, в которые имеют собственные "приватные" переменные/методы:

var module = (function (window) {
    // some logic here
    return {
        exported : 1;
    };    
})(window);

Контекст

this

В языке Си есть такое понятие как структуры (struct) они удобны тем, что позволяют из простых типов создать сложную структуру с полями, через которые можно обратится к значению.

struct Person {
    char* name;
    char* lastName;
};

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

void setName(Person* person, const char* name) {
    person->name = name;
}

К тому же, всегда первым параметром выступала структура (точнее указатель на неё), которую нужно изменить. И тогда изобрели "классы". В общем, добавили возможность в структуру добавлять и функции (методы), при этом при вызове функции ей автоматически передавался указатель на текущий экземпляр и стали именовать его this.

void Person::setName(const char* name) {
    this->name = name;
}

Классы имеют и много других плюшек, но они нам теперь не интересны. Какое это имеет отношение к JavaScript? Очень даже прямое. Авторы JavaScript тоже решили так сделать, только вот в JavaScript нет классов (традиционных), зато здесь есть объекты. Вот авторы и решили, что каждая функция всегда имеет "переменную" this, которая указывает на текущий объект. Если объекта нет, то указывает на глобальный (в браузерах window), что иногда приводит к немного печальным последствиям (поэтому в новых версиях поведение немного изменили , привет use strict). Вот как это выглядит в JavaScript:

var someObj = {
    name : "Ivan",
    lastName : "Petrov",
    setName : function (name) {
        this.name = name;
    } 
};

someObj.setName("Vasil");

Вызов функции которая определена в классе, делается примерно так же как и обращение к значению поля. Если очень по-простому то контекст функции будет равным тому, что идёт до точки перед именем функции. А теперь фокус:

var secondObj = {
    name: "Elena"
};

secondObj.setName = someObj.setName;

secondObj.setName("Vasil");

Как поведёт себя функция скопированная у другого объекта? Верно! Изменится состояние secondObj.name, так как контекст функции будет значение secondObj. Иногда есть надобность передать функцию как аргумент в другую функцию.

function doMagic(setName, name) {
    setName(name);
};
doMagic(secondObj.setName, "Vasil");

Пример работать не будет. Проблема в том, что передаётся функция как переменная, и у неё нет контекста (нет точки при вызове), точнее есть глобальный, но это не то что нужно. Решение проблемы описано высшее, а именно метод bind. Для того чтоб заработало нужно сделать так:

doMagic(secondObj.setName.bind(secondObj), "Vasil");

Или же есть возможность вообще создать функцию и потом применять её в контексте любого объекта. Здесь, кстати, метод bind исполняется в контексте функции setName поэтому он знает к которой функции привязывать контекст.

function setLastName (lastname) {
    this.lastName = lastname;
}

setLastName.bind(someObj)("Vasil");
setLastName.bind(secondObj)("Vasil")

В примере так же продемонстрировано, что результат, который возвращает функция, можно использовать сразу. Т.е. в данном примере метод bind возвращает новую функцию, которая сразу же вызывается. Аналогично можно делать с объектами, если функция возвращает объект можно сразу его использовать, как собственно сделано в jQuery. Данный подход называется цепочка вызовов (chaining) и бывает очень полезен, но в функциональном подходе его заменяет композиция.

Каррирование и частичное применение

var fn = curry(function (a, b, c) {
// ...
});

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

function sum(a, b) {
    return a + b;
}

Что делать, если нам часто надо суммировать одно число к другому, и при том одно число всегда одно и то же? Нужно где то запомнить это значение. Один из вариантов это использовать замыкание и сделать так:

function sum(a) {
    return function (b) {
        return a + b;
    };
}

var increment = sum(1),
    decrement = sum(-1);

increment(10); // => 11;

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

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

function sum3 (a, b, c) {
    return a + b + c;
}

var sumBase3 = sum3.bind(null, 3);
sumBase3(3, 4); // => 10;
sumBase3(3); // => NaN;

Теперь при вызове функции sum3 и передачи ей остаточных параметров будет выполнено суммирование. Здесь таится отличие от каррирования если вызвать sumBase3 с одним аргументом, то суммирование будет выполнено с ошибкой, по той причине, что последний аргумент не был передан. В случае с карированием такого не произойдёт. Карированая функция так же ожидает всех аргументов, только вот суммирование не будет вызвано до тех пор, пока все аргументы не будут получены:

var sum3 = curry(function (a, b, c) {
    return a + b + c;
});
var sumBase3 = sum3(3);
sumBase3(3, 4);     // => 10;
sumBase3(3)(4);     // => 10;
sumBase3(3)()(4);   // => 10;
sumBase3()(3, 4);   // => 10;
sumBase3(3);        // => function;

Вот таким вон нехитрым способом можно уменьшить количество параметров в функциях.

Сигнатура функций

В то время, как JavaScript является языком с динамической типизацией, нет необходимости указывать тип переменной. Это позволяет писать более абстрактный код, хотя временами трудно понять, что ожидает функция и что возвращает. Иногда это приводит к "неожиданным" результатам. Для того, чтобы помочь программисту быстрее ориентироваться в функциях есть несколько подходов, как описать сигнатуры функций. Ах, да, сигнатура функций это как бы описание того что функция получает (аргументы) и то что возвращает.

Первый и самый не интересный нам подход, который используется в ActionScript (часть семейства ECMAScript). В нем используется типизация и параметры описываются более явно, пример:

function sum3(a : Int, b : Int, c : Int) : Int {
    return a + b + c;
}

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

Следующий, более JavaScript подход, представляет JSDoc. Подход более лучший, так как позволят не только описать сигнатуру функций, но и дополнительную информацию, на основании которой можно сгенерировать документацию. Более того, некоторые IDE умеют читать этот формат и использовать для подсказок при разработке, что довольно удобно. Основной минус так это то, что он довольно многословен, и иногда читать смесь из документации и кода не очень-то и удобно. Пример:

/**
* Сумирует три числа
* 
* @param {Integer} a первый аргумент
* @param {Integer} b второй аргумент
* @param {Integer} c последний аргумент
* @return {Integer}
*/
function sum3(a, b, c) {
    return a + b + c;
};

И последний вариант, на котором и остановимся. Предназначен он для описания только самых сигнатур и не более. Не многословен, и нет поддержки в IDE (пока нет). Начнём с того, что он "украден" в правильных функциональных языков где каррирование у всех функций по умолчанию. Поэтому далее будем рассматривать все функции как каррированые. Пример сигнатуры:

//+ sum3 :: Int -> Int -> Int -> Int
function sum3(a, b, c) {
    return a + b + c;
};

Как видно с примера, функция работает с четырьмя параметрами первые три это собственно входные параметры, а четвёртый это результат работы. Ах, да важное замечание: в функциональных языках функции всегда возвращают что-нибудь. Если функция ничего не возвращает, то по факту она ничего не делает... и да, не забываем что в идеале функции чистые, т.е. ничего не изменяют (DOM, ввод-вывод, AJAX и т.п.). Функции также не должны изменять значения, которые переданы как аргументы. Если все же есть необходимость их изменить, тогда нужно создать копию оригинального значения и изменить его. Да, это немного расточительно по отошению к памяти, но более безопасно. Ещё пример:

//+ toUpperCase :: String -> String
function toUpperCase(s) {
    return s.toUpperCase();
}

Более сложный пример:

//+ map :: (a -> b) -> [a] -> [b]
function map(fn, arr) {
    return arr.map(fn);
};

Думаю, этот случай надо более детально рассмотреть. Типов a и b не существует в природе JavaScript, это просто мнемоническое обозначение что тип может быть любой. Квадратные скобки вокруг означают, что это массив элементов a. Так же с последнего примера видно (a -> b), что первый аргумент функции есть функция в которую будет передан один элемент с массива [a], и она должна вернуть значение b, которое впоследствии будет помещено в массив элементов [b]. Несколько примеров для самостоятельного изучения:

//+ indexOf :: a -> [a] -> Int
function indexOf(value, a) {
  return a.indexOf(value);
};

//+ reduce :: (b -> a -> b) -> b -> [a] -> b
function reduce(fn, acc, a) {
  return a.reduce(fn, acc);
};

//+ charAt :: Int -> String -> String
function charAt(i, s) {
  return s.charAt(i);
};

Композиция

var oneAndSecondFn = compose(secondFn, oneFn);

Композиция — процесс применения одной функции к результату другой.

fn1(fn2(fn3(value)));
// равносильно
compose(fn1, fn2, fn3)(value);

Нечто похожее существует уже довольно давно в *nix системах и называется конвеером (pipe).

$ cat file.txt | grep ^boo | foo

Смысл здесь в том, что для достижения конечного результата используется несколько маленьких утилит, которые делают маленький, но необходимый кусок работы. Как в этом примере, одна утилита читает текстовый файл cat file.txt и выводит на экран, но вывод на экран перехвачен и направлен на вход другой утилите grep ^boo, которая в свою очередь фильтрует полученные данные и опять выводит на экран, где они которые опять перехватываются и передаются на вход утилите foo, которая делает с ними тоже что-то светлое и хорошее, и после снова выведет на экран но на этот раз уже успешно, так как вывод не перенаправлен, поэтому пользователь видит результат работы.

Аналогично работает функция compose: переданные ей функции она сохраняет в массиве и возвращает новую функцию, и при её вызове будет выполняться последняя функция из переданных в compose и ей же будут переданные аргументы. В свою очередь результат работы последней функции будет передан предпоследней. И так до самой первой функции, результат которой будет возвращён. С помощью такого подхода можно необходимую логику декомпозировать (розбить на более мелкие части), в результате у нас будет увеличено переиспользование кода, а значит багам будет сложнее скрыться и разработка буте идти быстрее.

Очень важно отметить, что композиция/декомпозиция способствует более абстрактному коду. Абстракция в свою очередь способствует быстрой разработке, (но!) увеличивает время на обучение. Другими словами менее опытные разработчики будут тратить больше времени на обучение.

Функтор

var values = [1, 2, 3, 4, 5].map(function (item) {
    return item + 1;
});
values; // => [2, 3, 4, 5, 6]

Для всех, кто работет з JavaScript продолжительное время, хорошо знаком метод map для массивов. Если же немного подзабыли, то напомню: он обходит весь массив и к каждому элементу применяет функцию, переданную первым аргументом, создает новый массив и в него помещает элементы/значения, которые возвращает функция. Так вот, этот метод - функтор.

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

lentghTree = namesTree.map(function (name) {
    return name.length;
});
           *
          / \
         /   \
        *    "Vova"
       / \
      /   \
     /     \
    /       \
 "Jon"      "Nike"

Трансформируется в:

           *
          / \
         /   \
        *     4
       / \     
      /   \
     /     \
    /       \
   3         4

И вот последний еще более приземленный пример:

function Identity(value) {
    this.value = value;
}

Identity.prototype.map = function (fn) {
    // передаем значение и создаем новый
    // екземпляр с таким же классом
    return new Identity(fn(this.value))
};

Построитель вычислений aka "Монады"

Hey Underscore, You're Doing It Wrong!

Очень интересный доклад, рекомендую посмотреть, хотя если нет времени и/или желания смотреть 40 минутный доклад, ниже будет краткое содержание. Так в чем же проблема самого Underscore? Underscore позиционируется как библиотека с функциональным подходом. но проблема здесь кроется в параметрах, точнее их последовательности. Выше была описана основа функционального подхода - каррирование и композиция, так вот в Underscore сложно делать композицию через последовательность аргументов. Потому что в нем при вызове метода надо указать первым аргументом данные которые нужно обработать и после функцию которая будет обрабатывать. Это исключает возможность использовать каррирование для создания необходимых функций на лету.

var isOdd = function () {
    return item % 2;
};
var filtredData = _.filter([1, 3, 4, 5, 6], isOdd);

В этом и кроется проблема. Здесь нам нужна переменная для сохранения данных, которые будут обработаны дальше. Что если мы сделаем функцию следующего вида:

var filter = curry(function (fn, arr) {
    return arr.filter(fn);
});

Первое что нужно отметить мы сделали её каррированой и второе поменяли местами аргументы. И если теперь сделаем так:

var filterOdd = filter(isOdd);

У нас будет функция, которой можно передать любой массив (или не очень массив а все у чего есть метод filter) и она вернёт фильтрованный. Думаю, её мощь пока не очень видна - тогда немного усложним.

var showResult = curry(function (selector, data) {
        // пример для простоты с использованием jQuery 
        var $el = $(selector);
        $.each(data, $el.append.bind($el));
    }),
    processData = function (item) {
        return item + (item * 42);
    },
    processAndShow = compose(
        showResult('#data_block'),
        map(processData),  // "православный" и каррированый map
        filterOdd          // или filter(isOdd)
    );

processAndShow([1, 3, 4, 5, 6]);

Небольшое напоминание, что функции, скормленные compose, будут исполнены в обратном порядке. Иногда в библиотеках для упрощения делают функцию pipe, в которой аргументы-функции идут в прямом порядке.

Как видно из примера, у нас нигде явно нет передаваемых переменных с данными, но они передаются от одной функции к другой, и результат уже выводиться пользователю. Таким способом можно сделать много работы уже проверенными функциями, которые чётко делают свою роботу и без побочных эффектов (вывод пользователю это побочный эффект, но мы пока закроем на это глаза). Как помним, showResult каррированая и ей можно скормить аргументы по один, и когда она уже будет "сытая" - выдаст результат.

Более того с таким подходом можно делать и асинхронные операции:

var getJsonByUrl = function (url) {
    // ... тело скрыто для интриги
};
var getProcessAndShow = compose(
        showResult('#data_block'),
        map(processData),  // "православный" и каррированый map
        filterOdd,          // или filter(isOdd)
        getJsonByUrl
    );

getProcessAndShow('//myserver/getdata.json');

Промисы? Колбэки? Асинхронный код? Не, не слышал. Но вот тут есть небольшая проблема: а что если сервер недоступен? Или данные должен был ввести пользователь, а их нет? Ой, беда...

Maybe

Хотя если подумать не такая уже и беда, все можно решить в функциональном стиле. Для этого в нас есть (будут?) монады! Посмотрим, как можно определить монаду maybe:

var Maybe = function (value) {
    this.value;
}
// иногда можно встретить с именем fmap
Maybe.prototype.map = function (fn) {
    if (this.value !== null && typeof (this.value) !== 'undefined') {
        return new Maybe(fn(this.value));
    } else {
        return this;
    }
    return new 
}
//+ maybe :: a -> m a
var maybe = function (val) {
    return new Maybe(val);
}

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

var getProcessAndShow = compose(
        map(showResult('#data_block')),
        map(map(processData)),// извлекаем массив а из него елементы
        map(filterOdd),// извлекаем из монады массив
        maybe,
        getJsonByUrl
    );

getProcessAndShow('//myserver/getdata.json');

Ой, сколько же в нас появилось mapов, для чего же их столько нужно? Ну что ж, попробуем разобраться. Все дело в том, что maybe возвращает контейнер с данными (в даном случае пускай будет будет массив), и чтоб их извлечь, мы используем метод map. Извлечённые данные фильтруем. Далее, после извлечения и фильтрации, данные опять запаковываются, и опять получаем монаду, поэтому перед обработкой данных опять извлекаем данные из монады, а после и из массива. Обрабатываем, результат опять запаковывается дважды (в массив, и в монаду/контейнер). И последняя команда: собственно перед выводом массива извлекаем из монады и передаём обработчику.

Представим случай, что данные мы не получили, т.е. в maybe ничего не передали, при каждом map для монады просто будет проверка, есть ли данные (а их нет), поэтому обработчики (filterOdd, map(processData)...) и не будут выполнены.

Either

Хорошо, теперь у нас есть проверка на пустые значения maybe, но что если в нескольких местах эти значения могут отсутствовать? Как найти место где нет данных? На помощь приходит мистер Propper монада Either. Её смысл очень похож на монаду Maybe, только с тем различием, что мы можем вернуть осмысленный текст ошибки. Так же эта монада удобна в функциях, где может быть исключительная ситуация, и вместо выкидывания ошибки можно просто вернуть монаду без значения, но с информацией про ошибку, что поможет локализировать проблему и не дать коду что ни будь нам сломать.

Пример реализации:

var Either = function (lvalue, rvalue) {
    this.lvalue;
    this.rvalue;
}

Either.prototype.map = function (fn) {
    if (this.rvalue !== null && typeof (this.rvalue) !== 'undefined') {
        return new Either(this.lvalue, fn(this.rvalue));
    } else {
         return new Either(this.lvalue, undefined);
    }
    return new 
}
//+ either :: a -> b -> m b|a
var either = curry(function (lval, rval) {
    return new Either(lval, rval);
});

Напишем короткую функцию для демонстрации:

var getElement = function(selector) {
    return either(
        'getElement:: Element "' + selector + '" not found',
        document.querySelector(selector)
    );
};

var property = curry(function (name, object) {
    return object[name];
});

var getProcessAndShow = compose(
        map(showResult('#data_block')),
        map(map(processData)),  // извлекаем массив а из него елементы
        map(JSON.parse),
        map(property('innerHtml')),
        getElement
    );

getProcessAndShow('#elID');

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

var processAndShow = mcompose(
        showResult('#data_block'),
        map(processData),
        JSON.parse,
        property('innerHtml'),
    );

processAndShow(getElement('#elID'));

Обращаю внимание на то, что теперь метод getElement вынесен из композиции, так как ему на вход нужно "чистое" значение, а не монада, и возвращает он монаду.

Даже если не использовать функциональный подход, то использовать either или maybe в разработке может поднять стабильность на новый уровань. Всегда возаращая манады в качестве результата можно убрать кучу условий которые проверяют есть ли резульат от функции, а также можно забыть про try catch (которым часто злоупотребляют и оборачивают большие куски кода, и не обрабатывают пойманое исключение). Такой себе NullObject паттерн. Для удобства монадам можно добавить методы для получения данных, например, getOrElse, где единственным аргументом будет занчение по умолчанию, на случай если монада "пустая". Или метод getOr, где аргументом будет функция которая будет исполнена в случае отсутствия данных в монаде и результат в виде результата из этой функции.

var getProduct = function (id) {
    if (!id) {
        return either('Missing product ID', null);
    } else {
        return either(
            'Could not fetch product with ID: ' + id,
            db.getProduct(id)
        );
    }
}

var getDefaultProduct = function () {
    return {
        name: "",
        // other properties
    };
}

var product = getProduct('black underpants');

showName(product.getOrElse({name:""}).name);

showName(product.getOr(getDefaultProduct).name);

Future

TBD

Добавить комментарий