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