Це чиста функція?


117

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

  1. Його повернене значення однакове для тих же аргументів.
  2. Його оцінка не має побічних ефектів.

Це перша умова, яка мене стосується. У більшості випадків судити легко. Розглянемо наступні функції JavaScript (як показано в цій статті )

Чисто:

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-а функція дасть різні виходи для наступних дзвінків, тим самим порушуючи першу умову. А значить, це нечисто.

Ця частина я отримую.


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

(EDIT - використання 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

Припустимо, ми отримуємо обмінний курс з db, і він змінюється щодня.

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

IOW, сама функція не містить ніякої логіки для вимкнення вводу, але вона покладається на зовнішню константу, яка може змінитися в майбутньому. У цьому випадку абсолютно впевнено, що вона буде змінюватися щодня. В інших випадках це може статися; це не може.

Чи можемо ми називати такі функції чистими функціями. Якщо відповідь "НІ", то як тоді ми можемо перетворити його на один?


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 / ...
Жиля SO- перестати бути злом "

3
Сьогодні функція є (x) => {return x * 0.9;}. Завтра у вас буде інша функція, яка також може бути чистою, можливо (x) => {return x * 0.89;}. Зауважте, що кожен раз, коли ви запускаєте, (x) => {return x * exchangeRate;}вона створює нову функцію, і ця функція є чистою, тому що exchangeRateне може змінитися.
користувач253751

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яка походить з бази даних. Він каже, що це нечисто, тому що "це залежить від транзитивного транзиту".
Аадіт М Шах

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функція в наступному прикладі є нечистою, оскільки "це залежить від транзитивного введення".

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

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

Я з цим не погоджуюся, оскільки той факт, що exchangeRateприйшов із бази даних, не має значення. Це внутрішня деталь, і наша методологія чорного поля для визначення чистоти функції не стосується внутрішніх деталей.

У чисто функціональних мовах, таких як Haskell, у нас є випускний люк для виконання довільних ефектів IO. Це називається 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
@ Бергі: Насправді, в чистій мові з незмінними значеннями ідентичність не має значення. Незалежно від того, два посилання, які оцінюють одне і те ж значення, є двома посиланнями на один і той самий об'єкт або на різні об'єкти, можна спостерігати лише за допомогою мутації об'єкта через одну з посилань та спостереження, чи змінюється також значення при отриманні через інше посилання. Без мутації ідентичність стає неактуальною. (Як сказав Річ Хікі: Ідентичність - це серія держав у часі.)
Йорг W Міттаг

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: це залежить від транзитивного переходу.

Я погоджуюсь, що твердження вище неправильне, детальну інформацію див. В іншій відповіді: https://stackoverflow.com/a/58749249/251311

Інші відповідні ресурси:


4
@TJCrowder meяк zerkms, який надає відповідь.
zerkms

2
Так, у Javascript все стосується впевненості, а не гарантій
боб

4
@bob ... або це блокування дзвінка.
zerkms

1
@zerkms - спасибі Так що я на 100% впевнений, що ключова різниця між вашим add42і моїм addXполягає лише в тому, що моє xможе бути змінено, а ваше ftнеможливо змінити (і, таким чином, add42повернене значення не змінюється залежно від ft)?
TJ Crowder

5
Я не погоджуюся, що dollarToEuroфункція у вашому прикладі нечиста. Я пояснив, чому не згоден у своїй відповіді. stackoverflow.com/a/58749249/783743
Aadit M Shah

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());

@Snowman Вітаємо Вас! Я трохи оновив відповідь, щоб додати більше прикладів коду.
TheHansans

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зміниться коли-небудь волевиявлення в межах цієї сфери

Не існує "найкращого" простору для використання; зазвичай ми думаємо про один цикл програми або про термін експлуатації проекту. Як аналогія, уявіть, що зворотні значення кожної функції отримують кешування (як таблиця нагадувань у прикладі, поданий @ 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функціонує задовільно, то можна зробити висновок, що проекція на fetchFromDatabaseдалі dollarToEuroповинна бути задовільною. Або твердження " fetchFromDatabaseчистий" означає, що dollarToEuroце чисте (оскільки fetchFromDatabaseє підставою для dollarToEuroскалярного коефіцієнта x.

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

fetchFromDatabase = (мітка часу) => {/ * тут іде реалізація * /};

Зрештою, я б перетворив цю функцію наступним чином:

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; Я згадував це в ОП, що можуть бути й інші випадки використання. Я вибрав dollarToEuro, тому що це моментально викликає те, що я намагаюся зробити, але може бути щось менш тонке, що залежить від вільної змінної, яка може змінюватися, але не обов'язково як функція часу. Зважаючи на це, я вважаю, що рефактор з найбільшою популярністю є найбільш доступним та тим, що може допомогти іншим у подібних випадках використання. Дякуємо за вашу допомогу незалежно.
Сніговик

-1

Я двомовний Haskell / JS, і Haskell є однією з мов, яка робить велику справу щодо чистоти функції, тому я подумав, що я дам вам перспективу з того, як Haskell це бачить.

Як уже сказано, в Haskell, читання змінюваний змінної зазвичай вважається нечистою. Існує різниця між змінними та визначеннями в тому, що змінні можуть змінюватися пізніше, визначення назавжди однакові. Тож якби ви тоді це заявили const(якщо припустити, що це просто a numberі не має внутрішньої структури, що змінюється), для читання цього було б використання визначення, яке є чистим. Але ви хотіли моделювати курси валют, що змінюються з часом, і це вимагає певної змінності, і тоді ви потрапляєте в домішки.

Для опису таких видів нечистих речей (ми можемо називати їх "ефекти", а також їх використання "ефективним" на відміну від "чистого") в Haskell, ми робимо те, що ви можете назвати метапрограмуванням . Сьогодні метапрограмування зазвичай стосується макросів які я не маю на увазі, а просто ідеї написання програми для написання іншої програми взагалі.

У цьому випадку в Haskell ми пишемо чисте обчислення, яке обчислює ефективну програму, яка потім буде робити те, що ми хочемо. Тому вся суть вихідного файлу Haskell (принаймні, такого, який описує програму, а не бібліотеку) полягає в описі чистого обчислення для ефективної програми, яка виробляє-недійсна, називається 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, де речі за замовчуванням є чистими) і з радістю почнеш протидіяти. І все-таки, якщо судноводці захотіли залишити коментарі з поясненням того, що їм не подобається в цьому, я можу спробувати покращити його.
CR Drost

Так, мені було цікаво, чому так багато посилань, але жодного коментаря, окрім автора.
Буда Örs
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.