Master the JavaScript Interview: What is a Pure Function?
![]()
Pure functions are essential for a variety of purposes, including functional programming, reliable concurrency, and React+Redux apps. But what does “pure function” mean?
A pure function is a function which:
- Given the same input, always returns the same output.
- Produces no side effects.
We’re going to answer this question with a free lesson from “Learn JavaScript with Eric Elliott”:
Before we can tackle what a pure function is, it’s probably a good idea to take a closer look at functions. There may be a different way to look at them that will make functional programming easier to understand.
What is a Function?
A function is a process which takes some input, called arguments, and produces some output called a return value. Functions may serve the following purposes:
- Mapping: Produce some output based on given inputs. A function maps input values to output values.
- Procedures: A function may be called to perform a sequence of steps. The sequence is known as a procedure, and programming in this style is known as procedural programming.
- I/O: Some functions exist to communicate with other parts of the system, such as the screen, storage, system logs, or network.
Mapping
Pure functions are all about mapping. Functions map input arguments to return values, meaning that for each set of inputs, there exists an output. A function will take the inputs and return the corresponding output.
`Math.max()` takes numbers as arguments and returns the largest number:
In this example, 2, 8, & 5 are arguments. They’re values passed into the function.
`Math.max()` is a function that takes any number of arguments and returns the largest argument value. In this case, the largest number we passed in was 8, and that’s the number that got returned.
Functions are really important in computing and math. They help us process data in useful ways. Good programmers give functions descriptive names so that when we see the code, we can see the function names and understand what the function does.
Math has functions, too, and they work a lot like functions in JavaScript. You’ve probably seen functions in algebra. They look something like this:
Which means that we’re declaring a function called f and it takes an argument called x and multiplies x by 2.
To use this function, we simply provide a value for x:
In algebra, this means exactly the same thing as writing:
So any place you see f(2) you can substitute 4.
Now let’s convert that function to JavaScript:
You can examine the function’s output using `console.log()`:
Remember when I said that in math functions, you could replace `f(2)` with `4`? In this case, the JavaScript engine replaces `double(5)` with the answer, `10`.
Pure Functions
A pure function is a function which:
- Given the same input, will always return the same output.
- Produces no side effects.
So, `console.log( double(5) );` is the same as `console.log(10);`
This is true because `double()` is a pure function, but if `double()` had side-effects, such as saving the value to disk or logging to the console, you couldn’t simply replace `double(5)` with 10 without changing the meaning.
If you want referential transparency, you need to use pure functions.
A dead giveaway that a function is impure is if it makes sense to call it without using its return value. For pure functions, that’s a noop.
I recommend that you favor pure functions. Meaning, if it is practical to implement a program requirement using pure functions, you should use them over other options. Pure functions take some input and return some output based on that input. They are the simplest reusable building blocks of code in a program. Perhaps the most important design principle in computer science is KISS (Keep It Simple, Stupid). I prefer Keep It Stupid Simple. Pure functions are stupid simple in the best possible way.
Pure functions have many beneficial properties, and form the foundation of functional programming. Pure functions are completely independent of outside state, and as such, they are immune to entire classes of bugs that have to do with shared mutable state. Their independent nature also makes them great candidates for parallel processing across many CPUs, and across entire distributed computing clusters, which makes them essential for many types of scientific and resource-intensive computing tasks.
Pure functions are also extremely independent — easy to move around, refactor, and reorganize in your code, making your programs more flexible and adaptable to future changes.
The Trouble with Shared State
Several years ago I was working on an app that allowed users to search a database for musical artists and load the artist’s music playlist into a web player. This was around the time Google Instant landed, which displays instant search results as you type your search query. AJAX-powered autocomplete was suddenly all the rage.
The only problem was that users often type faster than an API autocomplete search response can be returned, which caused some strange bugs. It would trigger race conditions, where newer suggestions would be replaced by outdated suggestions.
Why did that happen? Because each AJAX success handler was given access to directly update the suggestion list that was displayed to users. The slowest AJAX request would always win the user’s attention by blindly replacing results, even when those replaced results may have been newer.
To fix the problem, I created a suggestion manager — a single source of truth to manage the state of the query suggestions. It was aware of a currently pending AJAX request, and when the user typed something new, the pending AJAX request would be canceled before a new request was issued, so only a single response handler at a time would ever be able to trigger a UI state update.
Any sort of asynchronous operation or concurrency could cause similar race conditions. Race conditions happen if output is dependent on the sequence of uncontrollable events (such as network, device latency, user input, randomness, etc…). In fact, if you’re using shared state and that state is reliant on sequences which vary depending on indeterministic factors, for all intents and purposes, the output is impossible to predict, and that means it’s impossible to properly test or fully understand. As Martin Odersky (creator of Scala) puts it:
non-determinism = parallel processing + mutable state
Program determinism is usually a desirable property in computing. Maybe you think you’re OK because JS runs in a single thread, and as such, is immune to parallel processing concerns, but as the AJAX example demonstrates, a single threaded JS engine does not imply that there is no concurrency. On the contrary, there are many sources of concurrency in JavaScript. API I/O, event listeners, web workers, iframes, and timeouts can all introduce indeterminism into your program. Combine that with shared state, and you’ve got a recipe for bugs.
Pure functions can help you avoid those kinds of bugs.
Given the Same Input, Always Return the Same Output
With our `double()` function, you can replace the function call with the result, and the program will mean the same thing — `double(5)` will always mean the same thing as `10` in your program, regardless of context, no matter how many times you call it or when.
But you can’t say the same thing about all functions. Some functions rely on information other than the arguments you pass in to produce results.
Consider this example:
Even though we didn’t pass any arguments into any of the function calls, they all produced different output, meaning that `Math.random()` is not pure.
`Math.random()` produces a new random number between 0 and 1 every time you run it, so clearly you couldn’t just replace it with 0.4011148700956255 without changing the meaning of the program.
That would produce the same result every time. When we ask the computer for a random number, it usually means that we want a different result than we got the last time. What’s the point of a pair of dice with the same numbers printed on every side?
Sometimes we have to ask the computer for the current time. We won’t go into the details of how the time functions work. For now, just copy this code:
What would happen if you replaced the `time()` function call with the current time?
It would always say it’s the same time: the time that the function call got replaced. In other words, it could only produce the correct output once per day, and only if you ran the program at the exact moment that the function got replaced.
So clearly, `time()` isn’t like our `double()` function.
A function is only pure if, given the same input, it will always produce the same output. You may remember this rule from algebra class: the same input values will always map to the same output value. However, many input values may map to the same output value. For example, the following function is pure:
The same input values will always map to the same output value:
Many input values may map to the same output value:
A pure function must not rely on any external mutable state, because it would no longer be deterministic or referentially transparent.
Pure Functions Produce No Side Effects
A pure function produces no side effects, which means that it can’t alter any external state.
Immutability
JavaScript’s object arguments are references, which means that if a function were to mutate a property on an object or array parameter, that would mutate state that is accessible outside the function. Pure functions must not mutate external state.
Consider this mutating, impure `addToCart()` function:
It works by passing in a cart, and item to add to that cart, and an item quantity. The function then returns the same cart, with the item added to it.
The problem with this is that we’ve just mutated some shared state. Other functions may be relying on that cart object state to be what it was before the function was called, and now that we’ve mutated that shared state, we have to worry about what impact it will have on the program logic if we change the order in which functions have been called. Refactoring the code could result in bugs popping up, which could screw up orders, and result in unhappy customers.
Now consider this version:
In this example, we have an array nested in an object, which is why I reached for a deep clone. This is more complex state than you’ll typically be dealing with. For most things, you can break it down into smaller chunks.
For example, Redux lets you compose reducers rather than deal with the entire app state inside each reducer. The result is that you don’t have to create a deep clone of the entire app state every time you want to update just a small part of it. Instead, you can use non-destructive array methods, or `Object.assign()` to update a small part of the app state.
Your turn. Fork this pen and change the impure functions into pure functions. Make the unit tests pass without changing the tests.
Принципы функционального программирования в JavaScript
Автор материала, перевод которого мы публикуем сегодня, говорит, что он, после того, как долго занимался объектно-ориентированным программированием, задумался о сложности систем. По словам Джона Оустерхаута, сложность (complexity) — это всё, что делает тяжелее понимание или модификацию программного обеспечения. Автор этой статьи, выполнив некоторые изыскания, обнаружил концепции функционального программирования наподобие иммутабельности и чистых функций. Применение таких концепций позволяет создавать функции, не имеющие побочных эффектов. Использование этих функций упрощает поддержку систем и даёт программисту некоторые другие преимущества.

Здесь мы поговорим о функциональном программировании и о некоторых его важных принципах. Всё это будет проиллюстрировано множеством примеров кода на JavaScript.
Что такое функциональное программирование?
О том, что такое функциональное программирование, можно почитать в Википедии. А именно, речь там идёт о том, что функциональное программирование — это парадигма программирования, в которой процесс вычисления трактуется как вычисление значений функций в математическом понимании последних. Функциональное программирование предполагает обходиться вычислением результатов функций от исходных данных и результатов других функций, и не предполагает явного хранения состояния программы. Соответственно, не предполагает оно и изменяемости этого состояния.
Сейчас мы, на примерах, разберём некоторые идеи функционального программирования.
Чистые функции
Чистые функции — это первая фундаментальная концепция, которую нужно изучить для того, чтобы понять сущность функционального программирования.
Что такое «чистая функция»? Что делает функцию «чистой»? Чистая функция должна отвечать следующим требованиям:
- Она всегда возвращает, при передаче ей одних и тех же аргументов, один и тот же результат (такие функции также называют детерминированными).
- Такая функция не обладает побочными эффектами.
▍Аргументы функций и возвращаемые ими значения
Представим себе, что нам надо создать функцию, которая вычисляет площадь круга. Функция, которая не является чистой, принимала бы, в качестве параметра, радиус круга ( radius ), после чего возвращала бы значение вычисления выражения radius * radius * PI :
Почему эту функцию нельзя назвать чистой? Дело в том, что она использует глобальную константу, которая не передаётся ей в качестве аргумента.
Теперь представьте себе, что некие математики пришли к выводу о том, что значением константы PI должно являться число 42 , из-за чего было изменено значение этой константы.
Теперь функция, не являющаяся чистой, при передаче ей того же входного значения, числа 10 , вернёт значение 10 * 10 * 42 = 4200 . Получается, что использование здесь такого же, как в прошлом примере, значения параметра radius , приводит к возврату функцией другого результата. Исправим это:
Теперь мы, вызывая эту функцию, всегда будем передавать ей аргумент pi . Как результат, функция будет работать только с тем, что передано ей при вызове, не обращаясь к глобальным сущностям. Если проанализировать поведение этой функции, то можно прийти к следующим выводам:
- Если функции передают аргумент radius , равный 10 , и аргумент pi , равный 3.14 , она всегда будет возвращать один и тот же результат — 314 .
- При вызове её с аргументом radius , равным 10 и аргументом pi , равным 42 , она всегда будет возвращать 4200 .
Чтение файлов
Если наша функция выполняет чтение файлов, то чистой она не будет. Дело в том, что содержимое файлов может меняться.
Генерирование случайных чисел
Любая функция, которая полагается на генератор случайных чисел, не может быть чистой.
Теперь поговорим о побочных эффектах.
▍Побочные эффекты
Примером побочного эффекта, который может проявиться при вызове функции, является модификация глобальных переменных или аргументов, передаваемых функциям по ссылке.
Предположим, нам нужно создать функцию, которая принимает целое число и выполняет увеличение этого числа на 1. Вот как может выглядеть реализация подобной идеи:
Тут имеется глобальная переменная counter . Наша функция, не являющаяся чистой, получает это значение в виде аргумента и перезаписывает его, добавляя к его прежнему значению единицу.
Глобальная переменная меняется, подобное в функциональном программировании не приветствуется.
В нашем случае модификации подвергается значение глобальной переменной. Как в этих условиях сделать функцию increaseCounter() чистой? На самом деле, это очень просто:
Как видите, функция возвращает 2 , но при этом значение глобальной переменной counter не меняется. Тут можно сделать вывод о том, что функция возвращает переданное ей значение, увеличенное на 1 , при этом ничего не изменяя.
Если следовать двум вышеописанным правилам написания чистых функций, это приведёт к тому, что в программах, созданных с использованием таких функций, будет легче ориентироваться. Окажется, что каждая функция будет изолирована и не будет оказывать воздействия на внешние по отношению к ней части программы.
Чистые функции стабильны, единообразны и предсказуемы. Получая одни и те же входные данные, такие функции всегда возвращают один и тот же результат. Это избавляет программиста от попыток учесть возможность возникновения ситуаций, в которых передача функции одних и тех же параметров приводит к разным результатам, так как подобное при использовании чистых функций просто невозможно.
▍Сильные стороны чистых функций
Среди сильных сторон чистых функций можно отметить тот факт, что код, написанный с их использованием, легче тестировать. В частности, не нужно создавать неких объектов-заглушек. Это позволяет выполнять модульное тестирование чистых функций в различных контекстах:
- Если функции передаётся параметр A — ожидается возврат значения B.
- Если функции передаётся параметр C — ожидается возврат значения D.
Здесь мы передаём функции массив чисел, после чего используем метод массивов map() , который позволяет модифицировать каждый элемент массива и формирует новый массив, возвращаемый функцией. Вызовем функцию, передав ей массив list :
От этой функции ожидается, что, приняв массив вида [1, 2, 3, 4, 5] , она возвратит новый массив [2, 3, 4, 5, 6] . Именно так она и работает.
Иммутабельность
Иммутабельность некоей сущности можно описать как то, что с течением времени она не меняется, или как невозможность изменений этой сущности.
Если иммутабельный объект пытаются изменить — сделать этого не удастся. Вместо этого нужно будет создать новый объект, содержащий новые значения.
Например, в JavaScript часто используется цикл for . В ходе его работы, как показано ниже, применяются мутабельные переменные:
На каждой итерации цикла меняется значение переменной i и значение глобальной переменной (её можно считать состоянием программы) sumOfValues . Как в подобной ситуации поддерживать неизменность сущностей? Ответ лежит в использовании рекурсии.
Тут имеется функция sum() , которая принимает массив чисел. Эта функция вызывает сама себя до тех пор, пока массив не опустеет (это базовый случай нашего рекурсивного алгоритма). На каждой такой «итерации» мы добавляем значение одного из элементов массива к параметру функции accumulator , не затрагивая при этом глобальной переменной accumulator . При этом глобальные переменные list и accumulator остаются неизменными, до и после вызова функции в них хранятся одни и те же значения.
Надо отметить, что для реализации подобного алгоритма можно воспользоваться методом массивов reduce . Об этом мы поговорим ниже.
В программировании распространена задача, когда нужно, на основе некоего шаблона объекта, создать его окончательное представление. Представьте, что у нас есть строка, которую нужно преобразовать в вид, подходящий для использования в качестве части URL, ведущего к некоему ресурсу.
Если решить эту задачу, используя Ruby и задействовав принципы ООП, то мы сначала создадим класс, скажем, назвав его UrlSlugify , после чего создадим метод этого класса slugify! , который используется для преобразования строки.
Алгоритм мы реализовали, и это замечательно. Тут мы видим императивный подход к программированию, когда мы, обрабатывая строку, расписываем каждый шаг её трансформации. А именно — сначала приводим её символы к нижнему регистру, потом убираем ненужные пробелы, и, наконец, меняем оставшиеся пробелы на тире.
Однако в ходе такого преобразования происходит мутация состояния программы.
Справиться с проблемой мутации можно, выполняя композицию функций или объединение вызовов функций в цепочку. Другими словами, результат, возвращаемый функцией, будет использован как входные данные для следующей функции, и так — для всех функций, объединённых в цепочку. При этом исходная строка меняться не будет.
Здесь мы используем следующие функции, представленные в JavaScript стандартными методами строк и массивов:
- toLowerCase : преобразует символы строки к нижнему регистру.
- trim : убирает пробельные символы из начала и конца строки.
- split : разбивает строку на части, помещая слова, разделённые пробелами, в массив.
- join : формирует на основе массива со словами строку, слова в которой разделены тире.
Ссылочная прозрачность
Создадим функцию square() , возвращающую результат умножения числа на это же число:
Это чистая функция, которая всегда, для одного и того же входного значения, будет возвращать одно и то же выходное значение.
Например, сколько бы ей ни передавали число 2 , эта функция всегда будет возвращать число 4 . В результате оказывается, что вызов вида square(2) можно заменить числом 4 . Это означает, что наша функция обладает свойством ссылочной прозрачности.
В целом, можно сказать, что если функция неизменно возвращает один и тот же результат для одних и тех же передаваемых ей входных значений, она обладает ссылочной прозрачностью.
▍Чистые функции + иммутабельные данные = ссылочная прозрачность
Задействовав идею, вынесенную в заголовок этого раздела, можно мемоизировать функции. Предположим, у нас имеется такая функция:
Мы вызываем её так:
Вызов sum(5, 8) всегда даёт 13 . Поэтому вышеприведённый вызов можно переписать так:
Это выражение, в свою очередь, всегда даёт 16 . Как результат, его можно заменить числовой константой и мемоизировать его.
Функции как объекты первого класса
Идея восприятия функций как объектов первого класса заключается в том, что такие функции можно рассматривать как значения и работать с ними как с данными. При этом можно выделить следующие возможности функций:
- Ссылки на функции можно хранить в константах и переменных и через них обращаться к функциям.
- Функции можно передавать другим функциям в качестве параметров.
- Функции можно возвращать из других функций.
Представьте, что у нас имеется функция, которая складывает переданные ей два числовых значения, после чего умножает их на 2 и возвращает то, что у неё получилось:
Теперь напишем функцию, которая вычитает из первого переданного ей числового значения второе, умножает то, что получилось, на 2 , и возвращает вычисленное значение:
Эти функции имеют схожую логику, различаются они лишь тем, какие именно операции они производят с переданными им числами. Если мы можем рассматривать функции как значения и передавать их как аргументы другим функциям, это означает, что мы можем создать функцию, которая принимает и использует другую функцию, описывающую особенности выполняемых вычислений. Эти рассуждения позволяют нам выйти на следующие конструкции:
Как видите, теперь у функции doubleOperator() имеется параметр f , а функция, которую он представляет, используется для обработки параметров a и b . Функции sum() и substraction() , передаваемые функции doubleOperator() , фактически, позволяют управлять поведением функции doubleOperator() , меняя его в соответствии с реализованной в них логикой.
Функции высшего порядка
Говоря о функциях высшего порядка мы имеем в виду функции, которые характеризуются хотя бы одной из следующих особенностей:
- Функция принимает другую функцию в качестве аргумента (таких функций может быть и несколько).
- Функция возвращает другую функцию в качестве результата своей работы.
▍Фильтрация массивов и метод filter()
Предположим, у нас есть некая коллекция элементов, которую мы хотим отфильтровать по какому-то атрибуту элементов этой коллекции и сформировать новую коллекцию. Функция filter() ожидает получить какой-то критерий оценки элементов, на основе которого она и определяет, нужно или не нужно включать некий элемент в результирующую коллекцию. Этот критерий задаёт передаваемая ей функция, которая возвращает true в том случае, если функция filter() должна включить элемент в итоговую коллекцию, а в противном случае возвращает false .
Представим, что у нас имеется массив целых чисел и мы хотим отфильтровать его, получив новый массив, в котором содержатся только чётные числа из исходного массива.
Императивный подход
При применении императивного подхода к решению этой задачи средствами JavaScript нам нужно реализовать следующую последовательность действий:
- Создать пустой массив для новых элементов (назовём его evenNumbers ).
- Перебрать исходный массив целых чисел (назовём его numbers ).
- Поместить чётные числа, обнаруженные в массиве numbers , в массив evenNumbers .
Кроме того, мы можем написать функцию (назовём её even() ), которая, если число является чётным, возвращает true , а если нечётным — false , после чего передать её методу массива filter() , который, проверив с её помощью каждый элемент массива, сформирует новый массив, содержащий лишь чётные числа:
Вот, кстати, решение одной интересной задачи, касающейся фильтрации массива, которое я выполнил, работая над задачами по функциональному программированию на Hacker Rank. По условию задачи нужно было отфильтровать массив целых чисел, выведя лишь те его элементы, которые меньше заданного значения x .
Императивное решение этой задачи на JavaScript может выглядеть так:
Суть императивного подхода заключается в том, что мы расписываем последовательность действий, выполняемых функцией. А именно — мы описываем перебор массива, сравнение текущего элемента массива с x и помещение этого элемента в массив resultArray в том случае, если он проходит проверку.
Декларативный подход
Как перейти к декларативному подходу решения этой задачи и соответствующему использованию метода filter() , являющегося функцией высшего порядка? Например, это может выглядеть так:
Возможно, вам в этом примере необычным покажется использование ключевого слова this в функции smaller() , но ничего сложного тут нет. Ключевое слово this представляет собой второй аргумент метода filter() . В нашем примере это — число 3 , представленное параметром x функции filterArray() . На это число и указывает this .
Такой же подход можно использовать и в том случае, если в массиве хранятся сущности, обладающие достаточно сложной структурой, например — объекты. Предположим, у нас имеется массив, хранящий объекты, содержащие имена людей, представленные свойством name , и сведения о возрасте этих людей, представленные свойством age . Вот как выглядит такой массив:
Мы хотим этот массив отфильтровать, выбрав из него только те объекты, которые представляют собой людей, чей возраст превысил 21 год. Вот как можно решить эту задачу:
Здесь у нас имеется массив с объектами, представляющими людей. Мы проверяем элементы этого массива с помощью функции olderThan21() . В данном случае мы, при проверке, обращаемся к свойству age каждого элемента, проверяя, превышает ли значение этого свойства 21 . Данную функцию мы передаём методу filter() , который и фильтрует массив.
▍Обработка элементов массивов и метод map()
Метод map() используется для преобразования элементов массивов. Он применяет к каждому элементу массива переданную ему функцию, после чего строит новый массив, состоящий из изменённых элементов.
Продолжим эксперименты с уже знакомым вам массивом people . Теперь мы не собираемся фильтровать этот массив, основываясь на свойстве объектов age . Нам нужно сформировать на его основе список строк вида TK is 26 years old . Строки, в которые превращаются элементы, при таком подходе будут строиться по шаблону p.name is p.age years old , где p.name и p.age — это значения соответствующих свойств элементов массива people .
Императивный подход к решению этой задачи на JavaScript выглядит так:
Если прибегнуть к декларативному подходу, то получится следующее:
Собственно говоря, основная мысль тут заключается в том, что с каждым элементом исходного массива нужно что-то сделать, после чего — поместить его в новый массив.
Вот ещё одна задача с Hacker Rank, которая посвящена обновлению списка. А именно, речь идёт о том, чтобы поменять значения элементов существующего числового массива на их абсолютные значения. Так, например, при обработке массива [1, 2, 3, -4, 5] он приобретёт вид [1, 2, 3, 4, 5] так как абсолютное значение -4 равняется 4 .
Вот пример простого решения этой задачи, когда мы перебираем массив и меняем значения его элементов на их абсолютные значения.
Тут для преобразования значений элементов массива использован метод Math.abs() , изменённые элементы записываются туда же, где они были до преобразования.
Это решение не является примером функционального подхода к программированию.
Первое, что надо вспомнить в связи с этим решением, заключается в том, что выше мы говорили о важности иммутабельности. Эта концепция делает результаты работы функций предсказуемыми и ведёт к стабильной работе функций. При таком подходе, решая нашу задачу, нужно создать новый массив, содержащий абсолютные значения элементов исходного массива.
Второй вопрос, который стоит себе задать в этой ситуации, касается метода массивов map() . Почему бы не воспользоваться им?
Вооружившись этими идеями, я решил поэкспериментировать с методом abs() , взглянуть на то, как он обрабатывает разные числа.
Как видно, он возвращает положительные числа, представляющие собой абсолютное значение передаваемых ему чисел.
После того, как мы поняли принцип преобразования числа к его абсолютному значению, мы можем использовать Math.abs() в качестве аргумента метода массива map() . Помните о том, что функции высшего порядка могут принимать другие функции и использовать их? Метод map() является именно такой функцией. Вот как решение нашей задачи будет выглядеть теперь:
Уверен, никто не поспорит с тем, что оно, в сравнении с предыдущим вариантом, получилось куда более простым, предсказуемым и понятным.
▍Преобразование массивов и метод reduce()
В основу метода reduce() положена идея преобразования массива к единственному значению путём комбинации его элементов с использованием некоей функции.
Распространённым примером применения этого метода является нахождение общей суммы по некоему заказу. Представьте, что речь идёт об интернет-магазине. Покупатель добавляет в корзину товары Product 1 , Product 2 , Product 3 и Product 4 . После этого нам надо найти общую стоимость этих товаров.
Если воспользоваться для решения этой задачи императивным подходом, то нужно перебрать список товаров из корзины и сложить их стоимости. Например, это может выглядеть так:
Если воспользоваться для решения этой задачи методом массивов reduce() , то мы можем создать функцию ( sumAmount() ), используемую для вычисления суммы элементов массива, после чего передать её методу reduce() :
Тут имеется массив shoppingCart , представляющий собой корзину покупателя, функция sumAmount() , которая принимает элементы массива (объекты order , при этом нас интересуют их свойства amount ), и текущее вычисленное значение суммы их стоимостей — currentTotalAmount .
При вызове метода reduce() , выполняемого в функции getTotalAmount() , ему передаётся функция sumAmount() и начальное значение счётчика, которое равняется 0 .
Ещё один способ решения нашей задачи заключается в комбинации методов map() и reduce() . Что имеется в виду под их «комбинацией»? Дело тут в том, что мы можем использовать метод map() для преобразования массива shoppingCart в массив, содержащий лишь значения свойств amount хранящихся в этом массиве объектов, а затем воспользоваться методом reduce() и функцией sumAmount() . Вот как это выглядит:
Функция getAmount() принимает объект и возвращает только его свойство amount . После обработки массива с использованием метода map() , которому передана эта функция, получается новый массив, который выглядит как [10, 30, 20, 60] . Затем, с помощью reduce() , мы находим сумму элементов этого массива.
▍Совместное использование методов filter(), map() и reduce()
Выше мы поговорили о том, как работают функции высшего порядка, рассмотрели методы массивов filter() , map() и reduce() . Теперь, на простом примере, рассмотрим использование всех трёх этих функций.
Продолжим пример с интернет-магазином. Предположим, корзина покупателя сейчас выглядит так:
Нам нужно узнать стоимость книг в заказе. Вот какой алгоритм можно предложить для решения этой задачи:
- Отфильтровать массив по значению свойства type его элементов, учитывая то, что нас интересует значение этого свойства books .
- Преобразовать полученный массив в новый, содержащий лишь стоимости товаров.
- Сложить все стоимости товаров и получить итоговое значение.
Итоги
В этом материале мы поговорили о применении некоторых идей функционального программирования в JavaScript-разработке. Надеемся, вам эти идеи пригодятся.
Уважаемые читатели! Пользуетесь ли вы методами функционального программирования в своих проектах?
Детерминированность — Основы JavaScript
Независимо от того, какой язык программирования используется, функции внутри него обладают некоторыми фундаментальными свойствами. Зная эти свойства, легче прогнозировать поведение функций, способы их тестирования и место их использования. К таким свойствам относится детерминированность. Функция называется детерминированной тогда, когда для одних и тех же входных параметров она возвращает один и тот же результат. Например, функция, считающая количество символов, детерминированная:
Сколько бы раз мы ни вызывали эту функцию, передавая туда значение 'hexlet' , она всегда вернет 6 . В свою очередь функция, возвращающая случайное число, не является детерминированной, так как у одного и того же входа (даже если он пустой, то есть параметры не принимаются) мы получим всегда разный результат. Насколько он разный – не важно, даже если хотя бы один из миллиона вызовов вернет что-то другое, эта функция автоматически считается недетерминированной.
Зачем это нужно знать? Детерминированность серьезно влияет на многие аспекты. Детерминированные функции удобны в работе, их легко оптимизировать, легко тестировать. Если есть возможность сделать функцию детерминированной, то лучше ее такой и сделать.
Побочные эффекты
Вы, скорее всего, уже заметили (может, подсознательно), что console.log() — это тоже функция. Она принимает на вход данные любого типа и выводит их на экран.
Внимание, вопрос: что возвращает функция console.log() ? Ответ: что бы она ни возвращала, это значение никак не используется.
console.log() выводит что-то на экран, но это не возврат значения, это просто какое-то действие, которое выполняет функция.
Вывод на экран и возврат значения из функции — разные и независимые операции. Технически вывод на экран равносилен записи в файл (немного особый, но все-таки файл). Для понимания этой темы необходимо немного разобраться в устройстве операционных систем, что крайне важно для программистов.
С точки зрения программы вывод на экран — это так называемый побочный эффект. Побочным эффектом называют действия, которые изменяют внешнее окружение (среду выполнения). К таким действиям относятся любые сетевые взаимодействия, взаимодействие с файловой системой (чтение и запись файлов), вывод информации на экран или печать на принтере и так далее.
Побочные эффекты — один из основных источников проблем и ошибок в программных системах. Код с побочными эффектами сложен в тестировании, ненадежен. При этом без побочных эффектов программирование не имеет смысла. Без них было бы невозможно получить результат работы программы (записать в базу, вывести на экран, отправить по сети и так далее).
Понимание принципов работы с побочными эффектами очень сильно влияет на стиль программирования и способность строить качественные программы. Эта тема полностью раскроется в следующих курсах на Хекслете.
Вопрос для самопроверки. Можно ли определить наличие побочных эффектов внутри функции, опираясь только на ее возврат?
Чистые функции — JS: Функции
Функции в программировании обладают рядом важных характеристик. Зная их, мы можем точнее определять, как лучше разбивать код на функции и когда вообще их стоит выделять.
Детерминированность
Встроенная в JavaScript функция Math.random() возвращает случайное число от 0 до 1:
Функция нужная и полезная, но неудобная в отладке и тестировании. Это связано с тем, что для одних и тех же входных аргументов (отсутствие аргументов также попадает под это понятие), она может возвращать разные значения. Функции с таким поведением называются недетерминированными.
Например, недетерминированными являются функции, оперирующие системным временем. Так, функция Date.now() каждый раз возвращает новое значение:
А вот пример с аргументами. Представьте функцию getAge() , которая принимает на вход год рождения и возвращает возраст:
Хотя прямо сейчас повторный запуск вернёт точно такое же значение, через год оно уже будет другим. То есть функция считается недетерминированной, если она ведёт себя так хотя бы единожды.
Детерминированные функции, напротив, ведут себя предсказуемо. Для одних и тех же входных данных они всегда выдают один и тот же результат. Именно такими являются функции в математике.
Интересно что, например, функция console.log() — детерминированная. Дело в том, что она всегда возвращает одно и то же значение для любых входных данных. Это значение undefined , а не то, что печатается на экран, как можно было бы подумать. Печать на экран — побочный эффект, о нём мы поговорим чуть позже.
Вызов console.log('Hexlet — Big Bang') выполнил два действия:
- Вывел сообщение Hexlet — Big Bang в терминал (или консоль браузера, в зависимости от среды выполнения)
- Вернул значение undefined . Какое сообщение бы мы ни печатали, возвращаемое значение всегда будет одно — undefined
Функция становится недетерминированной и в том случае, если она обращается не только к своим аргументам, но и некоторым внешним данным, например глобальным переменным, переменным окружения и так далее. Так происходит потому, что внешние данные могут измениться, и функция начнёт выдавать другой результат, даже если в неё передаются одни и те же аргументы.
Функция getCurrentShell() обращается к переменной окружения SHELL . Но в разные моменты времени и в разных окружениях значение этой переменной может быть различным.
В общем случае нельзя сказать, что отсутствие детерминированности — абсолютное зло. Для работы многих программ и сайтов нужна функция, возвращающая случайное число или вычисляющая текущую дату. С другой стороны, в нашей власти разделить код так, чтобы в нем было как можно больше детерминированных частей. Общая рекомендация при работе с детерминированностью звучит следующим образом: если есть возможность написать функцию так, что она будет детерминированной, то так и делайте. Не используйте глобальных переменных, создавайте функции, зависящие только от своих собственных аргументов.
Понятие "Детерминированность" не ограничивается программированием или математикой. Сквозь него можно рассматривать практически любой процесс. Например, подбрасывание монетки — недетерминированный процесс, его результат случаен.
Побочные эффекты (side effects)
Вторая ключевая характеристика функций — наличие побочных эффектов. Побочными эффектами называют любые взаимодействия с внешней средой. К ним относятся файловые операции, такие как запись в файл, чтение файла, отправка или приём данных по сети и даже вывод в консоль.
Кроме того, побочными эффектами считаются изменения внешних переменных (например, глобальных) и входных параметров в случае, когда они передаются по ссылке.
А вот вычисления (логика), напротив, не содержат побочных эффектов. Например, функция, суммирующая два переданных аргументами числа.
Побочные эффекты составляют одну из самых больших сложностей при разработке. Их наличие значительно затрудняет логику кода и тестирование. Приводит к возникновению огромного числа ошибок. Только при работе с файлами количество возможных ошибок измеряется сотней: начиная с того, что закончилось место на диске, заканчивая попыткой читать данные из несуществующего файла. Для их предотвращения код обрастает большим числом проверок и защитных механизмов.
Без побочных эффектов невозможно написать ни одной полезной программы. Какие бы важные вычисления она ни делала, их результат должен быть как-то продемонстрирован. В самом простом случае его нужно вывести на экран, что автоматически приводит нас к побочным эффектам:
В реальных же приложениях, обычно, все сводится к взаимодействию с базой данных или отправкой запросов по сети.
Не существует способа избавиться от побочных эффектов совсем, но их влияние на программу можно минимизировать. Как правило, в типичной программе побочных эффектов не так много по отношению к остальному коду, и происходят они лишь в самом начале и в конце. Например, программа, которая конвертирует файл из текстового формата в PDF, в идеале выполняет ровно два побочных эффекта:
- Читает файл в самом начале работы программы.
- Записывает результат работы программы в новый файл.
Между этими двумя пунктами и происходит основная работа, которая содержит чистую алгоритмическую часть. Побочные эффекты в таком случае будут находиться только в верхнем слое приложения, а ядро, выполняющее основную работу, останется чистым от них.
Инкремент и декремент — единственные базовые арифметические операции в JS, которые обладают побочными эффектами (изменяют само значение в переменной). Именно поэтому с ними сложно работать в составных выражениях. Они могут приводить к таким сложноотлавливаемым ошибкам, что во многих языках вообще отказались от их введения (в Ruby и Python их нет). В JS стандарты кодирования предписывают их не использовать.
Чистые функции
Идеальная функция с точки зрения удобства работы с ней называется чистой (pure). Чистая функция — это детерминированная функция, которая не производит побочных эффектов. Такая функция зависит только от своих входных аргументов и всегда ведёт себя предсказуемо.
Чистые функции обладают рядом ключевых достоинств:
- Их просто тестировать. Достаточно передать на вход функции нужные параметры и посмотреть ожидаемый выход.
- Их безопасно запускать повторно, что особенно актуально в асинхронном коде или в случае многопоточного кода.
- Их легко комбинировать, получая новое поведение без необходимости переписывать программу (подробнее далее по курсу).
Открыть доступ
Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно