Композиция функций
Вот еще пример простого приложения — опросника.
Необходимо ответить на каждый блок. Каждый блок содержит несколько вопросов. После ответа на один блок, мы переходим к следующему.
Каждый блок можно представить как панель, которая может быть в режиме вопроса, режиме результата или неактивной.
В каждой панели могут быть разные поля. Строковые, числовые, даты.
Поля могут быть в двух режимах — редактирования или результата.
Посмотрим, как можно подойти к решению этой задачи, используя функциональный подход.
Помните нашу любимую функцию prop?
tweeps.map(prop("name"));
У нее есть брат-близнец func.
tweeps.map(func("to.String"));
Она возвращает функцию, которую вы можете применять к объектам.
Теперь посчитаем результат каждого блока в опроснике
buildSummary: function(){ return div(this.components.map(func("buildSummary"))); }
Принцип должен быть очевиден. Мы возвращаем div, в котором будут элементы, созданные функциейbuildSummary для каждого блока опросника.
В этом примере каждый компонент сам знает как представить свой результат. Но иногда панель должна отобразить результат специфическим образом.
Поэтому мы можем написать 2 функции: buildSummary и getSummary.
Первая — строит полное представление, включая html теги.
Вторая — возвращает объект, который содержит необходимые результаты.
И как только нам понадобилась хитрая обработка результатов, вся красота начала рушиться.
buildSummary: function(){ var div = document.createElement("div"); for(var i =0; l=this.components.length; i<l; ++i) { p = document.CreateElement("p"); p.innerHTML = this.components[i].getSummary().text; div.appendChild(p); } return div; }
Однако, мы уже достаточно функционально ориентированы, чтобы улучить этот кусок кода. Первое очевидное улучшение — применить foreach.
buildSummary : function(){ var div = document.createElement("div"); this.components.forEach(function(component){ var p = document.createElement("p"); p.innerHTML = component.getSummary().text; div.appendChild(p); }); return div; }
Мы избавились от переменных цикла, но возможно ли использовать map?
buildSummary : function(){ return div(this.components.map(function(component){ var p = document.createElement("p"); p.innerHTML = component.getSummary().text; return p; })); }
Коротко, но далеко до идеала. Основная проблема в этом выражении:
component.getSummary().text;
Проблема в том, тут происходит не одна, а целых три вещи:
- Получение результата через getSummary()
- Получение свойства text
- Оборачивание результата в тег p
А как насчет нескольких функций map?
buildSummary: function() { return div(this.components. map(function(component){ return component.getSummary(); }).map(function(summary){ return summary.text; }).map(function(text){ var p = document.createElement("p"); p.innerHTML = text; return p; })); }
Функциональный стиль налицо, но выглядит страшно. И читать очень неудобно.
Но давайте глянем на код еще разок. Что у нас здесь?
return component.getSummary();
Здесь мы вызываем метод объекта. Но ведь мы создали специальную функцию для этого, func.
buildSummary: function() { return div(this.components. map(func("getSummary")). map(function(summary){ return summary.text; }).map(function(text){ var p = document.createElement("p"); p.innerHTML = text; return p; })); }
А здесь?
function(summary){ return summary.text; }
Мы получаем доступ к свойству объекта. И для этого тоже есть удобная функция.
buildSummary: function() { return div(this.components. map(func("getSummary")). map(prop("text")). map(function(text){ var p = document.createElement("p"); p.innerHTML = text; return p; })); }
Остался последний участок.
function(text){ var p = document.createElement("p"); p.innerHTML = text; return p; }
Мы здесь создаем DOM элемент и устанавливаем его внутреннее свойство. У нас есть что-то похожее в нашем DSL, не правда ли?
buildSummary: function() { return div(this.components. map(func("getSummary")). map(prop("text")). map(p)); }
Теперь почти красиво. Но есть один нюанс. Мы делаем 3 прохода по списку. В каких-то случаях это может быть нормально, но в целом несколько неоптимально. Что же можно сделать?
Пора использовать композицию функций. Мы хотим заставить одну функцию делать то, что делают три.
var summarize = compose( [p, prop("text"), func("getSummary")]);