Технически, любая программа, которую вы выполняете на компьютере, является нечистой, потому что она в конечном итоге компилируется в инструкции типа «переместить это значение в eax
» и «добавьте это значение к содержимому eax
», которые являются нечистыми. Это не очень полезно.
Вместо этого мы думаем о чистоте, используя черные квадраты . Если какой-то код всегда выдает одни и те же выходные данные при одинаковых входных данных, то он считается чистым. По этому определению следующая функция также является чистой, хотя внутренне она использует нечистую таблицу заметок.
const fib = (() => {
const memo = [0, 1];
return n => {
if (n >= memo.length) memo[n] = fib(n - 1) + fib(n - 2);
return memo[n];
};
})();
console.log(fib(100));
Мы не заботимся о внутренних органах, потому что мы используем методологию черного ящика для проверки на чистоту. Точно так же нас не волнует, что весь код в конечном итоге преобразуется в нечистые машинные инструкции, потому что мы думаем о чистоте, используя методологию черного ящика. Внутренние органы не важны.
Теперь рассмотрим следующую функцию.
const greet = name => {
console.log("Hello %s!", name);
};
greet("World");
greet("Snowman");
Является ли функция greet
чистой или нечистой? Согласно нашей методологии черного ящика, если мы даем ему одинаковый ввод (например, World
), то он всегда выводит один и тот же вывод на экран (т.е. Hello World!
). В этом смысле разве это не чисто? Нет, это не так. Причина, по которой это не чисто, в том, что мы считаем печать чего-то на экране побочным эффектом. Если наш черный ящик вызывает побочные эффекты, то он не является чистым.
Что такое побочный эффект? Именно здесь полезна концепция ссылочной прозрачности . Если функция является ссылочно прозрачной, то мы всегда можем заменить приложения этой функции их результатами. Обратите внимание, что это не то же самое, что вставка функции .
При вставке функции мы заменяем приложения функции телом функции без изменения семантики программы. Однако ссылочно-прозрачную функцию всегда можно заменить ее возвращаемым значением без изменения семантики программы. Рассмотрим следующий пример.
console.log("Hello %s!", "World");
console.log("Hello %s!", "Snowman");
Здесь мы ввели определение greet
, и оно не изменило семантику программы.
Теперь рассмотрим следующую программу.
undefined;
undefined;
Здесь мы заменили приложения функции greet
на их возвращаемые значения, и это изменило семантику программы. Мы больше не печатаем приветствия на экране. Вот почему печать считается побочным эффектом, и поэтому функция greet
нечиста. Это не ссылочная прозрачность.
Теперь давайте рассмотрим другой пример. Рассмотрим следующую программу.
const main = async () => {
const response = await fetch("https://time.akamai.com/");
const serverTime = 1000 * await response.json();
const timeDiff = time => time - serverTime;
console.log("%d ms", timeDiff(Date.now()));
};
main();
Очевидно, что функция main
нечиста. Однако функция timeDiff
чистая или нечистая? Хотя это зависит от serverTime
, который исходит от нечистого сетевого вызова, он все еще прозрачен по ссылкам, потому что он возвращает те же выходные данные для тех же самых входов и потому что у него нет никаких побочных эффектов.
zerkms , вероятно, не согласится со мной по этому вопросу. В своем ответе он сказал, что функция dollarToEuro
в следующем примере нечиста, потому что «она транзитивно зависит от IO».
const exchangeRate = fetchFromDatabase(); // evaluates to say 0.9 for today;
const dollarToEuro = (x, exchangeRate) => {
return x * exchangeRate;
};
Я должен с ним не согласиться, потому что тот факт, что exchangeRate
поступил из базы данных, не имеет значения. Это внутренняя деталь, и наша методология черного ящика для определения чистоты функции не заботится о внутренних деталях.
В чисто функциональных языках, таких как Haskell, у нас есть запасной люк для выполнения произвольных эффектов ввода-вывода. Он называется unsafePerformIO
, и, как следует из названия, если вы не используете его правильно, это небезопасно, поскольку может нарушить прозрачность ссылок. Однако, если вы знаете, что делаете, то использовать его совершенно безопасно.
Обычно он используется для загрузки данных из файлов конфигурации в начале программы. Загрузка данных из файлов конфигурации является нечистой операцией ввода-вывода. Однако мы не хотим обременять себя передачей данных в качестве входных данных для каждой функции. Следовательно, если мы используем unsafePerformIO
, тогда мы можем загрузить данные на верхнем уровне, и все наши чистые функции могут зависеть от неизменяемых глобальных данных конфигурации.
Обратите внимание, что только потому, что функция зависит от некоторых данных, загруженных изфайл конфигурации, база данных или сетевой вызов не означает, что функция нечиста.
Однако давайте рассмотрим ваш оригинальный пример с другой семантикой.
let exchangeRate = fetchFromDatabase(); // evaluates to say 0.9 for today;
const dollarToEuro = (x) => {
return x * exchangeRate;
};
dollarToEuro(100) //90 today
dollarToEuro(100) //something else tomorrow
ЗдесьЯ предполагаю, что, поскольку exchangeRate
не определен как const
, он будет изменен во время работы программы. Если это так, то dollarToEuro
определенно является нечистой функцией, потому что при изменении exchangeRate
она нарушит прозрачность ссылок.
Однако, если переменная exchangeRate
не будет изменена и никогда не будетизменено в будущем (т. е. если это постоянное значение), то даже если оно определено как let
, оно не нарушит ссылочную прозрачность. В этом случае dollarToEuro
действительно является чистой функцией.
Обратите внимание, что значение exchangeRate
может меняться при каждом повторном запуске программы и не нарушит ссылочную прозрачность. Он нарушает прозрачность ссылок только в том случае, если он изменяется во время работы программы.
Например, если вы выполните мой пример timeDiff
несколько раз, вы получите разные значения для serverTime
и, следовательно, разные результаты. Однако, поскольку значение serverTime
никогда не изменяется во время работы программы, функция timeDiff
является чистой.