Автор материала, перевод которого мы публикуем сегодня, говорит, что он, после того, как долго занимался объектно-ориентированным программированием, задумался о сложности систем. По словам Джона Оустерхаута, сложность (complexity) — это всё, что делает тяжелее понимание или модификацию программного обеспечения. Автор этой статьи, выполнив некоторые изыскания, обнаружил концепции функционального программирования наподобие иммутабельности и чистых функций. Применение таких концепций позволяет создавать функции, не имеющие побочных эффектов. Использование этих функций упрощает поддержку систем и даёт программисту некоторые другие преимущества.
Здесь мы поговорим о функциональном программировании и о некоторых его важных принципах. Всё это будет проиллюстрировано множеством примеров кода на JavaScript.
radius
), после чего возвращала бы значение вычисления выражения radius * radius * PI
:const PI = 3.14;
function calculateArea(radius) {
return radius * radius * PI;
}
calculateArea(10); // возвращает 314
PI
должно являться число 42
, из-за чего было изменено значение этой константы.10
, вернёт значение 10 * 10 * 42 = 4200
. Получается, что использование здесь такого же, как в прошлом примере, значения параметра radius
, приводит к возврату функцией другого результата. Исправим это:const PI = 3.14;
function calculateArea(radius, pi) {
return radius * radius * pi;
}
calculateArea(10, PI); // возвращает 314
pi
. Как результат, функция будет работать только с тем, что передано ей при вызове, не обращаясь к глобальным сущностям. Если проанализировать поведение этой функции, то можно прийти к следующим выводам:radius
, равный 10
, и аргумент pi
, равный 3.14
, она всегда будет возвращать один и тот же результат — 314
.radius
, равным 10
и аргументом pi
, равным 42
, она всегда будет возвращать 4200
.function charactersCounter(text) {
return `Character count: ${text.length}`;
}
function analyzeFile(filename) {
let fileContent = open(filename);
return charactersCounter(fileContent);
}
function yearEndEvaluation() {
if (Math.random() > 0.5) {
return "You get a raise!";
} else {
return "Better luck next year!";
}
}
let counter = 1;
function increaseCounter(value) {
counter = value + 1;
}
increaseCounter(counter);
console.log(counter); // 2
counter
. Наша функция, не являющаяся чистой, получает это значение в виде аргумента и перезаписывает его, добавляя к его прежнему значению единицу.increaseCounter()
чистой? На самом деле, это очень просто:let counter = 1;
function increaseCounter(value) {
return value + 1;
}
increaseCounter(counter); // 2
console.log(counter); // 1
2
, но при этом значение глобальной переменной counter
не меняется. Тут можно сделать вывод о том, что функция возвращает переданное ей значение, увеличенное на 1
, при этом ничего не изменяя.let list = [1, 2, 3, 4, 5];
function incrementNumbers(list) {
return list.map(number => number + 1);
}
map()
, который позволяет модифицировать каждый элемент массива и формирует новый массив, возвращаемый функцией. Вызовем функцию, передав ей массив list
:incrementNumbers(list); // возвращает [2, 3, 4, 5, 6]
[1, 2, 3, 4, 5]
, она возвратит новый массив [2, 3, 4, 5, 6]
. Именно так она и работает.for
. В ходе его работы, как показано ниже, применяются мутабельные переменные:var values = [1, 2, 3, 4, 5];
var sumOfValues = 0;
for (var i = 0; i < values.length; i++) {
sumOfValues += values[i];
}
sumOfValues // 15
i
и значение глобальной переменной (её можно считать состоянием программы) sumOfValues
. Как в подобной ситуации поддерживать неизменность сущностей? Ответ лежит в использовании рекурсии.let list = [1, 2, 3, 4, 5];
let accumulator = 0;
function sum(list, accumulator) {
if (list.length == 0) {
return accumulator;
}
return sum(list.slice(1), accumulator + list[0]);
}
sum(list, accumulator); // 15
list; // [1, 2, 3, 4, 5]
accumulator; // 0
sum()
, которая принимает массив чисел. Эта функция вызывает сама себя до тех пор, пока массив не опустеет (это базовый случай нашего рекурсивного алгоритма). На каждой такой «итерации» мы добавляем значение одного из элементов массива к параметру функции accumulator
, не затрагивая при этом глобальной переменной accumulator
. При этом глобальные переменные list
и accumulator
остаются неизменными, до и после вызова функции в них хранятся одни и те же значения.reduce
. Об этом мы поговорим ниже.UrlSlugify
, после чего создадим метод этого класса slugify!
, который используется для преобразования строки.class UrlSlugify
attr_reader :text
def initialize(text)
@text = text
end
def slugify!
text.downcase!
text.strip!
text.gsub!(' ', '-')
end
end
UrlSlugify.new(' I will be a url slug ').slugify! # "i-will-be-a-url-slug"
let string = " I will be a url slug ";
function slugify(string) {
return string.toLowerCase()
.trim()
.split(" ")
.join("-");
}
slugify(string); // i-will-be-a-url-slug
toLowerCase
: преобразует символы строки к нижнему регистру.trim
: убирает пробельные символы из начала и конца строки.split
: разбивает строку на части, помещая слова, разделённые пробелами, в массив.join
: формирует на основе массива со словами строку, слова в которой разделены тире.square()
, возвращающую результат умножения числа на это же число:function square(n) {
return n * n;
}
square(2); // 4
square(2); // 4
square(2); // 4
// ...
2
, эта функция всегда будет возвращать число 4
. В результате оказывается, что вызов вида square(2)
можно заменить числом 4
. Это означает, что наша функция обладает свойством ссылочной прозрачности.function sum(a, b) {
return a + b;
}
sum(3, sum(5, 8));
sum(5, 8)
всегда даёт 13
. Поэтому вышеприведённый вызов можно переписать так:sum(3, 13);
16
. Как результат, его можно заменить числовой константой и мемоизировать его.2
и возвращает то, что у неё получилось:function doubleSum(a, b) {
return (a + b) * 2;
}
2
, и возвращает вычисленное значение:function doubleSubtraction(a, b) {
return (a - b) * 2;
}
function sum(a, b) {
return a + b;
}
function subtraction(a, b) {
return a - b;
}
function doubleOperator(f, a, b) {
return f(a, b) * 2;
}
doubleOperator(sum, 3, 1); // 8
doubleOperator(subtraction, 3, 1); // 4
doubleOperator()
имеется параметр f
, а функция, которую он представляет, используется для обработки параметров a
и b
. Функции sum()
и substraction()
, передаваемые функции doubleOperator()
, фактически, позволяют управлять поведением функции doubleOperator()
, меняя его в соответствии с реализованной в них логикой.filter()
, map()
и reduce()
. Поговорим о них.filter()
ожидает получить какой-то критерий оценки элементов, на основе которого она и определяет, нужно или не нужно включать некий элемент в результирующую коллекцию. Этот критерий задаёт передаваемая ей функция, которая возвращает true
в том случае, если функция filter()
должна включить элемент в итоговую коллекцию, а в противном случае возвращает false
.evenNumbers
).numbers
).numbers
, в массив evenNumbers
.var numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
var evenNumbers = [];
for (var i = 0; i < numbers.length; i++) {
if (numbers[i] % 2 == 0) {
evenNumbers.push(numbers[i]);
}
}
console.log(evenNumbers); // (6) [0, 2, 4, 6, 8, 10]
even()
), которая, если число является чётным, возвращает true
, а если нечётным — false
, после чего передать её методу массива filter()
, который, проверив с её помощью каждый элемент массива, сформирует новый массив, содержащий лишь чётные числа:function even(number) {
return number % 2 == 0;
}
let listOfNumbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
listOfNumbers.filter(even); // [0, 2, 4, 6, 8, 10]
x
. var filterArray = function(x, coll) {
var resultArray = [];
for (var i = 0; i < coll.length; i++) {
if (coll[i] < x) {
resultArray.push(coll[i]);
}
}
return resultArray;
}
console.log(filterArray(3, [10, 9, 8, 2, 7, 5, 1, 3, 0])); // (3) [2, 1, 0]
x
и помещение этого элемента в массив resultArray
в том случае, если он проходит проверку.filter()
, являющегося функцией высшего порядка? Например, это может выглядеть так:function smaller(number) {
return number < this;
}
function filterArray(x, listOfNumbers) {
return listOfNumbers.filter(smaller, x);
}
let numbers = [10, 9, 8, 2, 7, 5, 1, 3, 0];
filterArray(3, numbers); // [2, 1, 0]
this
в функции smaller()
, но ничего сложного тут нет. Ключевое слово this
представляет собой второй аргумент метода filter()
. В нашем примере это — число 3
, представленное параметром x
функции filterArray()
. На это число и указывает this
.name
, и сведения о возрасте этих людей, представленные свойством age
. Вот как выглядит такой массив:let people = [
{ name: "TK", age: 26 },
{ name: "Kaio", age: 10 },
{ name: "Kazumi", age: 30 }
];
21
год. Вот как можно решить эту задачу:function olderThan21(person) {
return person.age > 21;
}
function overAge(people) {
return people.filter(olderThan21);
}
overAge(people); // [{ name: 'TK', age: 26 }, { name: 'Kazumi', age: 30 }]
olderThan21()
. В данном случае мы, при проверке, обращаемся к свойству age
каждого элемента, проверяя, превышает ли значение этого свойства 21
. Данную функцию мы передаём методу filter()
, который и фильтрует массив.map()
используется для преобразования элементов массивов. Он применяет к каждому элементу массива переданную ему функцию, после чего строит новый массив, состоящий из изменённых элементов.people
. Теперь мы не собираемся фильтровать этот массив, основываясь на свойстве объектов age
. Нам нужно сформировать на его основе список строк вида TK is 26 years old
. Строки, в которые превращаются элементы, при таком подходе будут строиться по шаблону p.name is p.age years old
, где p.name
и p.age
— это значения соответствующих свойств элементов массива people
.var people = [
{ name: "TK", age: 26 },
{ name: "Kaio", age: 10 },
{ name: "Kazumi", age: 30 }
];
var peopleSentences = [];
for (var i = 0; i < people.length; i++) {
var sentence = people[i].name + " is " + people[i].age + " years old";
peopleSentences.push(sentence);
}
console.log(peopleSentences); // ['TK is 26 years old', 'Kaio is 10 years old', 'Kazumi is 30 years old']
function makeSentence(person) {
return `${person.name} is ${person.age} years old`;
}
function peopleSentences(people) {
return people.map(makeSentence);
}
peopleSentences(people); // ['TK is 26 years old', 'Kaio is 10 years old', 'Kazumi is 30 years old']
[1, 2, 3, -4, 5]
он приобретёт вид [1, 2, 3, 4, 5]
так как абсолютное значение -4
равняется 4
.var values = [1, 2, 3, -4, 5];
for (var i = 0; i < values.length; i++) {
values[i] = Math.abs(values[i]);
}
console.log(values); // [1, 2, 3, 4, 5]
Math.abs()
, изменённые элементы записываются туда же, где они были до преобразования.map()
. Почему бы не воспользоваться им?abs()
, взглянуть на то, как он обрабатывает разные числа.Math.abs(-1); // 1
Math.abs(1); // 1
Math.abs(-2); // 2
Math.abs(2); // 2
Math.abs()
в качестве аргумента метода массива map()
. Помните о том, что функции высшего порядка могут принимать другие функции и использовать их? Метод map()
является именно такой функцией. Вот как решение нашей задачи будет выглядеть теперь:let values = [1, 2, 3, -4, 5];
function updateListMap(values) {
return values.map(Math.abs);
}
updateListMap(values); // [1, 2, 3, 4, 5]
reduce()
положена идея преобразования массива к единственному значению путём комбинации его элементов с использованием некоей функции.Product 1
, Product 2
, Product 3
и Product 4
. После этого нам надо найти общую стоимость этих товаров.var orders = [
{ productTitle: "Product 1", amount: 10 },
{ productTitle: "Product 2", amount: 30 },
{ productTitle: "Product 3", amount: 20 },
{ productTitle: "Product 4", amount: 60 }
];
var totalAmount = 0;
for (var i = 0; i < orders.length; i++) {
totalAmount += orders[i].amount;
}
console.log(totalAmount); // 120
reduce()
, то мы можем создать функцию (sumAmount()
), используемую для вычисления суммы элементов массива, после чего передать её методу reduce()
:let shoppingCart = [
{ productTitle: "Product 1", amount: 10 },
{ productTitle: "Product 2", amount: 30 },
{ productTitle: "Product 3", amount: 20 },
{ productTitle: "Product 4", amount: 60 }
];
const sumAmount = (currentTotalAmount, order) => currentTotalAmount + order.amount;
function getTotalAmount(shoppingCart) {
return shoppingCart.reduce(sumAmount, 0);
}
getTotalAmount(shoppingCart); // 120
shoppingCart
, представляющий собой корзину покупателя, функция sumAmount()
, которая принимает элементы массива (объекты order
, при этом нас интересуют их свойства amount
), и текущее вычисленное значение суммы их стоимостей — currentTotalAmount
.reduce()
, выполняемого в функции getTotalAmount()
, ему передаётся функция sumAmount()
и начальное значение счётчика, которое равняется 0
.map()
и reduce()
. Что имеется в виду под их «комбинацией»? Дело тут в том, что мы можем использовать метод map()
для преобразования массива shoppingCart
в массив, содержащий лишь значения свойств amount
хранящихся в этом массиве объектов, а затем воспользоваться методом reduce()
и функцией sumAmount()
. Вот как это выглядит:const getAmount = (order) => order.amount;
const sumAmount = (acc, amount) => acc + amount;
function getTotalAmount(shoppingCart) {
return shoppingCart
.map(getAmount)
.reduce(sumAmount, 0);
}
getTotalAmount(shoppingCart); // 120
getAmount()
принимает объект и возвращает только его свойство amount
. После обработки массива с использованием метода map()
, которому передана эта функция, получается новый массив, который выглядит как [10, 30, 20, 60]
. Затем, с помощью reduce()
, мы находим сумму элементов этого массива.filter()
, map()
и reduce()
. Теперь, на простом примере, рассмотрим использование всех трёх этих функций.let shoppingCart = [
{ productTitle: "Functional Programming", type: "books", amount: 10 },
{ productTitle: "Kindle", type: "eletronics", amount: 30 },
{ productTitle: "Shoes", type: "fashion", amount: 20 },
{ productTitle: "Clean Code", type: "books", amount: 60 }
]
type
его элементов, учитывая то, что нас интересует значение этого свойства books
.let shoppingCart = [
{ productTitle: "Functional Programming", type: "books", amount: 10 },
{ productTitle: "Kindle", type: "eletronics", amount: 30 },
{ productTitle: "Shoes", type: "fashion", amount: 20 },
{ productTitle: "Clean Code", type: "books", amount: 60 }
]
const byBooks = (order) => order.type == "books";
const getAmount = (order) => order.amount;
const sumAmount = (acc, amount) => acc + amount;
function getTotalAmount(shoppingCart) {
return shoppingCart
.filter(byBooks)
.map(getAmount)
.reduce(sumAmount, 0);
}
getTotalAmount(shoppingCart); // 70
К сожалению, не доступен сервер mySQL