純粋関数の利点


81

今日、私は純粋関数について読んでいて、その使用法に混乱しました:

関数が同じ入力セットに対して同じ値セットを返し、観察可能な副作用がない場合、関数は純粋であると言われます。

たとえば、strlen()は純粋関数rand()ですが、は不純な関数です。

__attribute__ ((pure)) int fun(int i)
{
    return i*i;
}

int main()
{
    int i=10;
    printf("%d",fun(i));//outputs 100
    return 0;
}

http://ideone.com/33XJU

上記のプログラムは、pure宣言がない場合と同じように動作します。

関数をpure[出力に変更がない場合]として宣言する利点は何ですか?


7
はい-生成されたアセンブリを確認します。
フィリップケンダル

4
この純粋性の定義は正しいとは思いません。printfたとえば、資格があります(同じ引数で2回呼び出すと、同じ戻り値が得られます)が、純粋ではありません。
tdammers 2012年

14
@tdammers:確かに、それはその...and no side-effects...部分を欠いています。
Frerich Raabe 2012

2
@ベン:エントロピーはどこから来るのですか?ここでは(理論的に)決定性マシンを扱っています。真のエントロピーをそれらに取り込む唯一の方法は、外部ソースからのものです。これは副作用を意味します。もちろん、プログラミング言語が非決定論的関数を定義できるようにすることもできます。技術的な副作用はなく、関数は実際には非決定論的です。しかし、そうすると、純度を追跡することの実際的な利点のほとんどが失われます。
tdammers 2012年

3
tdammersは正しいです-上記のpureの定義は正しくありません。純粋とは、出力が関数への入力のみに依存することを意味します。また、観察可能な副作用があってはなりません。「同じ入力に対して同じ出力」は、これらの要件の非常に不正確な要約です。en.wikipedia.org/wiki/Pure_function
Dancrumb 2012年

回答:


144

pure 関数について特定の最適化を行うことができることをコンパイラーに知らせます。次のようなコードを想像してください。

for (int i = 0; i < 1000; i++)
{
    printf("%d", fun(10));
}

純粋関数を使用すると、コンパイラーfun(10)は、1000回ではなく、1回だけ評価する必要があることを知ることができます。複雑な機能の場合、それは大きな勝利です。



@mobどういう意味ですか?何故なの?
コンラッドルドルフ

15
入力(文字列が始まるアドレスへのポインタ)を変更せずに文字列(あるアドレスから始まる文字のシーケンス)を変更できるため、つまり、メモ化することはできません。これは、不変の文字列を持つ言語(Javaなど)の純粋関数にすぎません。
暴徒

5
@KonradRudolph:長さ1000の文字列を想像してみてください。それを呼び出しstrlenます。また。同じことはい?次に、2番目の文字をに変更します\0strlenまだ今1000年を返しますか?開始アドレスは同じです(==入力は同じです)が、関数は異なる値を返すようになりました。
マイクベイリー2012年

5
@mobそれは良い異議です、明らかにあなたは正しいです。さえstrlen(GCC / glibcで)実際には純粋であると主張しているという事実に私は誤解されました。しかし、glibcの実装を見ると、これが間違っていることがわかりました。
コンラッドルドルフ

34

関数が「純粋」であると言うときは、外部から見える副作用がないことを保証します(そして、コメントが言うように、嘘をつくと、悪いことが起こる可能性があります)。関数が「純粋」であることを知ることは、特定の最適化を行うためにこの知識を使用できるコンパイラーにとって利点があります。

ここでは何であるGCCの文書は約言うpure属性:

ピュア

多くの関数は戻り値を除いて効果がなく、それらの戻り値はパラメーターやグローバル変数のみに依存します。このような関数は、算術演算子と同じように、共通部分式除去とループ最適化の対象になります。これらの関数は、pure属性を使用して宣言する必要があります。例えば、

          int square (int) __attribute__ ((pure));

フィリップの答えは、関数が「純粋」であることを知ることがループ最適化にどのように役立つかをすでに示しています。

これは一般的な部分式除去のためのものです(与えられたものfooは純粋です):

a = foo (99) * x + y;
b = foo (99) * x + z;

になることができる:

_tmp = foo (99) * x;
a = _tmp + y;
b = _tmp + z;

3
これを行うかどうかはわかりませんが、純粋関数を使用すると、関数が呼び出されたときにコンパイラーが並べ替えることもできます。並べ替えが有益であると見なされた場合です。副作用の可能性がある場合、コンパイラーはより保守的である必要があります。
mpdonadio 2012年

@ MPD-はい、それは合理的に聞こえます。また、call命令はスーパースカラーCPUのボトルネックであるため、コンパイラーの助けが役立つ場合があります。
ArjunShankar 2012年

数年前にDSPコンパイラを使用して、この手法を使用してより早く/後で戻り値を取得することを漠然と思い出します。これにより、パイプラインストールを最小限に抑えることができました。
mpdonadio 2012年

1
99はconstであり、fooは常に同じ結果を返すため、「foo(99)」を事前に計算できますか?たぶん、ある種の2段階コンパイルで?
markwatson 2012年

1
@ markwatson-わかりません。それが単に不可能な場合もあります。たとえば、fooが別のコンパイルユニット(別のCファイル)の一部である場合、またはプリコンパイルされたライブラリ内にある場合。どちらの場合も、コンパイラーは何をするのかわからfooず、事前に計算することもできません。
ArjunShankar 2012年

28

実行時の利点に加えて、純粋関数はコードを読むときに推論するのがはるかに簡単です。さらに、戻り値はパラメーターの値にのみ依存することがわかっているため、純粋関数をテストする方がはるかに簡単です。


2
+1、テストについてのあなたのポイントは興味深いものです。セットアップや分解は必要ありません。
ArjunShankar 2012年

15

非純粋関数

int foo(int x, int y) // possible side-effects

純粋関数の拡張のようなものです

int bar(int x, int y) // guaranteed no side-effects

ここでは、明示的な関数の引数x、yに加えて、宇宙の残りの部分(またはコンピューターが通信できるもの)を暗黙の潜在的な入力として持っています。同様に、明示的な整数の戻り値に加えて、コンピューターが書き込むことができるものはすべて、暗黙的に戻り値の一部です。

非純粋関数よりも純粋関数について推論する方がはるかに簡単である理由は明らかです。


1
+1:潜在的な入力として宇宙を使用することは、純粋と非純粋の違いを説明する非常に良い方法です。
ArjunShankar 2012年

確かに、これはモナドの背後にある考え方です。
クリストファーミシンスキー2012年

7

アドオンと同じように、C ++ 11はconstexprキーワードを使用して物事を成文化していることを述べておきます。例:

#include <iostream>
#include <cstring>

constexpr unsigned static_strlen(const char * str, unsigned offset = 0) {
        return (*str == '\0') ? offset : static_strlen(str + 1, offset + 1);
}

constexpr const char * str = "asdfjkl;";

constexpr unsigned len = static_strlen(str); //MUST be evaluated at compile time
//so, for example, this: int arr[len]; is legal, as len is a constant.

int main() {
    std::cout << len << std::endl << std::strlen(str) << std::endl;
    return 0;
}

constexprの使用に関する制限により、関数が確実に純粋になるようになっています。このようにして、コンパイラーはより積極的に最適化し(末尾再帰を使用するようにしてください!)、実行時ではなくコンパイル時に関数を評価できます。

したがって、あなたの質問に答えるには、C ++を使用している場合(Cと言ったことは知っていますが、それらは関連しています)、純粋関数を正しいスタイルで記述することで、コンパイラーは関数を使用してあらゆる種類のクールなことを実行できます。 -)


4

一般に、純粋関数には、コンパイラーが利用できる純粋関数に比べて3つの利点があります。

キャッシング

f100000回呼び出されている純粋関数があるとしましょう。これは決定論的であり、パラメーターのみに依存するため、コンパイラーはその値を1回計算し、必要に応じて使用できます。

並列処理

純粋関数は共有メモリに対して読み取りまたは書き込みを行わないため、予期しない結果を招くことなく、別々のスレッドで実行できます。

参照による通過

関数f(struct t)t値によって引数を取得しますが、一方、コンパイラは、の値が変更されず、パフォーマンスが向上することを保証しながら、純粋であると宣言されtているfかどうかを参照して渡すことができます。t


コンパイル時の考慮事項に加えて、純粋関数はかなり簡単にテストできます。それらを呼び出すだけです。

オブジェクトを構築したり、DB /ファイルシステムへのモック接続を作成したりする必要はありません。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.