web-dev-qa-db-ja.com

これは純粋な機能ですか?

ほとんどの sources は、次の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

Dbから為替レートを取得すると、毎日変化すると仮定します。

これで、この関数を何度呼び出してもtoday、入力100に対して同じ出力が得られます。ただし、明日は別の出力が表示される場合があります。これが最初の条件に違反するかどうかはわかりません。

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

このような関数を純粋な関数と呼ぶことができますか。答えがNOの場合、どのようにリファクタリングして1つにできますか?

115
Snowman

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

答えが「いいえ」の場合、どのようにして関数をリファクタリングして純粋にすることができますか?

1つのオプションは、exchangeRateを渡すことです。この方法では、引数は常に(something, somethingElse)、出力はguaranteedになるsomething * somethingElse

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

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

関数型プログラミングでは、letを避ける必要があります。再割り当てを避けるために、常にconstを使用してください。

133

技術的には、コンピューターで実行するプログラムは最終的にコンパイルされ、「この値を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!)が画面に出力されます。その意味では、純粋ではないでしょうか?いいえ、ちがいます。純粋ではない理由は、画面に何かを印刷することを副作用と考えるためです。ブラックボックスが副作用を引き起こす場合、それは純粋ではありません。

副作用とは何ですか?これは、 参照透過性 の概念が有用な場所です。関数が参照的に透過的である場合、その関数のアプリケーションを常に結果に置き換えることができます。これは function inlining と同じではないことに注意してください。

関数のインライン化では、プログラムのセマンティクスを変更することなく、関数のアプリケーションを関数の本体に置き換えます。ただし、参照透過関数は、プログラムのセマンティクスを変更せずに、常に戻り値に置き換えることができます。次の例を考えてみましょう。

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 は、おそらくこの点で私に同意しないでしょう。彼は answer で、次の例の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

ここでは、exchangeRateconstとして定義されていないため、プログラムの実行中に変更されると想定しています。その場合、dollarToEuroが変更されると、参照の透過性が失われるため、exchangeRateは間違いなく不純な関数です。

ただし、exchangeRate変数が変更されておらず、今後も変更されない場合(つまり、定数値である場合)、letとして定義されている場合でも、参照の透過性は失われません。その場合、dollarToEuroは実際に純粋な関数です。

exchangeRateの値は、プログラムを再度実行するたびに変更される可能性があり、参照の透過性が損なわれないことに注意してください。プログラムの実行中に変更された場合にのみ、参照の透過性が失われます。

たとえば、timeDiffの例を複数回実行すると、serverTimeの値が異なるため、結果が異なります。ただし、プログラムの実行中にserverTimeの値が変わることはないため、timeDiff関数は純粋です。

74
Aadit M Shah

私の純粋主義者の答え(「私」は文字通り私です。この質問には単一のformal「正しい」答えがないと思うからです) :

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

その他の関連リソース:

22
zerkms

他の答えが言ったように、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の不純なバージョンは、基本的に暗黙的にdateパラメーターを取り、関数が呼び出される瞬間に設定します。同じパラメーターを2回使用します。

最新の値を取得できるdollarToEuroの純粋なバージョンが必要な場合は、履歴権限をバインドできますが、日付パラメーターをバインドせずに呼び出し元に日付を要求できます引数として、次のような結果になります:

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

dollarToEuro(100, Date.now());
14
TheHansinator

この関数は純粋ではなく、外部変数に依存しますが、外部変数はほぼ間違いなく変更されます。

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

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

これは両方の条件を満たすでしょう。

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

サンプルコード:

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

dollarToEuro(100, fetchFromDatabase())
8
Jessica

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
_

exchangeRatedollarToEuro(100)の2つの呼び出しの間に変更できない場合は、dollarToEuro(100)の最初の呼び出しの結果をメモし、最適化して削除することができます。 2回目の呼び出し。結果は同じになるので、以前の値を思い出すことができます。

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

fetchFromDatabase()自体が定数に評価される純粋な関数であり、exchangeRateが不変である場合、計算を通してずっとこの定数を畳むことができます。これを知っているコンパイラは、dollarToEuro(100)が90.0と評価し、式全体を定数90.0で置き換えるという、コメントで行ったのと同じ推論を行うことができます。

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

8
Davislor

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

指定する2つのプロパティは、両方ともconsequences参照透過性です。たとえば、次の関数_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_は不純です。

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

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

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

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

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

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

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

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

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

7
Warbo

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

4
11112222233333

このような関数を純粋な関数と呼ぶことができますか。答えがNOの場合、どのようにリファクタリングして1つにできますか?

お気付きのとおり、「明日は別の出力が表示される場合があります」。その場合、答えは圧倒的 "no"になります。これは、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 is pure」は、dollarToEuroが純粋であることを意味します(fetchFromDatabasebasisdollarToEuroに対してスカラー係数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を単体テストできます。

2
Igwe Kalu