これは純粋な関数ですか?


117

ほとんどのソースは、純粋な関数を次の2つのプロパティを持つものとして定義します。

  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番目の関数が後続の呼び出しに対して異なる出力を提供するため、最初の条件に違反することは簡単にわかります。したがって、それは不純です。

私が手に入れるこの部分。


さて、私の質問のために、ドルで与えられた金額をユーロに変換するこの関数を考えてください:

(編集- 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。ただし、明日は別の出力になるかもしれません。これが最初の条件に違反しているかどうかはわかりません。

IOW、関数自体には入力を変更するロジックは含まれていませんが、将来変更される可能性のある外部定数に依存しています。この場合、毎日変化することは確実です。他の場合では、それが起こるかもしれません。そうではないかもしれません。

そのような関数を純粋な関数と呼ぶことができますか?答えがNOの場合、それを1にリファクタリングするにはどうすればよいですか?


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 /…を
Gilles 'SO- stop

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ディケードを保持する必要があります。後で何をしても
Vikash Tiwari

回答:


133

dollarToEuro戻り値は、引数ではありません外の変数に依存します。したがって、関数は不純です。

答えが「いいえ」の場合、関数を純粋にリファクタリングするにはどうすればよいですか?

1つのオプションは、を渡すことです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-私はこの質問に対するあなたの答えを見てみたいと思います(たとえそれが特定のパフォーマンスを異なる用語を使用するように言い換えたとしても)。私はそれが重複しているとは思いませんし、特に引用されている場合、それは明るいものになると思います(理想的には、上記のWikipediaの記事よりも優れたソースを使用しますが、それで十分なら、それでも勝利します)。(このコメントはある種の否定的な見方で読むのは簡単でしょう。私が本物であると信じてください。そのような答えは素晴らしいと思いますし、読みたいと思います。)
TJ Crowder

17
あなたと@zerkmsの両方が間違っていると思います。あなたのdollarToEuro答えの例の関数は自由変数に依存しているので不純だと思っているようですexchangeRate。それはばかげています。zerkmsが指摘したように、関数の純粋さは、自由変数があるかどうかには関係ありません。ただし、dollarToEuro関数がexchangeRateデータベースからのものに依存しているため、関数が不純であると考えているため、zerkmsも誤りです。彼は「それは推移的にIOに依存する」ので不純であると言います。
Aadit M Shah

9
(続き)繰り返しになりますが、は自由変数であるdollarToEuroため不純であることを示唆しているので、それはばかげていexchangeRateます。exchangeRateもしそれが自由変数でないなら、つまりそれが引数なら、それdollarToEuroは純粋であることを示唆しています。したがって、それdollarToEuro(100)は不純ですdollarToEuro(100, exchangeRate)が純粋です。どちらの場合もexchangeRate、データベースからのものに依存しているため、それは明らかにばかげています。唯一の違いはexchangeRatedollarToEuro関数内の自由変数かどうかです。
Aadit M Shah

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のような純粋に関数型の言語では、任意のIO効果を実行するためのエスケープハッチがあります。それは呼ばれていますunsafePerformIO、その名前が正しく使用しない場合は、参照の透明性が損なわれる可能性があるため安全ではありません。ただし、何をしているのかわかっている場合は、安全に使用できます。

これは通常、プログラムの最初の方にある構成ファイルからデータをロードするために使用されます。構成ファイルからデータをロードすることは、不純なIO操作です。ただし、すべての関数への入力としてデータを渡すことによる負担がかかりたくありません。したがって、使用する場合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
使用するつもりならconstdollarToEuro関数は確かに純粋です。の値exchangeRateが変わる唯一の方法は、プログラムを再度実行した場合です。その場合、古いプロセスと新しいプロセスは異なります。したがって、参照の透明性を損なうことはありません。これは、異なる引数で関数を2回呼び出すようなものです。引数は異なる場合がありますが、関数内では引数の値は一定のままです。
Aadit M Shah

3
これは、相対性理論についての小さな理論のように聞こえます。定数は絶対ではなく、比較的一定です。つまり、実行中のプロセスに対して相対的です。明らかに、ここで唯一正しい答えです。+1。
ボブ

5
「この値をeaxに移動する」や「この値をeaxのコンテンツに追加する」などの指示に最終的にコンパイルされるため、不純であることに同意しません。eaxロードまたはクリアによってクリアされた場合、コードは、他に何が起こっているか、したがって純粋です。それ以外の場合は、非常に包括的な答えです
3Dave

3
@Bergi:実際、不変の値を持つ純粋な言語では、アイデンティティは無関係です。同じ値に評価される2つの参照が同じオブジェクトまたは異なるオブジェクトへの2つの参照であるかどうかは、一方の参照を介してオブジェクトを変更し、もう一方の参照を介して取得したときに値も変化するかどうかを確認することによってのみ確認できます。変異がなければ、アイデンティティは無関係になります。(Rich Hickeyが言うように:Identityは時を経た一連の状態です。)
JörgW Mittag

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. その戻り値は、同じ引数でも同じです(ローカル静的変数、非ローカル変数、可変参照引数、またはI / Oデバイスからの入力ストリームのバリエーションはありません)。
  2. その評価には副作用がありません(ローカル静的変数、非ローカル変数、可変参照引数、またはI / Oストリームの変更はありません)。

言い換えれば、それが実装された方法ではなく、関数がどのように動作するかが重要になります。また、特定の関数がこれらの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
me答えを提供するzerkms としての@TJCrowder。
zerkms

2
ええ、Javascriptの場合、それは保証ではなく、すべて信頼に関するものです
bob

4
@bob ...またはそれはブロッキング呼び出しです。
zerkms

1
@zerkms-ありがとう。私が100%確信しているように、あなたadd42と私の主な違いは、私addXx変更される可能性があり、あなたftが変更できないことです(したがって、add42'の戻り値はに基づいて変化しませんft)?
TJクラウダー

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がそれらすべてを提供することです。最も直接的なパラメータは、変換する米ドルの金額と為替レートです。ただし、公開された情報から為替レートを取得するため、次の3つのパラメーターを指定できます。

  • 両替金額
  • 為替レートについて相談する歴史的権威
  • トランザクションが行われた日付(歴史的機関に索引を付けるため)

ここでの歴史的権威はあなたのデータベースであり、データベースが危険にさらされていないと仮定すると、特定の日の為替レートに対して常に同じ結果を返します。したがって、これらの3つのパラメーターを組み合わせることで、次のような完全に純粋な自己完結型の一般的なdollarToEuroを作成できます。

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

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

実装では、関数が作成された瞬間の履歴権限とトランザクションの日付の両方の定数値をキャプチャします-履歴権限はデータベースであり、キャプチャされた日付はプログラムを開始した日付です-残っているのはドルの金額だけです、発信者が提供します。dollarToEuro常に最新の値を取得する不純なバージョンは、基本的に日付パラメーターを暗黙的に取得し、関数が呼び出された瞬間にそれを設定します。これは、同じパラメーターで関数を2回呼び出すことができないため、純粋ではありません。

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への2つの呼び出しの間に変更できない場合dollarToEuro(100)は、最初の呼び出しの結果をメモしdollarToEuro(100)、2番目の呼び出しを最適化することができます。結果は同じになるので、以前の値を思い出せます。

exchangeRateそれをルックアップする任意の関数を呼び出す前に、一度設定されていない、と決して変更される場合があります。それほど制限的ではありませんがexchangeRate、特定の関数またはコードブロックを1回検索し、そのスコープ内で一貫して同じ為替レートを使用するコードがある場合があります。または、このスレッドのみがデータベースを変更できる場合、為替レートを更新しなかった場合、他の誰もあなたにそれを変更していないと想定する権利があります。

場合はfetchFromDatabase()、それ自体定数を求める純粋な関数であり、かつexchangeRate不変である、我々はこの定数を計算介してすべての道を折ることができます。これが事実であることを知っているコンパイラーは、コメントで行ったのと同じ演繹を行い、dollarToEuro(100)評価は90.0となり、式全体を定数90.0に置き換えることができます。

ただし、fetchFromDatabase()が副作用と見なされるI / Oを実行しない場合、その名前は最小驚きの原則に違反します。


8

この関数は純粋ではなく、ほぼ確実に変更される外部変数に依存しています。

したがって、関数は最初に行ったポイントに失敗します。同じ引数に対して同じ値を返しません。

この関数を「純粋」にするexchangeRateには、引数として渡します。

これは、両方の条件を満たすことになります。

  1. 同じ値と為替レートを渡すと、常に同じ値を返します。
  2. 副作用もありません。

コード例:

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

dollarToEuro(100, fetchFromDatabase())

1
「これはほぼ間違いなく変わるでしょう」---それはそうではありません、それはconstです。
zerkms

7

参照の透明性に関して他の人が行ったポイントを拡張するには、純粋さを関数呼び出しの参照の透明性として定義できます(つまり、プログラムのセマンティクスを変更せずに、関数へのすべての呼び出しを戻り値で置き換えることができます)。

指定する2つのプロパティは、どちらも参照透過性の結果です。たとえば、次の関数f1は不正確です。毎回同じ結果が得られるわけではないためです(番号を付けたプロパティ1)。

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

毎回同じ結果を得ることが重要なのはなぜですか?異なる結果を取得することは、関数呼び出しが値と異なるセマンティクスを持つための1つの方法であり、それによって参照の透過性を壊します。

コードを記述しf1("hello", "world")て実行し、戻り値を取得するとします"hello"。すべての呼び出しの検索/置換を行い、f1("hello", "world")それらを置き換える"hello"と、プログラムのセマンティクスが変更されます(すべての呼び出しはに置き換えられますが"hello"、元々、それらの約半分がに評価されます"world")。したがって、への呼び出しf1は参照透過的ではないためf1、不純です。

関数呼び出しが値に対して異なるセマンティクスを持つことができるもう1つの方法は、ステートメントを実行することです。例えば:

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

の戻り値f2("bar")は常にになり"bar"ます"bar"が、呼び出しf2("bar")もコンソールに記録されるため、値のセマンティクスは呼び出しとは異なります。一方を他方に置き換えると、プログラムのセマンティクスが変更されるため、参照に対して透過的ではないため、f2不純です。

dollarToEuro関数が参照透過的(したがって純粋)であるかどうかは、次の2つの要素に依存します。

  • 参照的に透明であると考えるものの「スコープ」
  • exchangeRateその「範囲」内で変化するかどうか

使用する「最良の」スコープはありません。通常、プログラムの1回の実行、またはプロジェクトの存続期間について考えます。類推として、すべての関数の戻り値がキャッシュされると想像してください(@ aadit-m-shahによって与えられた例のメモテーブルのように)。意味論?

exchangeRate使用している場合は、をvar呼び出すたびに変わる可能性がありdollarToEuroます。各呼び出しの間にキャッシュされた結果をクリアする必要があるので、参照する透明性はありません。

を使用しconstて、プログラムの実行に「スコープ」を拡張しますdollarToEuro。プログラムが終了するまで戻り値をキャッシュしても安全です。(Lispのような言語の)マクロを使用して、関数呼び出しを戻り値で置き換えることを想像できます。この純度は、構成値、コマンドラインオプション、一意のIDなどに共通です。プログラムの1つの実行について考えることに限定すると、純粋性の利点のほとんどを得ることができますが、実行全体で注意する必要があります(たとえば、データをファイルに保存してから、別の実行で読み込む)。私はそのような関数を抽象的な意味で「純粋」とは呼びませんが(たとえば、辞書の定義を書いている場合)、コンテキスト内でそれらを純粋なものとして扱うことに問題はありません。

プロジェクトの存続期間を「スコープ」として扱うと、抽象的な意味でも、「参照的に最も透明」であり、したがって「最も純粋」になります。仮想キャッシュをクリアする必要はありません。ディスク上のソースコードを直接書き換えて、呼び出しを戻り値に置き換えることで、この「キャッシュ」を行うこともできます。これはプロジェクト全体でも機能ます。たとえば、関数とその戻り値のオンラインデータベースを想像できます。この関数では、誰でも関数呼び出しを検索し、(DBにある場合)反対側の誰かから提供された戻り値を使用できます。数年前に別のプロジェクトで同じ機能を使用した世界。


4

書かれているように、それは純粋な関数です。副作用はありません。関数には1つの仮パラメーターがありますが、2つの入力があり、2つの入力に対して常に同じ値を出力します。


2

そのような関数を純粋な関数と呼ぶことができますか?答えがNOの場合、それを1にリファクタリングするにはどうすればよいですか?

お気づきのように、「明日は違う出力になるかもしれません」。その場合、答えは「いいえ」です。これは、意図した動作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は関数の純粋性を重要視する言語の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() どこかで呼び出さなければならず、それは不純です。しかし、計算の全体的な構造は純粋な計算で表すことができ、不純物をコードのマージンに押しやることができます。


なぜこれがダウン投票され続けるのか興味がありますが、私はまだそれを待機している(つまり、デフォルトで純粋なHaskellでプログラムを操作する方法です)ので、ダウン投票は喜んで行われます。それでも、反対投票者が嫌いなことを説明するコメントを残したい場合は、改善することができます。
CR Drost

ええ、なぜ著者のほかに、なぜそれほど多くの反対票があり、単一のコメントがないのでしょうか。
ブダÖrs19年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.