Функциональное программирование на Javascript

http://vimeo.com/49384334
  • Перевод
  • Tutorial


Краткое содержание:

Возьмем немного функций высшего порядка, добавим частичное применение функций, приправим fold с map-ом и получим Javascript DSL для работы с DOM.

Человеческим языком:
Простое и понятное введение в функциональное программирование на чистом и понятном Javascript.

В отличие от «Через тернии к Haskell» все разжевано (возможно даже слишком) и разложено по полочкам.

Прочтение статьи развоплотит миф о неприменимости ФП в реальной жизни. Вы сможете смотреть на решение одной и той же задачи с разных точек зрения. Прямо как на картинке.



Функции


Начнем с простого определения функции.
function add(a,b){
	console.log(a+b);
};



Можно тот же код записать по-другому.
var add = function(a,b){
	console.log(a + b);
};



Одним из больших плюсов Javascript является то, что функции в нем являются полноценными объектами. Настоящие First Class Citizen.
В отличие, например от Java, где функция отдельно от объекта существовать не может.

Приведенная выше функция работает с побочными эффектами, то есть изменяет состояние внешнего мира. Это выражается в использовании console.log().

А теперь рассмотрим пример чистой функции.
var add = function(a,b){
	return a + b;	
};



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

Итак, первое правило функционального программирования — We don't talk about fight club используем чистые функции.

Функции высшего порядка


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

Вот простой пример функции, которая тоже возвращает функцию.

function makeAdder(base){
	return function(num){
		return base + num;
	}
}


И пример ее использования.
var add2 = makeAdder(2);
add2(3); //5
add2(7); //9



Просто и очевидно.

А вот достаточно известный пример функции высшего порядка
var el = document.getElementById("btn");

el.addEventListener("click", function (event){

});



addEventListener в качестве параметра получает функцию. То есть addEventListener является функцией высшего порядка.

И функция-обработчик будет вызвана, когда произойдет какое-то событие.

Возможно вам более привычен другой вариант:

$("input[type=submit]").on("clink", function(event){
	// ...
});



Или еще тысяча и один способ, которыми jQuery позволяет описывать обработчики.

Итак еще раз определение:
ФВП — это функции, которые либо возвращают функции либо принимают функции в качестве параметров.

Циклы



Старые знакомые.
Под циклом будем понимать стандартное лобовое решение. Примерно такое

for(var i  =0; i<n; ++1){
	//
}



Или такое
while(n--){
	// ...
}
// ...



Зачем мы используем циклы? Давайте разберем несколько стандартных вариантов использования, и увидим, что циклы — не всегда лучшее решение.

Первый вариант — обход массивов и списков

for(var i =0; l< arr.length; i<l; ++i){
	console.log(arr[i]);
}



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

Второй вариант — вытаскивание данных из списков

var names = [];
for (var i =0; l= tweeps.length; i< l; ++i) {
	names.push(tweeps[i].name);
}


В этом случае — список пользователей твиттера.
При помощи цикла мы получаем список имен наших пользователей

Еще один вариант использования — агрегация данных в списке:

var html = "";
for(var i =0; l = items.length, i<l, i++){
	html += '<li>' + items[i] + '</li>';
}

this.list.innerHTML = html;



То есть мы агрегируем данные списка, и получаем на выходе другую структуру данных.

foreach


Я говорил, что циклы — не всегда самое лучшее решение, но какие вообще есть альтернативы?

Чем можно заменить подобный цикл?

for (var i =1; l = arr.length; i< l; ++i){
	console.log(arr[i]);
}



Например foreach.

Array.prototype.forEach

arr.forEach(function(item){
	console.log(item);
});



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

Но в чем принципиальная разница между этим
for (var i =1; l = arr.length; i< l; ++i){
	console.log(arr[i]);
}


и этим

arr.forEach(function(item){
	console.log(item);
});

?

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

Но есть еще кое что. Глядя на код, можно сказать, чему уделяется внимание в каждой из реализаций.

Первый участок ориентирован на саму механику цикла. Взять число, увеличить его на единицу, получить элемент массива по индексу, произвести действие.

Второй пример гораздо проще для понимания. Мы делаем что-то с каждым элементом списка.

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

Итак, для чего мы можем использовать циклы:
  • Побочные эффекты
  • Трансформация
  • Фильтры
  • Комбинирование элементов
  • Еще куча вариантов


map


Давайте рассмотрим еще одну функцию, которая есть в Javascript.

var names = [];

for( var i =0, l= tweeps.length, i< l; ++i){
	names.push(tweeps[i].name);
}


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

	//Array.prototype.map
var names = tweeps.map(function (tweep){
	return tweep.name;
});



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

var names = tweeps.map(function(t){return t.name;});



Я не фанатик записи кода в одну строку. Но то, сколько идей можно выразить одной строкой говорит о выразительности вашего API.
Теперь поищем упоминания в твиттере.

var str = "mentioned by";

for(var i =0; l= tweeps.length; i < l; ++i){
	str += tweeps[i].name;
	if(i< tweeps.length-1) {str += ", "}
}



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

  • Вытаскиваем имена пользователей
  • Объединяем имена пользователей (при этом в конце списка не должно быть запятой)
  • Используем запятую в качестве разделителя


Перепишем, используя map и join

var str = "mentioned by " + tweeps.map(function(t){
	return t.name;
}).join(", ");


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

Назовем ее prop

function prop(name){
	return function (object){
		return  object[name];
	}
}


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

Какое-то запутанное объяснение получилось. Давайте просто попробуем использовать эту фукнцию на реальной задаче.

var str = "Mentioned by " + tweeps.map(prop ("name")).join(", ");


Итак, еще один однострочник. Достаточно неплохая выразительность. А функция prop не так уж бесполезна.

reduce


Это прабабушка для for, foreach, while и прочих подобных структур. Эта функция также известна под именем fold.

Опять начнем с примитивного примера.
var totalLength = 0;
for(var i=0; i< buffers.length; i++){
	total.Length += buffers[i].length;
}


Просто суммируем длину буферов.
Какие шаги мы должны выполнить?
  • Получить длины буферов
  • Просуммировать длины


Use the function, Luke.

Сначала мы используем map, чтобы получить список, содержащий длины буферов
var totalLength = buffers.
	map(function (buffer) {return buffer.length; })


А на втором шаге мы применим reduce, чтобы получить их сумму.
var totalLength = buffers.
	map(function (buffer) {return buffer.length; }).
	reduce(function(sum, curr){return sum+curr;}, 0);


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

Как-то опять слишком сложно. Давайте просто посмотрим, что будет происходить, если мы применим reduce к простому списку.


[10, 5, 15, 10, 10].reduce(function(sum, curr){return sum+curr;}, 0);

// [10, 5, 15, 10, 10]
//    sum   curr 
// => 0,    10    => 10
// => 10,   5     => 15
// => 15,   15    => 30
// => 30,   10    => 40
// => 40,   10    => 50


Итак, с помощью reduce мы можем легко просуммировать элементы списка.

Но у нас уже было что-то похожее. Сравните.

function (prev, curr){return prev + curr;}

и
function add(a,b){
	return a+b;
}

Поэтому мы можем немного упростить нашу функцию по вычислению суммарной длины буферов.

var totalLength = buffers.
	map(function (buffer) {return buffer.length; }).
	reduce(add, 0);


Теперь стало яснее? reduce просто суммирует все элементы списка, применяя функцию add. Начальное значение суммы равно нулю. Что может быть проще?

Но на этом упрощения не заканчиваются. Сравните

function (buffer) {return buffer.length; }

и
prop("length")


Брюки превращаются…
var totalLength = buffers.
	map(prop("length")).
	reduce(add, 0);


В элегантные шорты.

Ну и, естественно, мы можем записать это в одну строку

var totalLength = buffers.map(prop("length")).reduce(add, 0);


Использование свертки (reduce) вместо циклов позволяет нам думать на другом уровне абстракции. Мы совершаем операции над списком, а не на уровне каждого элемента.

Асинхронные вызовы



Но использование reduce a.k.a fold для суммирования списков — очень упрощенный пример. Идея гораздо мощнее. Давайте разберем еще один пример.

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

Задача.
  • Загрузить несколько скриптов
  • Склеить их
  • сохранить порядок скриптов при склейке

То есть надо написать функцию примерно такого вида:

combine(["/jquery.js",
		"/underscore.js",
		"/backbone.js"], function(content){
			// content должен содержать все скрипты, склеенные в правильном порядке.
	});



Напишем реализацию функции combine. Сначала — лобовой подход.
function combine(scripts, callback){
	var data [];
	for(var i =0; l = scripts.length; i< l; ++i){
		// ....
	}
}


Для получения скриптов было бы логично использовать jQuery.ajax:

function combine(scripts, callback){
	var data [];
	for(var i =0; l = scripts.length; i< l; ++i){
		jQuery.ajax({

			url: scripts[i],
			success : function(response){
				// ....
			}
		});
	}
}

Подобный код не будет тормозить браузер, поскольку запросы к серверу будут отправлены асинхронно. То есть при выполнении будет 3 параллельных запроса.

Напишем обработчик для успешного скачивания скрипта.

function combine(scripts, callback){
	var data [];
	for(var i =0; l = scripts.length; i< l; ++i){
		jQuery.ajax({

			url: scripts[i],
			success : function(response){
				data[i] = response;
				if(data.length === scripts.length){
					callback(data.join(""));
				}
			}
		});
	}
}


Вроде бы функция готова. Но есть два но.
Во-первых уродливо, во-вторых — оно не будет работать.

С чем тут могут быть проблемы? С областями видимости Javascript. В этом языке область видимость не поблочная, а функциональная. то есть все 3 функции будут видеть одно и то значение переменной i. Поскольку цикл отработает раньше, чем придут ответы от сервера, все три функции будут работать с i == 3;
Эта проблема решается стандартным способом — мы кэшируем значение переменной цикла. Но нельзя сказать, что код от этого стал красивее.

function combine(scripts, callback){
	var data [];
	for(var i =0; l = scripts.length; i< l; ++i){
		(function (i){
			jQuery.ajax({

				url: scripts[i],
				success : function(response){
					data[i] = response;
					if(data.length === scripts.length){
						callback(data.join(""));
					}
				}
			});
		}(i));
	}
}


Почти даже работает. Для того, чтобы избавиться от замыканий и хитрых переменных, можно использовать foreach

function combine(scripts, callback){
	var data [];
	scripts.forEach(function(script,i){
			jQuery.ajax({

				url: scripts[i],
				success : function(response){
					data[i] = response;
					if(data.length === scripts.length){
						callback(data.join(""));
					}
				}
			});
		});
	}
}


Лучше, конечно, но все равно страшновато. Кстати, код все еще не будет работать правильно. Его можно допилить до работоспособного состояния, но это создаст дополнительные сложности и в разработке и в последующей поддержке.

Continuation Passing Style


Для избавления от головной боли воспользуемся библиотекой

github.com/caolan/async

Для работы будем использовать такую вещь как CPS.

Звучит гораздо страшнее, чем есть на самом деле. Это функция, которая получает в качестве параметра другую функцию, и когда первая функция завершается, она вместо retrun вызывает функцию-параметр.

Обернем jQuery.ajax таким образом, чтобы получить требуемый результат.

function ajax(url, callback){
	jQuery.ajax({url: url, success: callback});
}


Функция получает в качестве параметера callback, и мы не описали обработчик ошибок. В реальном коде он обязан быть, но для простоты изложения, мы о нем забудем.
Что же будет, если использовать библиотеку async? Получится что-то типа такого:

function combine(scripts, callback){
	async.map(scripts, ajax, function(contents){
		callback(contents.join(""));
	});
}


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

Сравните с тем, что было:
function combine(scripts, callback){
	var data [];
	for(var i =0; l = scripts.length; i< l; ++i){
		(function (i){
			jQuery.ajax({

				url: scripts[i],
				success : function(response){
					data[i] = response;
					if(data.length === scripts.length){
						callback(data.join(""));
					}
				}
			});
		}(i));
	}
}


Поскольку map для меня уже является естественным способом написания программ, я бы никогда не написал кода приведенного выше. Я бы думал, как приспособить map к асинхронному окружению. И если бы не было библиотеки async, то написал бы асинхронный map сам.

Функциональный подход позволяет гораздо проще смотреть на вещи. И реализовывать более красивые решения.

Частичное применение функций



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

В качестве примера мы будем создавать DOM элементы.
(Прим. переводчика: cull.dom — библиотека, автора, которую он создавал для одного из проектов. Но функции в ней очевидны и просты.)

var ul = cull.dom.el("ul"); //document.createElement("ul")
ul.nodeType === 1 // true


Также можно задавать атрибуты свойств.

var ul = cull.dom.el("ul", {className: "bands"});


И указывать дочерние элементы

var li = cull.dom.el("li", "Tom Waits");
var ul = cull.dom.el("ul", {className: "bands"}, li);


Если их использовать друг внутри друга, можно получить некое подобие DSL для HTML.

va ul = cull.dom.el("ul", className:"bands"},
					cull.dom.el("li", "Tom Waits"));


А теперь все-таки приступим к обсуждению частичного применения функций. Помните один из первых примеров?

function makeAdder(base){
	return function(num){
		return base + num;
	}
}


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

function makeAdder(base){
		return function(num){
			return add(base, num);
	}
}


И теперь мы видим, что функция makeAdder берет функцию add и фиксирует один из ее аргументов. Вы получаете функцию сложения, в которой один из аргументов — константа

var add2 = cull.partial(add, 2);
add2(5); //7


Теперь мы получили достаточно интересную возможность — сделать наш DSL по созданию DOM элементов еще красивее.

var ul = cull.partial(cull.dom.el, "ul");
var li = cull.partial(cull.dom.el, "li");


И можем строить HTML списки приблизительно так

var list = ul({className: "bands"},
	[li("Diamanda Galas"),
	 li("Руки вверх"),
	 li("John Zorn")]);


Если вы как и я, не любите программировать на уровне строковых переменных — это отличный способ упростить себе жизнь. У вас теперь будет работать автодополнение кода, и другие приятные вещи. А еще ваш код очень похож на обычный HTML.
И поскольку наш подход достаточно красив, мы можем создать функции для всех элементов документа заранее:

["a", "br", "code", "div", ...].forEach(function(tagName){
	cull.dom.el[tagName] = cull.partial(cull.dom.el, tagName);
});


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

var e = cull.dom.el;
var list = ul({className: "bands"},
	[e.li("Pan Sonic"),
	 e.li("Веня Дркин"),
	 e.li("Muslimgauze")]);


Теперь мы не завязаны на глобальные переменные и функции, что хорошо.

Композиция функций



Вот еще пример простого приложения — опросника.


Необходимо ответить на каждый блок. Каждый блок содержит несколько вопросов. После ответа на один блок, мы переходим к следующему.

Каждый блок можно представить как панель, которая может быть в режиме вопроса, режиме результата или неактивной.



В каждой панели могут быть разные поля. Строковые, числовые, даты.
Поля могут быть в двух режимах — редактирования или результата.

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

Помните нашу любимую функцию 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;


Проблема в том, тут происходит не одна, а целых три вещи:
  1. Получение результата через getSummary()
  2. Получение свойства text
  3. Оборачивание результата в тег 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")]);


Как же нам реализовать compose?


По частям. Для начала создадим синонимы, чтобы не писать много кода.

var callGetSummary = func("getSummary");
var getText = prop("text");
var summarize = compose([p, getText, callGetSummary]);


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

Шаг первый


var callGetSummary = func("getSummary");
var getText = prop("text");
var summarize = compose([p, getText, callGetSummary]);
// summarize(obj);
//  =>			callGetSummary(obj)

Объект предеается в последнюю функцию из списка, а именно getSummary. Она возвращает нам объект типа summary. А этот объект передается в следующую функцию, getText

Шаг второй


var callGetSummary = func("getSummary");
var getText = prop("text");
var summarize = compose([p, getText, callGetSummary]);
// summarize(obj);
//  =>	getText(callGetSummary(obj))


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

Шаг третий


var callGetSummary = func("getSummary");
var getText = prop("text");
var summarize = compose([p, getText, callGetSummary]);
// summarize(obj);
//  =>	p(getText(callGetSummary(obj)))


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

Итак, вернемся к нашему многострадальному примеру.

builSummary: function() {
	var summarize = compose(
				[p, prop("text"), func("getSummary")]);
	return div(this.components.map(summarize));
}


Сначала мы создали функцию вычисления результатов. А потом применили map.
При этом заметьте, что функция summarize абсолютно не знает, с каким объектом она работает. Это три различных абстракции, которые соединяются исключительно благодаря функции compose. Поэтому мы можем вынести summarize в отдельную сущность.


var summarize = compose(
				[p, prop("text"), func("getSummary")]);
// ...
builSummary: function() {
	return div(this.components.map(summarize));
	
}


Выглядит здорово и красиво, но что насчет производительности?

Вопросы производительности



for — 5M операций в секунду
forEach — 1,5M операций в секунду
reduce — 1.5M операций в секунду

Работа с DOM — 50K операций в секунду

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

Заключение



Используем чистые функции
Используем функции высшего порядка (map, reduce).
Используем небольшие абстракции.
Много мелких абстракций могут легко собираться в одну большую мощную вещь.

P.S. Слайды оригинального выступления можно посмотреть по адресу cjohansen.no/talks/2012/javazone
P.P.S. А почему нету хаба функциональное программирование?
Метки:
Поделиться публикацией
Похожие публикации
Комментарии 54
  • +2
    А что насчет ленивых вычислений?
    • 0
      То же самое, что и выводом типов ;) Я хочу заинтересовать людей, чтобы они начали смотреть по сторонам. Ступившие на скользкую дорожку функционального программирования рано или поздно знакомятся с Haskell. И ленивостью, и pattern matching, и монадами с функторами. И другими страшными словами.
    • +3
      Классная статья и хорошие примеры!
      • +1
        Небольшое замечание: вместо того, чтобы добавлять обертку вокруг jQuery.ajax можно было бы сделать аналогичное на базе jQuery.Deferred. Собственно вызов jQuery.ajax и возвращает jQuery.Deferred. Правда тогда бы было труднее показать преимущества map/reduce :)
        • –5
          а чем не угодил такой кусок:
          for (key in arr){ return arr[key]; }
          и зачем надо было городить foreach (через численный), который в принципе реализован нативно?
          Много просто лишено смысла в реализации: все сводится к тому что «мне красиво и нравится, поэтому функционального программирования тут нет»
          • 0
            Не универсальное решение.
            В чистом JS это сработает, но некоторые фреймворки могут добавить свой функционал в Array.prototype и тогда при итерировании вы этот атрибут тоже обойдете.
            • –5
              У автора мешанина JQuery и чистого с последующим сравнением того, что сравнивать, по сути, нельзя.
              Добавление в Array.prototype атрибутов, которые можно обойти с помощью for(key in arr) — феерический бред, т.к. есть объекты, а есть массивы.
              • +2
                Не понял про бред, поясните.

                Array.prototype.newProp = "newprop"
                var b = new Array();
                b.push(0);
                b.push(1);
                for (var i in b) console.log(b[i]);
                

                Выводит

                0
                1
                newprop
                • +1
                  Я про то, что надобность данных структур по-моему сильно преувеличена.
                  Массив данных должен являться массивом данных, а не объектом хранения дополнительных структур, функций и т.д., т.к. для этого есть объекты.
                  • +1
                    Если использовать, например, тот же es5-shim, то в старых браузерах код будет работать некорректно
                    • +1
                      Возможно я не прав.
                    • +2
                      Это в идеале.
                      А реальности можно словить че нить интересное.
                      Например в старых версия IE (не знаю как сейчас), у массива не было функции indexOf, и некоторые для исправления этой проблемы поступали очень просто:

                      if (!Array.prototype.indexOf) {
                        Array.prototype.indexOf = function(item) {
                          ...
                        }
                      }
                      
              • +1
                Вероятно этим и не угодил:
                var a = [1, 2], i;
                Array.prototype.someFunc = function() {};
                for (i in a) {
                    console.log(i, ' > ' , a[i]);
                }
                


                P.S. То есть надо деллать дополнительную проверну на a.hasOwnProperty(i)
                P.P.S. Насколько я помню, конструкция с in не гарантирует верную последовательность перебора. Она может быть такой исключительно по совпадению.
              • 0
                Спасибо большое за статью!
                • +4
                  for(var i =0; l< arr.length; i<l; ++i){
                      console.log(arr[i]);
                  }
                  


                  А не так должно быть?

                  for(var i=0, l = arr.length; i<l; ++i){
                      console.log(arr[i]);
                  }
                  


                  И так в нескольких местах оператору for 4 терма передается.
                  • +1
                    По поводу ФП на javascript — все хорошо, но количество слов function иногда задалбывает

                    Лично мне приятнее видеть, когда асинхронные запросы выполняются в виде. Не знаю, как остальным, так скобок нет грядки в конце выражения

                    SomeAsyncronousController.SomeMethodAsync(args).then(function(callback) { });
                    
                    • 0
                      Смотрите на CoffeeScript
                    • 0
                      Думаю, стоит упомянуть, что ни map, ни reduce, ни forEach не работают в IE < 9.
                      • 0
                        Есть куча библиотек, которые либо расширяют стандартные объекты, либо вводят подобный функционал через внешние объекты, например, underscore.js
                        • 0
                          Я в курсе.
                          • –1
                            SugarJS тут больше подходит. underscore.js немного портит выразительность.
                            • –2
                              А Sugar портит методами, не описанными в специффикации.
                          • +3
                            Да, не работают, но чукча не писатель, чукча переводчик :)

                            Внизу кстати правильно подсказали про underscore.js.
                            • +2
                              Или es5-shim для совместимости.
                          • 0
                            Очень интересно, однако я смог осилить ровно половину. Через пол годика приду дочитывать.
                            • 0
                              Это перевод 45-минутной презентации. Полгодика на один школьный урок? Я думаю вы лукавите )
                              • 0
                                Возможно. В новом для меня мире программирования время течет абсолютно непредсказуемо.
                            • 0
                              В JavaScript для функционального программирования важно понимать несколько ключевых особенностей языка:
                              1. функции — вызываемые объекты (их можно возвращать и передавать)
                              2. функция не проверяет тип и кол-во аргументов в нее переданных (имена аргументов формальны)
                              3. каждая функция образует лексическое замыкание (и это замыкание путешествует с ней)
                              4. this и прототипы

                              Дальше все прочее можно написать уже исходя из потребностей. Можно даже таких слов как «Функции высшего порядка» и «Частичное применение функций» не знать :)
                              • 0
                                Да, эти нюансы очень важны, но их редко где можно встретить описанными вместе. Да еще и в контексте функционального программирования. На ум приходит только Secrets of Javascript Ninja.

                              • 0
                                По поводу производительности функций:
                                — Браузеры оптимизируют итераторы (сворачивают функции) так что ваша функция формально может и не вызываться (если вы, конечно, не делаете нечто извращенное и оптимизация не проходит). Так что пишите .map() .forEach() для холодных и теплых циклов — берегите глаза от for ;-)
                                — Наследование через замыкания дороже прототипного наследования (Scope функции может создаваться каждый раз при вызове функции). Опять же это заметно только в очень горячих функциях.
                                • 0
                                  Что есть наследование через замыкания?
                                  • +1
                                    Ох я уж с этим наследованием наговорил чего-попало. Хотел сказать конструкторы через фактори дороже обычных через new (но не фатально).
                                    var createSmth = function (myPrivate) {
                                        myPrivate = myPrivate || 0;
                                    
                                        return {
                                            setMyPrivate: function (val) {
                                                myPrivate = b;
                                            } 
                                        };
                                    };
                                    
                                    // или что-то такое
                                    
                                    function makeF() {
                                      var x = 11;
                                      var y = 22;
                                      return function (what) {
                                        switch (what) {
                                          case "x": return x;
                                          case "y": return y;
                                        }
                                      }
                                    }
                                    
                                    var f = makeF();
                                    f("x");
                                    

                                    В общем всякие фактори функций и куча вложенных функций это дороже обычного new и нескольких неизменных функций.
                                • 0
                                  А это:
                                  var totalLength = buffers.
                                      map(function (buffer) {return buffer.length; }).
                                      reduce(function(sum, curr){return sum+curr;}, 0);
                                  

                                  Разве нельзя заменить на:
                                  var totalLength = buffers.
                                      reduce(function(sum, buffer){return sum + buffer.length;}, 0);
                                  
                                  ?
                                  • 0
                                    Конечно можно. Но чем проще каждый шаг, тем проще понимать, что происходит. Особенно для тех, кто не сильно знаком с reduce.

                                    Можно ведь и
                                    function combine(scripts, callback){
                                        async.map(scripts, ajax, function(contents){
                                            callback(contents.join(""));
                                        });
                                    }
                                    


                                    заменить на

                                    function combine(scripts, callback){
                                        async.map(scripts, ajax, function(contents){
                                            callback(contents.reduce(function(result, current) { return result + current},""));
                                        });
                                    }
                                    


                                    Чтобы совсем функцоинально. Но будет ли понятнее? :)
                                    • 0
                                      Ваш пример вообще не коррелирует с тем, что я предложил.
                                      Вы предлагаете расписать один шаг так, как он там внутри представляется, я же предложил два шага в один записать.
                                      • +1
                                        Разделение на map и reduce было сделано специально. Давайте представим, что buffer.length не просто buffer.length, а какое-то тяжелое вычисление. Которое надо раскинуть на кластер машин для ускорения.

                                        В случае с явно выделенным map это будет просто и писать и читать. Если же все запихнуть в один reduce- это будет сложнее для восприятия.

                                        Композиция функций поэтому и получилась такой мощной, что сначала мы разделили все на элементарные блоки. Их проще писать, их проще понимать, их проще тестировать.
                                    • 0
                                      не туда( написал комент
                                  • 0
                                    >>Под циклом будем понимать стандартное лобовое решение. Примерно такое
                                    >>for(var i =0; i
                                    • 0
                                      Парсер пожрал.
                                      >>Под циклом будем понимать стандартное лобовое решение. Примерно такое
                                      >>for(var i = 0; i < n; ++1){

                                      В очередной раз убедился, что на js может писать только «исполин духа и корифей».
                                      • 0
                                        Что не так?
                                        • 0
                                          Ах да. Плохо, что нельзя удалять комментарии.
                                    • +2
                                      Для полного погружения в функциональное программирование на JavaScript:


                                      И еще один интересный пост про оптимизация хвостовой рекурсии: Tail call optimization on it’s way… Who gives a frak?!
                                      • –4
                                        7uiip[poop
                                        'p[;
                                        yiio
                                        gthhhjkipk kooo,l,k,p'\
                                      • +1
                                        Спешу заметить, что пост начинается в ошибочного примера (с функцией add). Говорится, что «Можно тот же код записать по-другому.». На самом деле эти формы записи имеют достаточно серьезные отличия, и, вообще говоря, работают по-разному. Объяснение которых уходит корнями спецификацию языка. О этих отличиях, кстати, любят спрашивать на собеседованиях. Кому интересно разобраться в данном вопросе, рекомендую почитать про Function Statement и Function Definition, например у Дмитрия Сошникова: dmitrysoshnikov.com/ecmascript/ru-chapter-5-functions/

                                        • 0
                                          > var str = «Mentioned by » + tweeps.map(prop («name»)).join(", ");

                                          Есть очень интересная вещь под названием «вызов по имени» (в RightJS, кстати, используется), позволяет заменить .map(prop(«name»)) на .map(«name»).
                                          • 0
                                            Собственно, реализацию этой возможности можно уместить ровно в одну строку.
                                            Главное понимать, когда строка это свойство, когда — название функции, а когда — просто строка.
                                          • –1
                                            Писал библиотеку для glomper.com реализующую функциональный подход при разработке «богатых приложений» на js github.com/freeart/brisk
                                            • 0
                                              > С чем тут могут быть проблемы? С областями видимости Javascript. В этом языке область видимость не поблочная, а функциональная. то есть все 3 функции будут видеть одно и то значение переменной i. Поскольку цикл отработает раньше, чем придут ответы от сервера, все три функции будут работать с i == 3;
                                              Просто замените var на let.
                                              • 0
                                                va ul = cull.dom.el("ul", className:"bands"},
                                                                    cull.dom.el("li", "Tom Waits"));
                                                

                                                очепятка, нету {
                                                • 0
                                                  Статья настолько простая, что уже сложная.

                                                  «Порадовала» масса опечаток.
                                                  • 0
                                                    Неплохо бы что-то сделать с ясностью формулировок:

                                                    Одним из больших плюсов Javascript является то, что функции в нем являются полноценными объектами. Настоящие First Class Citizen.
                                                    В отличие, например от Java, где функция отдельно от объекта существовать не может.

                                                    По факту сказано — в JS функция является объектом, (то есть фактически, отдельно от объекта существовать не может), в отличие от Java где отдельно от объекта существовать не может...
                                                    • 0
                                                      Хотелось бы обратить внимание, что замена

                                                      for (var i =1; l = arr.length; i< l; ++i){
                                                      }

                                                      на foreach.

                                                      далеко не эквивалентна в общем случае. Стандарт языка не гарантирует что порядок вывода будет совпадать в обоих конструкциях, более того в разных реализациях (разных браузерах) присутствуют собственные эффекты.
                                                      • 0
                                                        Ну тут надо понимать, что пытаемся сделать

                                                        Я бы и foreach не использовал, а брал бы map/fold функции или их аналоги.

                                                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.