При буквальном рассмотрении определения Википедии функция, которая принимает в качестве аргумента ссылку на изменяемые данные структура (например, собственный массив) не является чистой:
Его возвращаемое значение одинаково для одних и тех же аргументов (никаких изменений с локальными статическими переменными, нелокальными переменными, изменяемыми ссылочными аргументами или входными потоками от устройств ввода-вывода).
Эквивалентность
Хотя это ясно говорит об отсутствии изменений с изменяемыми ссылочными аргументами, мы могли бы сказать, что это открыто для интерпретации и зависит от значения слов same и variation. Возможны разные определения, и поэтому мы вступаем в область мнений. Цитата из статьи, на которую вы ссылаетесь:
На эти вопросы нет однозначно правильного ответа. Таким образом, детерминизм является параметризованным свойством: данное определение эквивалентности аргументов делает метод детерминированным, если все вызовы с эквивалентными аргументами возвращают результаты, неотличимые от внутри языка
Функциональная чистота, предложенная в той же статье, использует следующее определение эквивалентности:
Два набора ссылок на объекты считаются эквивалентными, если они приводят к идентичным графам объектов.
Таким образом, с этим определением следующие два массива считаются эквивалентными:
let a = [1];
let b = [1];
Но эту концепцию нельзя применить к JavaScript без дополнительных ограничений. Ни к Java, по которой авторы статьи ссылаются на урезанный язык, названный Joe-E:
объекты имеют идентичность: концептуально у них есть «адрес», и мы можем сравнить, указывают ли две ссылки на один и тот же «адрес» с помощью оператора ==
. Это понятие идентичности объекта может разоблачить недетерминизм.
Проиллюстрировано на JavaScript:
const compare = (array1, array2) => array1 === array2;
let arr = [1];
let a = compare(arr, arr);
let b = compare(arr, [1]);
console.log(a === b); // false
Поскольку два вызова возвращают разные результаты, даже если аргументы имеют одинаковую форму и содержание, мы должны заключить (с этим определением эквивалентности), что приведенная выше функция compare
не является чистой. В то время как в Java вы можете влиять на поведение оператора ==
(Joe-E запрещает вызывать Object.hashCode
) и таким образом избегать этого, в JavaScript это обычно невозможно при сравнении объектов.
Непреднамеренные побочные эффекты
Другая проблема заключается в том, что JavaScript не является строго типизированным, и поэтому функция не может быть уверена, что аргументы, которые она получает, соответствуют их назначению. Например, следующая функция выглядит чистой:
const add = (a, b) => a + b;
Но это может быть вызвано тем, что дает побочные эффекты:
const add = (a, b) => a + b;
let i = 0;
let obj = { valueOf() { return i++ } };
let a = add(1, obj);
let b = add(1, obj);
console.log(a === b); // false
Та же проблема существует с функцией в вашем вопросе:
const f = (arr) => arr.length;
const x = { get length() { return Math.random() } };
let a = f(x);
let b = f(x);
console.log(a === b) // false
В обоих случаях функция непреднамеренно вызвала нечистую функцию и вернула результат, который от нее зависел. Хотя в первом примере легко сделать функцию чистой с помощью проверки typeof
, для вашей функции это менее тривиально. Мы можем думать о instanceof
или Array.isArray
, или даже о какой-то умной функции deepCompare
, но тем не менее вызывающие могут установить прототип странного объекта, установить его свойство конструктора, заменить примитивные свойства на геттеры, обернуть объект в прокси, ... и т.д., и т.д. , и так одурачить даже самых умных контролеров на равенство.
Прагматизм
Поскольку в JavaScript слишком много незавершенных вопросов, нужно быть прагматичным, чтобы иметь полезное определение чистоты, иначе почти ничто не может быть помечено как чистое.
Например, на практике многие вызовут функцию типа Array#slice
чистой, даже несмотря на то, что она страдает от упомянутых выше проблем (в том числе связанных со специальным аргументом this
).
Вывод
В JavaScript при вызове функции в чистом виде вам часто приходится согласовывать контракт о том, как должна вызываться функция. Аргументы должны быть определенного типа и не иметь (скрытых) методов, которые можно было бы вызвать, но которые являются нечистыми.
Кто-то может возразить, что это противоречит идее pure, которая должна только определяться самим определением функции, а не тем, как она в конечном итоге может быть вызвана.
person
trincot
schedule
24.01.2021
f
— это чистая функция. - person Iven Marquardt   schedule 24.01.2021f
с эквивалентными аргументами в двух случаях в моем примере. - person mb21   schedule 24.01.2021x
разными, чтобы функция считалась чистой. Если это то, что делает анализатор, прекрасно. Но без этого контекста эта функция не является чистой. - person ikegami   schedule 24.01.2021f = () => []
чистым, даже еслиf() === f()
ложно, потому что[] === []
ложно. Так что вам обязательно нужно определить, что означает эквивалентность вещей в контексте того, как вы собираетесь их использовать. Также подумайте, что произойдет, если вы просто запечете мутацию в качестве получателя свойства:x = { get length() { return global++; } }
Тогдаf(x) == f(x)
может легко оказаться ложным, если заранее установитьglobal
в1
. Так чтоarr => arr.length
так же чист, какg => g()
- person Wyck   schedule 24.01.2021JSON.stringify(a) === JSON.stringify(b)
(оставляя в стороне такие случаи, как NaN и т. д.), и предположим, что никто не вмешивался в какие-либо прототипы и т. д., то использование изменяемого ссылочного аргумента кажется идеальным безопасно — если вы не используете асинхронный код — верно? - person mb21   schedule 24.01.2021_.isEqual
является еще более полезным определением того же самого на практике. - person mb21   schedule 24.01.2021