Чистые функции — JS: Функции
Функции в программировании обладают рядом важных характеристик. Зная их, мы можем точнее определять, как лучше разбивать код на функции и когда вообще их стоит выделять.
Детерминированность
Встроенная в JavaScript функция Math.random() возвращает случайное число от 0 до 1:
Math.random(); // 0.9337432365797949 Math.random(); // 0.5550694016887598
Функция нужная и полезная, но неудобная в отладке и тестировании. Это связано с тем, что для одних и тех же входных аргументов (отсутствие аргументов также попадает под это понятие), она может возвращать разные значения. Функции с таким поведением называются недетерминированными.
Например, недетерминированными являются функции, оперирующие системным временем. Так, функция Date.now() каждый раз возвращает новое значение:
// Возвращает текущее время в миллисекундах Date.now(); // 1571909874844 Date.now(); // 1571909876648
А вот пример с аргументами. Представьте функцию getAge() , которая принимает на вход год рождения и возвращает возраст:
getAge(2000); // ?
Хотя прямо сейчас повторный запуск вернёт точно такое же значение, через год оно уже будет другим. То есть функция считается недетерминированной, если она ведёт себя так хотя бы единожды.
Детерминированные функции, напротив, ведут себя предсказуемо. Для одних и тех же входных данных они всегда выдают один и тот же результат. Именно такими являются функции в математике.
Интересно что, например, функция console.log() — детерминированная. Дело в том, что она всегда возвращает одно и то же значение для любых входных данных. Это значение undefined , а не то, что печатается на экран, как можно было бы подумать. Печать на экран — побочный эффект, о нём мы поговорим чуть позже.
console.log('Hexlet – Big Bang');
Вызов console.log(‘Hexlet — Big Bang’) выполнил два действия:
- Вывел сообщение Hexlet — Big Bang в терминал (или консоль браузера, в зависимости от среды выполнения)
- Вернул значение undefined . Какое сообщение бы мы ни печатали, возвращаемое значение всегда будет одно — undefined
Функция становится недетерминированной и в том случае, если она обращается не только к своим аргументам, но и некоторым внешним данным, например глобальным переменным, переменным окружения и так далее. Так происходит потому, что внешние данные могут измениться, и функция начнёт выдавать другой результат, даже если в неё передаются одни и те же аргументы.
const getCurrentShell = () => process.env.SHELL; getCurrentShell(); // /bin/bash
Функция getCurrentShell() обращается к переменной окружения SHELL . Но в разные моменты времени и в разных окружениях значение этой переменной может быть различным.
В общем случае нельзя сказать, что отсутствие детерминированности — абсолютное зло. Для работы многих программ и сайтов нужна функция, возвращающая случайное число или вычисляющая текущую дату. С другой стороны, в нашей власти разделить код так, чтобы в нем было как можно больше детерминированных частей. Общая рекомендация при работе с детерминированностью звучит следующим образом: если есть возможность написать функцию так, что она будет детерминированной, то так и делайте. Не используйте глобальных переменных, создавайте функции, зависящие только от своих собственных аргументов.
Понятие «Детерминированность» не ограничивается программированием или математикой. Сквозь него можно рассматривать практически любой процесс. Например, подбрасывание монетки — недетерминированный процесс, его результат случаен.
Побочные эффекты (side effects)
Вторая ключевая характеристика функций — наличие побочных эффектов. Побочными эффектами называют любые взаимодействия с внешней средой. К ним относятся файловые операции, такие как запись в файл, чтение файла, отправка или приём данных по сети и даже вывод в консоль.
const someFunction = () => // Функция fetch выполняет HTTP-запрос // HTTP-запрос — это побочный эффект fetch('https://ru.hexlet.io/courses'); >;
Кроме того, побочными эффектами считаются изменения внешних переменных (например, глобальных) и входных параметров в случае, когда они передаются по ссылке.
const someFunction = (obj) => // Какая-то логика // Побочный эффект. Изменение входного аргумента. obj.key = 'value'; >;
А вот вычисления (логика), напротив, не содержат побочных эффектов. Например, функция, суммирующая два переданных аргументами числа.
const sum = (num1, num2) => num1 + num2;
Побочные эффекты составляют одну из самых больших сложностей при разработке. Их наличие значительно затрудняет логику кода и тестирование. Приводит к возникновению огромного числа ошибок. Только при работе с файлами количество возможных ошибок измеряется сотней: начиная с того, что закончилось место на диске, заканчивая попыткой читать данные из несуществующего файла. Для их предотвращения код обрастает большим числом проверок и защитных механизмов.
Без побочных эффектов невозможно написать ни одной полезной программы. Какие бы важные вычисления она ни делала, их результат должен быть как-то продемонстрирован. В самом простом случае его нужно вывести на экран, что автоматически приводит нас к побочным эффектам:
console.log(sum(4, 11)); // => 15
В реальных же приложениях, обычно, все сводится к взаимодействию с базой данных или отправкой запросов по сети.
Не существует способа избавиться от побочных эффектов совсем, но их влияние на программу можно минимизировать. Как правило, в типичной программе побочных эффектов не так много по отношению к остальному коду, и происходят они лишь в самом начале и в конце. Например, программа, которая конвертирует файл из текстового формата в PDF, в идеале выполняет ровно два побочных эффекта:
- Читает файл в самом начале работы программы.
- Записывает результат работы программы в новый файл.
Между этими двумя пунктами и происходит основная работа, которая содержит чистую алгоритмическую часть. Побочные эффекты в таком случае будут находиться только в верхнем слое приложения, а ядро, выполняющее основную работу, останется чистым от них.
Инкремент и декремент — единственные базовые арифметические операции в JS, которые обладают побочными эффектами (изменяют само значение в переменной). Именно поэтому с ними сложно работать в составных выражениях. Они могут приводить к таким сложноотлавливаемым ошибкам, что во многих языках вообще отказались от их введения (в Ruby и Python их нет). В JS стандарты кодирования предписывают их не использовать.
Чистые функции
Идеальная функция с точки зрения удобства работы с ней называется чистой (pure). Чистая функция — это детерминированная функция, которая не производит побочных эффектов. Такая функция зависит только от своих входных аргументов и всегда ведёт себя предсказуемо.
Чистые функции обладают рядом ключевых достоинств:
- Их просто тестировать. Достаточно передать на вход функции нужные параметры и посмотреть ожидаемый выход.
- Их безопасно запускать повторно, что особенно актуально в асинхронном коде или в случае многопоточного кода.
- Их легко комбинировать, получая новое поведение без необходимости переписывать программу (подробнее далее по курсу).
Открыть доступ
Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно
- 130 курсов, 2000+ часов теории
- 1000 практических заданий в браузере
- 360 000 студентов
Наши выпускники работают в компаниях:
«Чистые» и «нечистые» функции в JavaScript
Что такое «чистые» и «нечистые» функции в JavaScript, и как их различать.
JavaScriptFunctional programming · 06.06.2019 · читать 3 мин · Автор: Alexey Myzgin
«Чистые» функции — это любые функции, исходные данные которых получены исключительно из их входных данных и не вызывают побочных эффектов в приложении. Математические функции являются примерами «чистых» функций. «Нечистые» функции бывают разных форм и размеров. Вот некоторые примеры:
- функции, вывод которых зависит от внешнего / глобального состояния;
- функции, которые возвращают разные исходные данные при одинаковых входных;
- функции, которые изменяют состояние приложения;
- функции, которые изменяют «внешний мир».
Функциональное программирование основано на использовании «чистых» функций и строгом контроле побочных эффектов. Способность распознавать любой тип функции является ключевым для функционального программирования.
«Чистая» функция — это функция, которая выводит свои данные основываясь исключительно на свои входные данные и не вызывает побочных эффектов в приложении.
Например у нас есть функция, которая получает одно значение x и возвращает в данном случае x + 1 :
// function f(x) const f = x => x + 1;
Довольно легко понять, что это «чистая» функция. Мы получаем тот же результат при вызове этой функции, с тем же входным значением; плюс у нас нет внешних зависимостей, которые бы вызывали побочные эффекты. Чтобы понять это далее, давай сравним это с несколькими «нечистыми» функциями.
Первая «нечистая» функция
Первая «нечистая» функция, которую мы собираемся сделать — это та, чей результат не основан исключительно на её входных данных. Например, давай рассмотрим функцию totalPrice . У нас есть глобальная переменная — COST_OF_ITEM , которая содержит цену на товар. Функция totalPrice берет quantity и умножает ее на эту переменную.
// Глобальная переменная const COST_OF_ITEM = 250; const totalPrice = quantity => COST_OF_ITEM * quantity;
На первый взгляд может показаться, что это «чистая» функция, потому что мы всегда получаем один и тот же результат на основе одного и того же входного значения. Это можно увидеть, вызвав её несколько раз с одним и тем же значением, и вывив в консоль. В обоих случаях мы получаем 500.
// Глобальная переменная const COST_OF_ITEM = 250; const totalPrice = quantity => COST_OF_ITEM * quantity; console.log(totalPrice(2)); // 500 console.log(totalPrice(2)); // 500
Хоть мы и получаем тот же результат, но это «нечистая» функция, так как состояние нашего приложения влияет на вывод нашей функции. Мы можем увидеть это, изменив значение COST_OF_ITEM и посмотреть снова в консоль.
const COST_OF_ITEM = 200; const totalPrice = quantity => COST_OF_ITEM * quantity; console.log(totalPrice(2)); // 400 console.log(totalPrice(2)); // 400
Вторая «нечистая» функция
Наш второй пример «нечистой» функции — это функция, которая получает один и тот же аргумент, но возвращает разные результаты. Часто в наших приложениях нам нужно создавать объекты, которые имеют уникальные идентификаторы, такие как id .
Давай создадим функцию generateID . Эта функция будет возвращать целое число в случайном порядке. Это «нечистая» функция, так как вызвав её несколько раз, каждый раз мы получим разный результат.
const generateID = () => Math.floor(Math.random() * 10000); console.log(generateID()); // 7602 console.log(generateID()); // 1377 console.log(generateID()); // 7131
Давай используем нашу «нечистую» функцию generateID внутри фабричной функции для создания пользовательских объектов. Чтобы создать пользователя, createUser принимает два параметра: name и age , и возвращает объект с id , используя функцию generateID для его создания, а также name и age .
const generateID = () => Math.floor(Math.random() * 10000); const createUser = (name, age) => ( id: generateID(), name, age >);
Вызовем createUser несколько раз, с одинаковыми аргументами.
const generateID = () => Math.floor(Math.random() * 10000); const createUser = (name, age) => ( id: generateID(), name, age >); console.log(createUser("Alex", 28)); // console.log(createUser("Alex", 28)); // console.log(createUser("Alex", 28)); //
Если посмотрим в консоль, то увидим, что мы получили похожие объекты, но они не одинаковые — id у всех разный.
«Нечистота» функции generateID делает нашу фабричную функцию createUser «нечистой». Для того чтобы исправить это — можно переместить «нечистую» функцию за пределы фабрики и вызвать её где-нибудь, где мы ожидаем побочный эффект, и передать id в качестве параметра в нашу фабрику createUser .
Третья «нечистая» функция
Третий пример. Как и в примере с глобальной переменной, мы можем создавать функции, которые изменяют состояние нашего приложения, вызывая побочный эффект.
Допустим, мы отслеживаем изменяемое значение (в данном случае id ). Если мы создадим функцию, которая изменяет это значение, у нас будет «нечистая» функция. Например фабричная функция createPersone .
let id = 0; const createPersone = name => ( id: ++id, name >);
Если мы генерируем наш id для этого объекта, изменяя значение глобального id , то это «нечистая» функция. Вызвав эту функции несколько раз с разными name , то увидим, что id увеличился как мы и ожидали, но если мы также выведем в консоль глобальное значение id , то увидим, что оно тоже изменилось.
let id = 0; const createPersone = name => ( id: ++id, name >); console.log(createPersone("Alex")); // console.log(createPersone("Julia")); // console.log(id); // 2
Четвертая «нечистая» функция
Последний четвертый пример «нечистой» функции — это побочный эффект “внешнего мира”. console.log — «нечистая» функция, так как она создает побочный эффект во “внешнем мире”.
Каждый раз, когда мы используем console.log , это влияет на нашу консоль, а это побочный эффект. Если у нас есть какая-либо функция, использующая console.log , (например, функция logger , которая принимает сообщение и выводит его) это тоже «нечистая» функция.
const logger = msg => console.log(msg); >; logger("Всем привет!");
Что такое чистые функции в JavaScript?
Чистые функции — строительные блоки в функциональном программировании. Их обожают за простоту и тестируемость.
В этой статье вы найдете чек-лист, который поможет определить чистая функция или нет.
Чек-лист
Функция должна удовлетворять двум условиям, чтобы считаться «чистой»:
— Каждый раз функция возвращает одинаковый результат, когда она вызывается с тем же набором аргументов
— Нет побочных эффектов
1. Одинаковый вход => Одинаковый выход
const add = (x, y) => x + y; add(2, 4); // 6
let x = 2; const add = (y) => < x += y; >; add(4); // x === 6 (the first time)
В первом случае значение возвращается на основании заданных параметров, независимо от того, где/когда вы его вызываете.
Если вы сложите 2 и 4, всегда получите 6.
Ничего не влияет на результат.
Нечистые функции = непостоянные результаты
Второй пример ничего не возвращает. Он полагается на общее состояние для выполнения своей работы путем увеличения переменной за пределами своей области.
Эта модель кошмар для разработчиков.
Разделяемое состояние вводит зависимость от времени. Вы получаете разные результаты в зависимости от того, когда вы вызвали функцию. В первый раз результат 6, в следующий раз 10 и так далее.
В каком случае вы получите меньше багов, которые появляются только при определенных условиях?
В каком случае с большей вероятностью вы преуспеете в многопоточной среде, где временные зависимости могут сломать систему?
Определенно в первом.
2. Нет побочных эффектов
Этот тест сам по себе контрольный список.
Примеры побочных эффектов:
- Видоизменение входных параметров
- console.log
- HTTP вызовы (AJAX/fetch)
- Изменение в файловой системе
- Запросы DOM
Советую посмотреть видео Боба Мартина.
Вот “нечистая” функция с побочным эффектом.
const impureDouble = (x) => < console.log('doubling', x); return x * 2; >; const result = impureDouble(4); console.log(< result >);
console.log здесь это побочный эффект, но он не повредит. Мы все равно получим те же результаты, учитывая те же данные.
Однако, это может вызвать проблемы.
“Нечистое” изменение объекта
const impureAssoc = (key, value, object) => < object[key] = value; >; const person = < name: 'Bobo' >; const result = impureAssoc('shoeSize', 400, person); console.log(< person, result >);
Переменная person была изменена навсегда, потому что функция была объявлена через оператор присваивания.
Разделяемое состояние означает, что влияние impureAssoc уже не полностью очевидно. Понимание влияния на систему теперь включает отслеживание каждой переменной, к которой когда-либо прикасалась, и знание ее истории.
Разделяемое состояние = временные зависимости.
Мы можем очистить impureAssoc, просто вернув новый объект с желаемыми свойствами.
“Очищаем это”
const pureAssoc = (key, value, object) => (< . object, [key]: value >); const person = < name: 'Bobo' >; const result = pureAssoc('shoeSize', 400, person); console.log(< person, result >);
Теперь pureAssoc возвращает тестируемый результат, и можно не беспокоиться, если он изменится где-то в другом месте.
Можно было сделать и так:
const pureAssoc = (key, value, object) => < const newObject = < . object >; newObject[key] = value; return newObject; >; const person = < name: 'Bobo' >; const result = pureAssoc('shoeSize', 400, person); console.log(< person, result >);
Изменять входные данные может быть опасно, но изменять их копию не проблема. Конечный результат — тестируемая, предсказуемая функция, которая работает независимо от того, где и когда вы ее вызываете.
Изменения ограничиваются этой небольшой областью, и вы все еще возвращаете значение.
Резюме
- Функция чистая, если не имеет побочных эффектов и каждый раз возвращает одинаковый результат, когда она вызывается с тем же набором аргументов.
- Побочные эффекты включают: меняющийся вход, HTTP-вызовы, запись на диск, вывод на экран.
- Вы можете безопасно клонировать, а затем менять входные параметры. Просто оставьте оригинал без изменений.
- Синтаксис распространения (… syntax) — это самый простой способ клонирования объектов и массивов.
Существуют ли чистые функции в JavaScript?
Недавно я ввязался в дискуссию о том, как определить чистую функцию в JavaScript. Вся концепция чистоты, кажется, расплывается в таком динамичном языке. Следующие примеры показывают, что нам, возможно, придётся пересмотреть термин «чистая функция», или, как минимум, быть очень осторожными, когда мы его используем.
Что такое чистая функция?
Если вы не знакомы с этим термином, то я рекомендую вам сначала прочитать небольшое вступление. Определение «чистая функция» от Alvin Alexander и Мастер JavaScript интервью: что такое чистая функция? от Eric Elliott будут отличным выбором.
Вкратце, функция называется чистой, если она удовлетворяет двум условиям:
- Функция возвращает точно такой же результат каждый раз, когда она вызывается с тем же набором аргументов.
- Выполнение функции не изменяет какое-либо состояние за пределами её области видимости и не оказывает видимого воздействия на внешний мир, кроме возвращения значения (никаких побочных эффектов).
Иногда добавляется третье условие: «не опирается на внешнее изменяемое состояние». Это, по сути, избыточно, поскольку такая зависимость от изменяемой переменной неизбежно приведёт к нарушению первого условия.
Что из этого является чистым?
Здесь я написал четыре примера функций. Прежде чем продолжить, просмотрите их и решите самостоятельно, какие являются чистыми, а какие нечистыми.
// A: Простое умножение
function doubleA(n) return n * 2
>// B: C переменной
var two = 2
function doubleB(n) return n * two
>// C: С вспомогающей функцией
function getTwo() return 2
>
function doubleC(n) return n * getTwo()
>// D: Преобразование массива
function doubleD(arr) return arr.map(n => n * 2)
>
Сделали? Отлично, давайте сравним.
Когда я спрашивал, подавляющее большинство ответило, что функция doubleB является единственной нечистой, а функции doubleA , doubleC и doubleD чисты.
Итак, давайте рассмотрим условия. Последнее условие очевидно: не должно быть побочных эффектов.
Первое более интересное. При вызове с теми же аргументами все они возвращают одно и то же значение (используем toEqual для поддержки массивов):
expect( doubleA(1) ).toEqual( doubleA(1) )
expect( doubleB(1) ).toEqual( doubleB(1) )
expect( doubleC(1) ).toEqual( doubleC(1) )
expect( doubleD([1]) ).toEqual( doubleD([1]) )
Нуууу, написано так, что да. Однако, как насчет этой части кода, опубликованного моим другом Alexander?
doubleB(1) // -> 2
two = 3
doubleB(1) // -> 3
Результат верен. Я дважды запустил функцию с теми же аргументами и получил разное значение. Это делает её нечистой. Независимо от того, что происходило между ними.
Это заставило меня задуматься. Если это подтвердилось, то что насчёт других? Выдержат ли они, если я достаточно постараюсь? Как вы догадались, нет, они этого не сделают. Фактически, я сейчас говорю:
Ни одна из четырёх функций не является чистой.
Функции как объекты первого класса
В JavaScript функции являются объектами первого класса, то есть они могут быть значением переменной, которое может быть передано, возвращено и, да, переназначено. Если я могу изменить переменную two , я могу сделать и следующее:
doubleC(1) // -> 2
getTwo = function() < return 3 >
doubleC(1) // -> 3
Важно подчеркнуть, что это не отличается от того, что сделал Alex. Вместо того, чтобы содержать число, переменная содержит функцию.
Map по массиву
«Map, filter, reduce. Повторить». Это было название одной из моих livecoding-сессий. Эти три метода — ядро преобразования данных в функциональном программировании. Поэтому они должны быть безопасными для использования в чистых функциях.
Как выясняется, в JavaScript ничто не высечено в камне. Или я должен сказать в прототипе?
doubleD([1]) // -> [2]
Array.prototype.map = function() return [3]
>
doubleD([1]) // -> [3]
Подождите. Это, конечно, недопустимо. Это неправильно.
Это может быть неверно, это может быть глупо. По правде говоря, я просто дважды вызвал функцию doubleD с тем же аргументом и получил разные значения. Независимо от того, что произошло между ними.
Всё, что мы делаем, это переопределяем переменные между вызовами функций. Как мы сделали раньше.
Поэтому функция doubleD не является чистой.
Умножение чисто
Однако в JavaScript нельзя динамически переопределять встроенные операторы, как в некоторых языках.
Кроме того, n — локальная переменная, живущая только в области видимости этой функции. Её невозможно изменить извне.
Нет, это действительно невозможно. Вы должно быть невысокого мнения о JavaScript, если у вас есть на это надежда .
Но я выдал себя, когда написал, что ни одна из четырех функций не является чистой. Я приберег ещё один трюк в рукаве.
Хоть я и не могу изменить операцию или аргумент после его передачи, у меня есть свобода выбора того, что можно передать. Числа, строки, булевые значения, объекты…
Объекты? Какое применение у них может быть? Число, помноженное на объект, равно, эм… как 2 * <> . NaN . Проверьте это в консоли. Как это сделал я.
Хотя это не поможет. Если бы только был способ заставить среду выполнения преобразовать объект в число при умножении.
toString для чисел
Если объект появляется в контексте строки, например в случае конкатенации со строкой, движок запускает функцию toString объекта и использует результат. Если функция не реализована, он будет возвращаться к известному ‘[object Object]’ , созданному методом Object.prototype.toString .
В то же время JavaScript вызывает метод valueOf объекта, когда он ожидает число (или булевое значение, или функцию). Осталось только сделать так, чтобы эта функция возвращала разные значения при каждом вызове.
var o = valueOf: Math.random
>
doubleA(o) // -> 1.7709942335937932
doubleA(o) // -> 1.2600863386367704
Уфф, да. Функция вызывалась дважды с точно таким же (в любом смысле) аргументом, во второй раз она возвращала другое значение. Это не чисто.
Примечание: в предыдущей версии этой статьи использовался @@toPrimitive или, более точно, Symbol.toPrimitive . Как отметил Alexandre Morgaut, valueOf достаточно и он поддерживается с первой версии JavaScript. Если вы не знакомы с @@toPrimitive , вы всё ещё можете прочитать здесь.
И все-таки: что такое чистая функция?
Я знаю, я скрупулезный, злой и я использую грязные трюки. Мы используем чистые функции, чтобы получить уверенность в коде. Чтобы убедиться, что код делает то, что он должен. В любое время, при любых обстоятельствах.
Я хочу, чтобы все четыре функции были чистыми, если я так решил. Да, включая функции типа doubleB . Что делать, если эта переменная (в нашем случае, two ) не может изменяться, например это математическая константа e , pi или phi? Она должна быть чистой.
Я хочу иметь возможность доверять встроенным функциям. Какие программы я могу создать, если я предполагаю, что всё что угодно в Array.prototype или Object.prototype может измениться? Всё просто: никто никогда не захочет их использовать.
В результате этого небольшого забавного упражнения я считаю, что нам нужно новое определение того, что мы считаем чистой функцией в JavaScript. К сожалению, я не вижу, что это возможно сделать только техническими терминами.
В некотором роде он должен учитывать предполагаемое использование кода. Функция может считаться чистой в одном проекте и нечистой в другом. И это нормально. Пока программа работает. Пока у разработчиков есть уверенность.
У вас есть идеи для определения? Как вы решаете, что функция является чистой? Есть что-нибудь, что я упустил? Вы чему-нибудь научились?
Примечание
Есть несколько способов защиты от некоторых трюков, использованных выше.
Переопределение свободной переменной типа two или getTwo можно избежать, инкапсулируя весь блок в функцию. Либо использовать IIFE или модули:
var doubleB = (function () var two = 2
return function (n) return n * two
>
>)()
Лучшим решением было бы использование const, представленного в ES2015:
const two = 2
const doubleB = (n) => n * two
Предотвращение от злоупотребления valueOf или @@toPrimitive также возможно, но громоздко. Например, вот так:
function doubleA(n) if (typeof n !== 'number') return NaN
return n * 2
>
Можно было обойти трюк с изменением Array.prototype , только избегая таких функций и возвращаясь к циклам for (for . of) . Это уродливо, непрактично и потенциально невозможно. Абстрагирование этого или использование библиотеки имеет свои недостатки.
Не забывайте, что для того, чтобы сделать функцию действительно чистой, нужно было бы объединить все эти анти-трюки вместе. Представьте себе, как будет выглядеть эта элегантная doubleD , какая она будет длинная и как это повредит читаемости.