Я двомовний 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()
кудись , і це буде нечисто. Але всю структуру вашого обчислення можна описати чистим обчисленням, і ви можете підштовхнути домішки до поля вашого коду.
function myNumber(n) { this.n = n; }; myNumber.prototype.valueOf = function() { console.log('impure'); return this.n; }; const n = new myNumber(42); add(n, 1);