私はHaskell / JSバイリンガルであり、Haskellは関数の純粋性を重要視する言語の1つであるため、Haskellがそれをどのように見るかについて、あなたに視点を与えたいと思いました。
他の人が言ったように、Haskellでは、可変変数を読み取ることは一般に不純であると考えられています。変数と定義には違いがあります。変数は後で変更でき、定義は永久に同じです。したがって、それを宣言したconst
場合(それがであり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>
副作用が発生せず、これらは完全に機能的に純粋なエンティティであることです。プログラムに関数をマッピングしても、その関数が純粋な関数でない限り、副作用はありません。2つのプログラムのシーケンス処理には副作用がありません。等
したがって、これをあなたのケースに適用する方法の例として、IDでユーザーを取得し、データベースを変更して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がキャンセルを約束した場合、2つのプログラムが互いに競合し、最初の結果を取り、2番目のプログラムをキャンセルすることができます。(まだできるということですが、どうすればよいかがはっきりしなくなります。)
同様に、あなたのケースでは、為替レートの変化を
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);