Что такое чистая функция js
Перейти к содержимому

Что такое чистая функция js

  • автор:

«Чистые» и «нечистые» функции в 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-функций

Каждый программист знаком с функциями. В JavaScript функции отличаются множеством возможностей, что позволяет называть их «функциями высшего порядка». Но, даже если вы постоянно пользуетесь JavaScript-функциями, возможно, им есть чем вас удивить.

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

Чистые функции

Функция, которая соответствует двум следующим требованиям, называется чистой:

  • Она всегда, при вызове её с одними и теми же аргументами, возвращает один и тот же результат.
  • При выполнении функции не происходит никаких побочных эффектов.
function circleArea(radius)

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

Вот ещё один пример:

let counter = (function() < let initValue = 0 return function()< initValue++; return initValue >>)() 

Испытаем эту функцию в консоли браузера.

Испытание функции в консоли браузера

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

А вот — ещё пример:

let femaleCounter = 0; let maleCounter = 0; function isMale(user) < if(user.sex = 'man')< maleCounter++; return true >return false > 

Здесь показана функция isMale , которая, при передаче ей одного и того же аргумента, всегда возвращает один и тот же результат. Но у неё есть побочные эффекты. А именно, речь идёт об изменении глобальной переменной maleCounter . В результате эту функцию чистой назвать нельзя.

▍Зачем нужны чистые функции?

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

1. Код чистых функций понятнее, чем код обычных функций, его легче читать

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

2. Чистые функции лучше поддаются оптимизации при компиляции их кода

Предположим, имеется такой фрагмент кода:

for (int i = 0; i

Если fun — это функция, не являющаяся чистой, то во время выполнения этого кода данную функцию придётся вызвать в виде fun(10) 1000 раз.

А если fun — это чистая функция, то компилятор сможет оптимизировать код. Он может выглядеть примерно так:

let result = fun(10) for (int i = 0; i
3. Чистые функции легче тестировать

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

Вот простой пример. Чистая функция принимает в виде аргумента массив чисел и прибавляет к каждому элементу этого массива число 1, возвращая новый массив. Вот её сокращённое представление:

const incrementNumbers = function(numbers) < // . >

Для проверки такой функции достаточно написать модульный тест, напоминающий следующий:

let list = [1, 2, 3, 4, 5]; assert.equals(incrementNumbers(list), [2, 3, 4, 5, 6]) 

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

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

Функция высшего порядка — это функция, которая обладает как минимум одной из следующих возможностей:

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

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

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

const arr1 = [1, 2, 3]; const arr2 = []; for (let i = 0; i

Если же над задачей поразмыслить, то окажется, что у объектов типа Array в JavaScript есть метод map() . Этот метод вызывают в виде map(callback) . Он создаёт новый массив, заполненный элементами массива, для которого его вызывают, обработанными с помощью переданной ему функции callback .

Вот как выглядит решение этой задачи с использованием метода map() :

const arr1 = [1, 2, 3]; const arr2 = arr1.map(function(item) < return item * 2; >); console.log(arr2); 

Метод map() — это пример функции высшего порядка.

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

Кеширование результатов работы функций

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

function computed(str) < // Представим, что в этой функции проводятся ресурсозатратные вычисления console.log('2000s have passed') // Представим, что тут возвращается результат вычислений return 'a result' >

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

Как оснастить функцию кешем? Для этого можно написать особую функцию, которую можно использовать в качестве обёртки для целевой функции. Этой особой функции мы дадим имя cached . Данная функция принимает целевую функцию в виде аргумента и возвращает новую функцию. В функции cached можно организовать кеширование результатов вызова оборачиваемой ей функции с использованием обычного объекта ( Object ) или с помощью объекта, представляющего собой структуру данных Map .

Вот как может выглядеть код функции cached :

function cached(fn) < // Создаёт объект для хранения результатов, возвращаемых после каждого вызова функции fn. const cache = Object.create(null); // Возвращает функцию fn, обёрнутую в кеширующую функцию. return function cachedFn (str) < // Если в кеше нет нужного результата - вызывается функция fn if ( !cache[str] ) < let result = fn(str); // Результат, возвращённый функцией fn, сохраняется в кеше cache[str] = result; >return cache[str] > > 

Вот результаты экспериментов с этой функцией в консоли браузера.

Эксперименты с функцией, результаты работы которой кешируются

«Ленивые» функции

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

В таких условиях улучшить производительность функции можно «удаляя» эти инструкции после их первого выполнения. В результате окажется, что функции, при её последующих вызовах, не придётся производить проверки, которые будут уже ненужными. Это и будет «ленивая» функция.

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

Её код может выглядеть так:

let fooFirstExecutedDate = null; function foo() < if ( fooFirstExecutedDate != null) < return fooFirstExecutedDate; >else < fooFirstExecutedDate = new Date() return fooFirstExecutedDate; >> 

Каждый раз, когда вызывается эта функция, нужно проверить условие. Если это условие будет очень сложным, то вызовы такой функции приведут к падению производительности программы. Здесь-то мы и можем воспользоваться методикой создания «ленивых» функций для оптимизации кода.

А именно, функцию мы можем переписать так:

var foo = function() < var t = new Date(); foo = function() < return t; >; return foo(); > 

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

Это был очень простой условный пример. Давайте теперь рассмотрим нечто, более близкое к реальности.

При подключении к элементам DOM обработчиков событий нужно выполнять проверки, позволяющие обеспечить совместимость решения с современными браузерами и с IE:

function addEvent (type, el, fn) < if (window.addEventListener) < el.addEventListener(type, fn, false); >else if(window.attachEvent) < el.attachEvent('on' + type, fn); >> 

Получается, что каждый раз, когда мы вызываем функцию addEvent , в ней проверяется условие, которое достаточно проверить лишь один раз, при её первом вызове. Сделаем эту функцию «ленивой»:

function addEvent (type, el, fn) < if (window.addEventListener) < addEvent = function (type, el, fn) < el.addEventListener(type, fn, false); >> else if(window.attachEvent) < addEvent = function (type, el, fn) < el.attachEvent('on' + type, fn); >> addEvent(type, el, fn) > 

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

Каррирование функций

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

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

Какая от этого польза?

  • Каррирование помогает избежать ситуаций, когда функцию нужно вызывать, снова и снова передавая ей один и тот же аргумент.
  • Эта техника помогает создавать функции высшего порядка. Она чрезвычайно полезна при обработке событий.
  • Благодаря каррированию можно организовать предварительную подготовку функций к выполнению неких действий, а потом с удобством многократно использовать такие функции в коде.
function add(a,b,c)

Такую функцию можно вызвать, передав ей меньшее количество аргументов, чем ей нужно (правда, это приведёт к тому, что возвратит она совсем не то, чего от неё ожидают). Её можно вызывать и с большим количеством аргументов, чем предусмотрено при её создании. В подобной ситуации «лишние» аргументы будут просто проигнорированы. Вот как могут выглядеть эксперименты с подобной функцией:

add(1,2,3) --> 6 add(1,2) --> NaN add(1,2,3,4) --> 6 //Дополнительный параметр игнорируется. 

Как каррировать такую функцию?

Вот — код функции curry , которая предназначена для каррирования других функций:

function curry(fn) < if (fn.length  < if (fn.length === args.length) < return fn(. args) >else < return (. args2) => < return generator(. args, . args2) >> > return generator > 

Вот результаты экспериментов с этой функцией в браузерной консоли.

Эксперименты с функцией curry в консоли браузера

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

Предположим, надо написать функцию, которая, принимая на вход, например, строку bitfish , возвращает строку HELLO, BITFISH .

Как видно, эта функция выполняет две задачи:

  • Конкатенация строк.
  • Преобразование символов результирующей строки к верхнему регистру.
let toUpperCase = function(x) < return x.toUpperCase(); >; let hello = function(x) < return 'HELLO, ' + x; >; let greet = function(x)< return hello(toUpperCase(x)); >; 

Поэкспериментируем с ней.

Испытание функции в консоли браузера

Эта задача включает в себя две подзадачи, оформленные в виде отдельных функций. В результате код функции greet получился достаточно простым. Если бы нужно было выполнить больше операций над строками, то функция greet содержала бы в себе конструкцию наподобие fn3(fn2(fn1(fn0(x)))) .

Упростим решение задачи и напишем функцию, которая выполняет композицию других функций. Назовём её compose . Вот её код:

let compose = function(f,g) < return function(x) < return f(g(x)); >; >; 

Теперь функцию greet можно создать, прибегнув к помощи функции compose :

let greet = compose(hello, toUpperCase); greet('kevin'); 

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

Сейчас наша функция compose принимает лишь два параметра. А нам хотелось бы, чтобы она могла бы принимать любое количество параметров.

Подобная функция, способная принимать любое количество параметров, имеется в широко известной опенсорсной библиотеке underscore.

function compose() < var args = arguments; var start = args.length - 1; return function() < var i = start; var result = args[start].apply(this, arguments); while (i--) result = args[i].call(this, result); return result; >; >; 

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

Применяете ли вы в своих JavaScript-проектах какие-то особенные способы работы с функциями?

Что такое чистые функции в JavaScript?

Чистые функции — строительные блоки в функциональном программировании. Их обожают за простоту и тестируемость.

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

image

Чек-лист

Функция должна удовлетворять двум условиям, чтобы считаться «чистой»:

— Каждый раз функция возвращает одинаковый результат, когда она вызывается с тем же набором аргументов

— Нет побочных эффектов

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. Нет побочных эффектов

image

Этот тест сам по себе контрольный список.

Примеры побочных эффектов:

  1. Видоизменение входных параметров
  2. console.log
  3. HTTP вызовы (AJAX/fetch)
  4. Изменение в файловой системе
  5. Запросы 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 будут отличным выбором.

Вкратце, функция называется чистой, если она удовлетворяет двум условиям:

  1. Функция возвращает точно такой же результат каждый раз, когда она вызывается с тем же набором аргументов.
  2. Выполнение функции не изменяет какое-либо состояние за пределами её области видимости и не оказывает видимого воздействия на внешний мир, кроме возвращения значения (никаких побочных эффектов).

Иногда добавляется третье условие: «не опирается на внешнее изменяемое состояние». Это, по сути, избыточно, поскольку такая зависимость от изменяемой переменной неизбежно приведёт к нарушению первого условия.

Что из этого является чистым?

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

// 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 , какая она будет длинная и как это повредит читаемости.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *