Это чистая функция?


117

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

  1. Его возвращаемое значение одинаково для тех же аргументов.
  2. Его оценка не имеет побочных эффектов.

Это первое условие, которое касается меня. В большинстве случаев это легко судить. Рассмотрим следующие функции JavaScript (как показано в этой статье )

Pure:

const add = (x, y) => x + y;

add(2, 4); // 6

Нечистая:

let x = 2;

const add = (y) => {
  return x += y;
};

add(4); // x === 6 (the first time)
add(4); // x === 10 (the second time)

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

Эту часть я получаю.


Теперь, на мой вопрос, рассмотрим эту функцию, которая конвертирует заданную сумму в долларах в евро:

(РЕДАКТИРОВАТЬ - Использование constв первой строке. Использовалось letранее непреднамеренно.)

const exchangeRate =  fetchFromDatabase(); // evaluates to say 0.9 for today;

const dollarToEuro = (x) => {
  return x * exchangeRate;
};

dollarToEuro(100) //90 today

dollarToEuro(100) //something else tomorrow

Предположим, мы выбираем обменный курс из базы данных, и он меняется каждый день.

Теперь, независимо от того, сколько раз я вызываю эту функцию сегодня , она выдаст мне один и тот же вывод для ввода 100. Однако это может дать мне другой результат завтра. Я не уверен, нарушает ли это первое условие или нет.

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

Можем ли мы назвать такие функции чистыми функциями. Если ответ НЕТ, как мы можем изменить его на один?


6
Чистота такого динамического языка, как JS, является очень сложной темой:function myNumber(n) { this.n = n; }; myNumber.prototype.valueOf = function() { console.log('impure'); return this.n; }; const n = new myNumber(42); add(n, 1);
zerkms

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

1
Чтобы узнать немного больше о том, что представляет собой побочный эффект, и с более теоретической терминологией, см. Cs.stackexchange.com/questions/116377/…
Жиль "ТАК, перестань быть злым"

3
Сегодня функция есть (x) => {return x * 0.9;}. Завтра у вас будет другая функция, которая также может быть чистой (x) => {return x * 0.89;}. Обратите внимание, что каждый раз, когда вы запускаете, (x) => {return x * exchangeRate;}он создает новую функцию, и эта функция чистая, потому что exchangeRateне может измениться.
user253751

2
Это нечистая функция. Если вы хотите сделать ее чистой, вы можете использовать ее const dollarToEuro = (x, exchangeRate) => { return x * exchangeRate; }; для чистой функции, которая Its return value is the same for the same arguments.должна храниться всегда, 1 секунда, 1 декада ... позже, несмотря ни на что
Викаш Тивари

Ответы:


133

В dollarToEuro«S возвращаемое значение зависит от внешней переменной, не аргумент; следовательно, функция нечиста.

В ответ НЕТ, как тогда мы можем реорганизовать функцию, чтобы она была чистой?

Один из вариантов - пройти exchangeRate. Таким образом, каждый раз, когда аргументы есть (something, somethingElse), выходные данные гарантированно будут something * somethingElse:

const exchangeRate =  fetchFromDatabase(); // evaluates to say 0.9 for today;

const dollarToEuro = (x, exchangeRate) => {
  return x * exchangeRate;
};

Обратите внимание, что для функционального программирования следует избегать let- всегда использовать, constчтобы избежать переназначения.


6
Отсутствие свободных переменных не является обязательным требованием, чтобы функция была чистой: const add = x => y => x + y; const one = add(42);здесь и так addи oneесть чистые функции.
zerkms

7
const foo = 42; const add42 = x => x + foo;<- это еще одна чистая функция, которая снова использует свободные переменные.
zerkms

8
@zerkms - я бы очень хотел увидеть ваш ответ на этот вопрос (даже если он просто перефразирует слова CertainPerformance для использования другой терминологии). Я не думаю, что это будет дублировать, и это будет полезно, особенно когда цитируется (в идеале, с лучшими источниками, чем статья в Википедии выше, но если это все, что мы получаем, все равно победа). (Было бы легко прочитать этот комментарий в каком-то негативном свете. Поверьте мне, что я искренен, я думаю, что такой ответ был бы замечательным и хотел бы прочитать его.)
TJ Crowder

17
Я думаю, что и вы, и @zerkms не правы. Вы, кажется, думаете, что dollarToEuroфункция в примере в вашем ответе нечиста, потому что она зависит от свободной переменной exchangeRate. Это абсурдно. Как указал zerkms, чистота функции не имеет ничего общего с тем, есть ли у нее свободные переменные. Однако, zerkms также неверен, потому что он считает, что dollarToEuroфункция нечиста, потому что она зависит от того, exchangeRateчто исходит из базы данных. Он говорит, что это нечисто, потому что «это зависит от IO транзитивно».
Аадит М Шах

9
(продолжение) Опять же, это абсурдно, потому что предполагает, что dollarToEuroэто нечисто, потому что exchangeRateэто свободная переменная. Это говорит о том, что если бы exchangeRateне было свободной переменной, т.е. если бы это был аргумент, то dollarToEuroбыло бы чисто. Следовательно, это говорит о том, что dollarToEuro(100)это нечисто, но dollarToEuro(100, exchangeRate)чисто. Это явно абсурдно, потому что в обоих случаях вы зависите exchangeRateот базы данных. Разница лишь в том, является ли exchangeRateсвободная переменная внутри dollarToEuroфункции.
Аадит М Шах

76

Технически, любая программа, которую вы выполняете на компьютере, является нечистой, потому что она в конечном итоге компилируется в инструкции типа «переместить это значение в 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функция является чистой.


3
Это было очень информативно. Спасибо. И я хотел использовать constв своем примере.
Снеговик

3
Если вы действительно хотели использовать, constто dollarToEuroфункция действительно чистая. Единственный способ exchangeRateизменить значение - это если вы снова запустите программу. В этом случае старый процесс и новый процесс различны. Следовательно, это не нарушает ссылочную прозрачность. Это как дважды вызвать функцию с разными аргументами. Аргументы могут быть разными, но внутри функции значение аргументов остается постоянным.
Аадит М Шах

3
Это звучит как небольшая теория относительности: константы только относительно постоянны, а не абсолютно, а именно относительно текущего процесса. Очевидно, что единственный правильный ответ здесь. +1.
Боб

5
Я не согласен с «нечистым», потому что он в конечном итоге компилируется в инструкции типа «переместить это значение в eax» и «добавить это значение в содержимое eax» . Если eaxочищено - с помощью загрузки или очистки - код остается детерминированным независимо от того, что еще происходит и, следовательно, чисто. В противном случае, очень полный ответ.
3Dave

3
@ Берги: На самом деле, на чистом языке с неизменными ценностями идентичность не имеет значения. Являются ли две ссылки, которые оценивают одно и то же значение, двумя ссылками на один и тот же объект или на разные объекты, можно только наблюдая, изменяя объект через одну из ссылок и наблюдая, изменяется ли значение также при получении через другую ссылку. Без мутации идентичность становится неактуальной. (Как сказал бы Рич Хикки: «Идентичность - это серия состояний во времени».)
Йорг Миттаг,

23

Ответ ме-пуриста (где «я» буквально «я», так как я думаю, что у этого вопроса нет ни одного формального «правильного» ответа):

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

Демо:

const add = (x, y) => x + y;

function myNumber(n) { this.n = n; };
myNumber.prototype.valueOf = function() {
    console.log('impure'); return this.n;
};

const n = new myNumber(42);

add(n, 1); // this call produces a side effect

Ответ меня-прагматика:

Из самого определения из Википедии

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

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

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

Теперь к вашей функции:

const exchangeRate =  fetchFromDatabase(); // evaluates to say 0.9 for today;

const dollarToEuro = (x, exchangeRate) => {
  return x * exchangeRate;
};

Это нечисто, потому что не соответствует требованию 2: оно транзитивно зависит от IO.

Я согласен с тем, что приведенное выше утверждение неверно, подробности см. В другом ответе: https://stackoverflow.com/a/58749249/251311.

Другие соответствующие ресурсы:


4
@TJCrowder meкак zerkms, который дает ответ.
zerkms

2
Да, с Javascript это все о доверии, а не о гарантиях
Боб

4
@ боб ... или это блокирующий звонок.
zerkms

1
@zerkms - Спасибо. Просто я уверен на 100%, что ключевое различие между вашим add42и моим addXчисто в том, что мое xможет быть изменено, а ваше ftне может быть изменено (и, следовательно, add42возвращаемое значение не зависит от ft)?
TJ Crowder

5
Я не согласен с тем, что dollarToEuroфункция в вашем примере нечиста. Я объяснил, почему я не согласен в своем ответе. stackoverflow.com/a/58749249/783743
Аадит М Шах

14

Как и в других ответах, как вы реализовали dollarToEuro,

let exchangeRate = fetchFromDatabase(); // evaluates to say 0.9 for today;

const dollarToEuro = (x) => { return x * exchangeRate; }; 

действительно чисто, потому что обменный курс не обновляется во время работы программы. Концептуально, однако, dollarToEuroкажется, что это должна быть нечистая функция, поскольку она использует любой самый актуальный обменный курс. Самый простой способ объяснить это несоответствие - это то, что вы не реализовали, dollarToEuroно dollarToEuroAtInstantOfProgramStart.

Ключевым моментом здесь является то, что есть несколько параметров, которые требуются для расчета конвертации валюты, и что действительно чистая версия генерала dollarToEuroпредоставит все из них. Самыми прямыми параметрами являются количество долларов США для конвертации и обменный курс. Однако, поскольку вы хотите получить свой обменный курс на основе опубликованной информации, теперь у вас есть три параметра для предоставления:

  • Сумма денег для обмена
  • Исторический авторитет для консультации по обменным курсам
  • Дата совершения транзакции (для индексации исторического авторитета)

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

function dollarToEuro(x, authority, date) {
    const exchangeRate = authority(date);
    return x * exchangeRate;
}

dollarToEuro(100, fetchFromDatabase, Date.now());

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

Если вы хотите иметь чистую версию, dollarToEuroкоторая все еще может получить наиболее актуальное значение, вы все равно можете привязать исторический авторитет, но оставить параметр даты свободным и запросить дату у вызывающей стороны в качестве аргумента, в результате чего с чем-то вроде этого:

function dollarToEuro(x, date) {
    const exchangeRate = fetchFromDatabase(date);
    return x * exchangeRate;
}

dollarToEuro(100, Date.now());

@ Снеговик Добро пожаловать! Я немного обновил ответ, чтобы добавить больше примеров кода.
TheHansinator

8

Я хотел бы немного отступить от конкретных деталей JS и абстракции формальных определений, а также поговорить о том, какие условия необходимо соблюдать, чтобы включить конкретные оптимизации. Обычно это главное, о чем мы заботимся при написании кода (хотя это также помогает доказать правильность). Функциональное программирование не является ни руководством к последней моде, ни монашеским обетом самоотречения. Это инструмент для решения проблем.

Когда у вас есть такой код:

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между двумя вызовами невозможно изменить их dollarToEuro(100), можно запомнить результат первого вызова dollarToEuro(100)и оптимизировать второй вызов. Результат будет таким же, поэтому мы можем просто запомнить значение из ранее.

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

Если fetchFromDatabase()сама по себе является чистой функцией, exchangeRateвычисляющей константу, и является неизменной, мы можем сложить эту константу на всем протяжении вычисления. Компилятор, который знает, что это так, может сделать тот же вывод, который вы сделали в комментарии, который dollarToEuro(100)оценивается как 90.0, и заменить все выражение константой 90.0.

Однако, если fetchFromDatabase()не выполняется ввод-вывод, что считается побочным эффектом, его название нарушает принцип наименьшего удивления.


8

Эта функция не является чистой, она зависит от внешней переменной, которая почти наверняка изменится.

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

Чтобы сделать эту функцию «чистой», передайте exchangeRateв качестве аргумента.

Это будет тогда удовлетворять обоим условиям.

  1. Он всегда будет возвращать одно и то же значение при передаче одного и того же значения и обменного курса.
  2. Это также не будет иметь побочных эффектов.

Пример кода:

const dollarToEuro = (x, exchangeRate) => {
  return x * exchangeRate;
};

dollarToEuro(100, fetchFromDatabase())

1
«который почти наверняка изменится» - это не так const.
zerkms

7

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

Два указанных вами свойства являются следствием ссылочной прозрачности. Например, следующая функция f1является нечистой, так как она не дает один и тот же результат каждый раз (свойство, которое вы пронумеровали 1):

function f1(x, y) {
  if (Math.random() > 0.5) { return x; }
  return y;
}

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

Допустим, мы пишем код f1("hello", "world"), запускаем его и получаем возвращаемое значение "hello". Если мы сделаем поиск / замену каждого вызова f1("hello", "world")и заменим их на, "hello"мы изменим семантику программы (теперь все вызовы будут заменены на "hello", но первоначально около половины из них были бы оценены "world"). Следовательно, призывы f1не являются ссылочно прозрачными, а значит f1нечистыми.

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

function f2(x) {
  console.log("foo");
  return x;
}

Возвращаемое значение f2("bar")всегда будет "bar", но семантика значения "bar"отличается от вызова, f2("bar")так как последний также войдет в консоль. Замена одного другим изменила бы семантику программы, поэтому она не является прозрачной по ссылкам и, следовательно f2, нечистой.

Является ли ваша dollarToEuroфункция ссылочно прозрачной (и, следовательно, чистой), зависит от двух вещей:

  • «Объем» того, что мы считаем референтно прозрачным
  • Изменится ли exchangeRateкогда-либо в пределах этого «объема»

Не существует «лучшей» области применения; обычно мы думаем об одном прогоне программы или времени жизни проекта. Как аналогия, представьте, что возвращаемые значения каждой функции кэшируются (как таблица memo в примере, приведенном @ aadit-m-shah): когда нам нужно очистить кеш, чтобы гарантировать, что устаревшие значения не будут мешать нашим семантика?

Если exchangeRateиспользовать, varто он может меняться между каждым вызовом dollarToEuro; нам нужно будет очищать любые кэшированные результаты между каждым вызовом, чтобы не было ссылочной прозрачности, о которой можно было бы говорить.

При использовании constмы расширяем область действия до запуска программы: было бы безопасно кешировать возвращаемые значения dollarToEuroдо завершения программы. Мы можем представить себе использование макроса (на языке, подобном Lisp) для замены вызовов функций их возвращаемыми значениями. Эта степень чистоты является общей для таких вещей, как значения конфигурации, параметры командной строки или уникальные идентификаторы. Если мы ограничимся размышлениями об одном прогоне программы, тогда мы получим большинство преимуществ чистоты, но мы должны быть осторожны при каждом прогоне (например, сохранение данных в файл, а затем загрузка его в другой прогон). Я бы не назвал такие функции «чистыми» в абстрактном смысле (например, если бы я писал словарное определение), но у меня не было бы проблем с обработкой их как чистых в контексте .

Если мы рассматриваем время существования проекта как нашу «область видимости», то мы являемся «наиболее прозрачными» и, следовательно, «наиболее чистыми», даже в абстрактном смысле. Нам никогда не нужно будет очищать наш гипотетический кеш. Мы могли бы даже сделать это «кэширование», напрямую переписав исходный код на диске, чтобы заменить вызовы их возвращаемыми значениями. Это могло бы работать даже в разных проектах, например, мы могли бы представить онлайновую базу данных функций и их возвращаемых значений, где любой может найти вызов функции и (если он находится в БД) использовать возвращаемое значение, предоставленное кем-то на другой стороне мир, который использовал идентичную функцию несколько лет назад в другом проекте.


4

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


2

Можем ли мы назвать такие функции чистыми функциями. Если ответ НЕТ, как мы можем изменить его на один?

Как вы должным образом заметили, «это может дать мне другой результат завтра» . Если это так, ответом будет громкое «нет» . Это особенно dollarToEuroверно, если ваше предполагаемое поведение было правильно интерпретировано как:

const dollarToEuro = (x) => {
  const exchangeRate =  fetchFromDatabase(); // evaluates to say 0.9 for today;
  return x * exchangeRate;
};

Однако существует иная интерпретация, в которой она будет считаться чистой:

const dollarToEuro = ( () => {
    const exchangeRate =  fetchFromDatabase();

    return ( x ) => x * exchangeRate;
} )();

dollarToEuro прямо выше чисто.


С точки зрения разработки программного обеспечения важно объявить зависимость от dollarToEuroфункции fetchFromDatabase. Поэтому рефакторим определение dollarToEuroследующим образом:

const dollarToEuro = ( x, fetchFromDatabase ) => {
  return x * fetchFromDatabase();
};

С этим результатом, учитывая предпосылку , что fetchFromDatabaseфункции удовлетворительно, то мы можем заключить , что проекция fetchFromDatabaseON dollarToEuroдолжна быть удовлетворительной. Или утверждение « fetchFromDatabaseчисто» подразумевает dollarToEuroчисто (поскольку fetchFromDatabaseявляется основой для dollarToEuroскалярного множителя x.

Из исходного поста я могу понять, что fetchFromDatabaseэто функция времени. Давайте улучшим работу по рефакторингу, чтобы сделать это понимание прозрачным, и, следовательно, четко определенным fetchFromDatabaseкак чистая функция:

fetchFromDatabase = (timestamp) => {/ * здесь идет реализация * /};

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

const fetchFromDatabase = ( timestamp ) => { /* here goes the implementation */ };

// Do a partial application of `fetchFromDatabase` 
const exchangeRate = fetchFromDatabase.bind( null, Date.now() );

const dollarToEuro = ( dollarAmount, exchangeRate ) => dollarAmount * exchangeRate();

Следовательно, dollarToEuroможет быть проверен модулем, просто доказав, что он правильно вызывает fetchFromDatabase(или его производная exchangeRate).


1
Это было очень поучительно. +1. Спасибо.
Снеговик

Хотя я нахожу ваш ответ более информативным, и, возможно, лучше рефакторинг для конкретного случая использования dollarToEuro; Я упомянул это в OP, что могут быть другие варианты использования. Я выбрал dollarToEuro, потому что он мгновенно вызывает то, что я пытаюсь сделать, но может быть что-то менее тонкое, что зависит от свободной переменной, которая может измениться, но не обязательно как функция времени. Имея это в виду, я считаю, что рефактор с верхним голосованием является более доступным и может помочь другим в аналогичных случаях использования. Спасибо за вашу помощь независимо.
Снеговик

-1

Я - двуязычный язык Haskell / JS, и Haskell - один из языков, который имеет большое значение для чистоты функций, поэтому я подумал, что дам вам точку зрения с точки зрения Haskell.

Как уже говорили другие, в Хаскеле чтение изменяемой переменной обычно считается нечистым. Существует разница между переменными и определениями в том, что переменные могут изменяться позже, определения остаются неизменными навсегда. Так что, если вы уже объявили его constтогда (при условии , что это просто numberи не имеет изменчивую внутреннюю структуру), чтение из этого будет использовать определение, которое является чистым. Но вы хотели смоделировать обменные курсы, изменяющиеся со временем, и это требует некоторой изменчивости, и тогда вы попадаете в нечистоту.

Чтобы описать такие виды нечистых вещей (мы можем назвать их «эффектами», а их использование «эффективными», а не «чистыми») в Haskell, мы делаем то, что вы могли бы назвать метапрограммированием . Сегодня метапрограммирование обычно относится к макросам, и это не то, что я имею в виду, а просто идея написать программу для написания другой программы в целом.

В этом случае в Haskell мы пишем чистые вычисления, которые вычисляют эффективную программу, которая затем будет делать то, что мы хотим. Таким образом, весь смысл исходного файла на Haskell (по крайней мере, тот, который описывает программу, а не библиотеку) состоит в том, чтобы описать чистые вычисления для вызываемой эффективной программы, которая производит «void» main. Затем задача компилятора Haskell состоит в том, чтобы взять этот исходный файл, выполнить эти чистые вычисления и поместить эту эффективную программу в виде двоичного исполняемого файла где-то на жестком диске, чтобы запускать его позже на досуге. Другими словами, существует разрыв между временем выполнения чистых вычислений (когда компилятор создает исполняемый файл) и временем выполнения эффективной программы (всякий раз, когда вы запускаете исполняемый файл).

Таким образом, для нас эффективные программы на самом деле являются структурой данных, и они по сути ничего не делают, просто будучи упомянутыми (они не имеют * побочных эффектов в дополнение к своему возвращаемому значению; их возвращаемое значение содержит их эффекты). Для очень легкого примера класса TypeScript, который описывает неизменяемые программы и некоторые вещи, которые вы можете сделать с ними,

export class Program<x> {
   // wrapped function value
   constructor(public run: () => Promise<x>) {}
   // promotion of any value into a program which makes that value
   static of<v>(value: v): Program<v> {
     return new Program(() => Promise.resolve(value));
   }
   // applying any pure function to a program which makes its input
   map<y>(fn: (x: x) => y): Program<y> {
     return new Program(() => this.run().then(fn));
   }
   // sequencing two programs together
   chain<y>(after: (x: x) => Program<y>): Program<y> {
    return new Program(() => this.run().then(x => after(x).run()));
   }
}

Ключ в том, что если у вас есть, Program<x>то никаких побочных эффектов не произошло, и это полностью функционально чистые объекты. Отображение функции на программу не имеет побочных эффектов, если только функция не была чистой; последовательность двух программ не имеет побочных эффектов; и т.п.

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

// assuming a database library in knex, say
function getUserById(id: number): Program<{ id: number, name: string, supervisor_id: number }> {
    return new Program(() => knex.select('*').from('users').where({ id }));
}
function notifyUserById(id: number, message: string): Program<void> {
    return new Program(() => knex('messages').insert({ user_id: id, type: 'notification', message }));
}
function fetchJSON(url: string): Program<any> {
  return new Program(() => fetch(url).then(response => response.json()));
}

а затем вы могли бы описать работу cron, чтобы свернуть URL-адрес, найти какого-нибудь сотрудника и уведомить его руководителя в чисто функциональном виде, как

const action =
  fetchJSON('http://myapi.example.com/employee-of-the-month')
    .chain(eotmInfo => getUserById(eotmInfo.id))
    .chain(employee => 
        getUserById(employee.supervisor_id)
          .chain(supervisor => notifyUserById(
            supervisor.id,
            'Your subordinate ' + employee.name + ' is employee of the month!'
          ))
    );

Дело в том, что каждая функция здесь является полностью чистой функцией; на самом деле ничего не произошло, пока я фактически не привел action.run()его в движение. Кроме того, я могу написать такие функции, как,

// do two things in parallel
function parallel<x, y>(x: Program<x>, y: Program<y>): Program<[x, y]> {
    return new Program(() => Promise.all([x.run(), y.run()]));
}

и если бы у JS было обещание отмены, мы могли бы поспорить между двумя программами, взять первый результат и отменить второй. (Я имею в виду, что мы все еще можем, но становится менее понятно, что делать.)

Точно так же в вашем случае мы можем описать изменение обменного курса с

declare const exchangeRate: Program<number>;

function dollarsToEuros(dollars: number): Program<number> {
  return exchangeRate.map(rate => dollars * rate);
}

и exchangeRateможет быть программа, которая смотрит на изменяемое значение,

let privateExchangeRate: number = 0;
export function setExchangeRate(value: number): Program<void> {
  return new Program(() => { privateExchangeRate = value; return Promise.resolve(undefined); });
}
export const exchangeRate: Program<number> = new Program(() => {
  return Promise.resolve(privateExchangeRate); 
});

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

Цена, конечно, заключается в том, что вы должны в конечном итоге назвать это .run() где-то , и это будет нечистым. Но вся структура ваших вычислений может быть описана чисто вычислениями, и вы можете отодвинуть примеси на полях вашего кода.


Мне любопытно, почему за это продолжают понижать голос, но я имею в виду, что я все еще поддерживаю его (на самом деле, это то, как вы манипулируете программами на Haskell, где все чисто по умолчанию) и с радостью подавит отрицательные голоса. Тем не менее, если пользователи хотят оставлять комментарии, объясняющие, что им не нравится в этом, я могу попытаться улучшить это.
ЧР Дрост

Да, мне было интересно, почему так много отрицательных голосов, но нет ни одного комментария, кроме, конечно, автора.
Буда Орс
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.