関数ポインタ、クロージャ、ラムダ


86

私はちょうど今関数ポインタについて学んでいて、この主題に関するK&Rの章を読んでいたとき、最初に私を襲ったのは「ねえ、これはちょっと閉鎖のようなものだ」でした。私はこの仮定がどういうわけか根本的に間違っていることを知っていました、そしてオンラインで検索した後、私はこの比較の分析を実際には見つけませんでした。

では、なぜCスタイルの関数ポインターがクロージャーやラムダと根本的に異なるのでしょうか。私が知る限り、それは、関数を匿名で定義する慣行とは対照的に、関数ポインターがまだ定義された(名前付き)関数を指しているという事実と関係があります。

関数を関数に渡すことは、名前が付けられていない2番目のケースでは、渡される通常の日常の関数である最初のケースよりも強力であると見なされるのはなぜですか?

2つを非常に密接に比較するのが間違っている方法と理由を教えてください。

ありがとう。

回答:


108

ラムダ(またはクロージャ)は、関数ポインターと変数の両方をカプセル化します。これが、C#で次のことができる理由です。

int lessThan = 100;
Func<int, bool> lessThanTest = delegate(int i) {
   return i < lessThan;
};

そこで、クロージャとして匿名デリゲートを使用しました(構文はラムダの同等のものよりも少し明確でCに近いです)。これにより、lessThan(スタック変数)がクロージャに取り込まれました。クロージャが評価されると、lessThan(スタックフレームが破棄された可能性があります)が引き続き参照されます。lessThanを変更すると、比較が変更されます。

int lessThan = 100;
Func<int, bool> lessThanTest = delegate(int i) {
   return i < lessThan;
};

lessThanTest(99); // returns true
lessThan = 10;
lessThanTest(99); // returns false

Cでは、これは違法です。

BOOL (*lessThanTest)(int);
int lessThan = 100;

lessThanTest = &LessThan;

BOOL LessThan(int i) {
   return i < lessThan; // compile error - lessThan is not in scope
}

ただし、2つの引数を取る関数ポインタを定義できます。

int lessThan = 100;
BOOL (*lessThanTest)(int, int);

lessThanTest = &LessThan;
lessThanTest(99, lessThan); // returns true
lessThan = 10;
lessThanTest(100, lessThan); // returns false

BOOL LessThan(int i, int lessThan) {
   return i < lessThan;
}

しかし、今、私はそれを評価するときに2つの引数を渡さなければなりません。この関数ポインターをlessThanがスコープ内にない別の関数に渡したい場合は、チェーン内の各関数に渡すか、グローバルにプロモートすることで、手動で関数ポインターを存続させる必要があります。

クロージャをサポートするほとんどの主流言語は無名関数を使用しますが、その必要はありません。匿名関数なしのクロージャと、クロージャなしの匿名関数を使用できます。

概要:クロージャは、関数ポインターとキャプチャーされた変数の組み合わせです。


おかげで、あなたは本当に他の人が得ようとしているアイデアを家に持ち帰りました。
なし

これを書いたときはおそらく古いバージョンのCを使用していたか、関数を前方宣言することを忘れていましたが、これをテストしたときに述べたのと同じ動作は観察されません。ideone.com/JsDVBK
smac89

@ smac89-lessThan変数をグローバルにしました-代わりにそれを明示的に述べました。
マークブラケット2016年

42

「実際の」クロージャーがある言語とない言語の両方のコンパイラーを書いた人として、私は上記の答えのいくつかに敬意を表して同意しません。Lisp、Scheme、ML、またはHaskellクロージャは、新しい関数を動的に作成しません。代わりに、既存の関数を再利用しますが、新しい自由変数を使用し再利用します。自由変数のコレクションは、少なくともプログラミング言語理論家によって、しばしば環境と呼ばれます。

クロージャは、関数と環境を含む単なる集合体です。NewJerseyコンパイラのStandardMLでは、1つをレコードとして表しました。1つのフィールドにはコードへのポインターが含まれ、他のフィールドには自由変数の値が含まれていました。コンパイラーは、同じコードへのポインターを含むが、自由変数の値が異なる新しいレコードを割り当てることにより、新しいクロージャー(関数ではない)を動的に作成しました

これらすべてをCでシミュレートできますが、それはお尻の痛みです。2つのテクニックが人気があります:

  1. 関数(コード)へのポインターと自由変数への個別のポインターを渡して、クロージャーが2つのC変数に分割されるようにします。

  2. 構造体へのポインターを渡します。構造体には自由変数の値とコードへのポインターが含まれています。

テクニック#1は、Cである種のポリモーフィズムをシミュレートしようとしていて、環境のタイプを明らかにしたくない場合に理想的です---環境を表すためにvoid *ポインターを使用します。たとえば、DaveHansonのCインターフェイスと実装を見てください。関数型言語のネイティブコードコンパイラで行われることにより近いテクニック#2は、別のよく知られたテクニックにも似ています...仮想メンバー関数を持つC ++オブジェクト。実装はほとんど同じです。

この観察は、ヘンリーベイカーからの賢明な亀裂につながりました:

Algol / Fortranの世界の人々は、将来の効率的なプログラミングで関数クロージャがどのように使用できるかを理解していないと長年不満を漏らしていました。その後、「オブジェクト指向プログラミング」革命が起こり、今では誰もが関数クロージャを使用してプログラムを作成していますが、それを呼び出すことを拒否しています。


1
説明とOOPが実際にはクロージャであるという引用のための+ 1-既存の関数を再利用しますが、新しい自由変数でそれを行います-環境を取得する関数(メソッド)(新しい状態に過ぎないオブジェクトインスタンスデータへの構造体ポインタ)操作する。
legends2k 2014年

8

Cでは、関数をインラインで定義できないため、実際にクロージャを作成することはできません。あなたがしているのは、いくつかの事前定義されたメソッドへの参照を渡すことだけです。匿名メソッド/クロージャをサポートする言語では、メソッドの定義ははるかに柔軟です。

簡単に言うと、関数ポインターにはスコープが関連付けられていません(グローバルスコープを数えない限り)が、クロージャーには、それらを定義するメソッドのスコープが含まれます。ラムダを使用すると、メソッドを作成するメソッドを作成できます。クロージャを使用すると、「いくつかの引数を関数にバインドし、結果としてアリティの低い関数を取得する」ことができます。(トーマスのコメントから引用)。Cではそれを行うことはできません。

編集:例を追加します(Actionscript風の構文を使用します。これが今の私の頭の中にあります):

別のメソッドを引数として取るメソッドがあるが、呼び出されたときにそのメソッドにパラメーターを渡す方法が提供されていないとしますか?たとえば、渡したメソッドを実行する前に遅延が発生するメソッド(愚かな例ですが、単純にしておきたい)などです。

function runLater(f:Function):Void {
  sleep(100);
  f();
}

ここで、runLater()を使用して、オブジェクトの処理を遅らせたいとします。

function objectProcessor(o:Object):Void {
  /* Do something cool with the object! */
}

function process(o:Object):Void {
  runLater(function() { objectProcessor(o); });
}

process()に渡す関数は、静的に定義された関数ではなくなりました。動的に生成され、メソッドが定義されたときにスコープ内にあった変数への参照を含めることができます。したがって、グローバルスコープにない場合でも、「o」と「objectProcessor」にアクセスできます。

それが理にかなっていることを願っています。


私はあなたのコメントに基づいて私の答えを微調整しました。用語の詳細についてはまだ100%明確ではないので、直接引用しました。:)
Herms

無名関数のインライン機能は、(ほとんど?)主流のプログラミング言語の実装の詳細です-クロージャーの要件ではありません。
マークブラケット

6

クロージャ=ロジック+環境。

たとえば、次のC#3メソッドについて考えてみます。

public Person FindPerson(IEnumerable<Person> people, string name)
{
    return people.Where(person => person.Name == name);
}

ラムダ式は、ロジック(「名前の比較」)だけでなく、パラメーター(つまりローカル変数)「名前」を含む環境もカプセル化します。

これについて詳しくは、C#1、2、3を紹介するクロージャに関する私の記事をご覧ください。クロージャによって、作業が簡単になります。


voidをIEnumerable <Person>に置き換えることを検討してください
Amy B

1
@デビッドB:乾杯、完了。@edg:変更可能な状態ので、単なる状態ではないと思います。言い換えると、ローカル変数を変更するクロージャを実行すると(メソッド内にある間)、そのローカル変数も変更されます。「環境」は私にはこれをよりよく伝えているようですが、それは羊毛です。
Jon Skeet

私は答えに感謝しますが、それは私にとって本当に何も明確になりません。人々は単なるオブジェクトであり、あなたはその上でメソッドを呼び出すように見えます。多分それは私がC#を知らないだけです。
なし

はい、メソッドを呼び出していますが、渡されるパラメーターはクロージャです。
Jon Skeet

4

Cでは、関数ポインターを関数に引数として渡し、関数から値として返すことができますが、関数は最上位にのみ存在します。関数定義を相互にネストすることはできません。Cが、外部関数の変数にアクセスできるネストされた関数をサポートし、呼び出しスタックの上下に関数ポインターを送信できるようにするために必要なことを考えてください。(この説明に従うには、関数呼び出しがCおよびほとんどの同様の言語で実装される方法の基本を知っている必要があります。ウィキペディアの呼び出しスタックエントリを参照してください。)

入れ子関数へのポインタはどのようなオブジェクトですか?コードを呼び出すと、外部関数の変数にどのようにアクセスするのでしょうか。(再帰のため、一度にアクティブな外部関数のいくつかの異なる呼び出しがある可能性があることに注意してください。)これはfunarg問題と呼ばれ、下向きのfunargs問題と上向きのfunargs問題の2つのサブ問題があります。

下向きのfunargs問題、つまり、呼び出す関数への引数として関数ポインターを「スタックの下位」に送信することは、実際にはCと互換性がなく、GCCネストされた関数を下向きのfunargsとしてサポートします。GCCでは、ネストされた関数へのポインターを作成すると、実際にはトランポリンへのポインターが取得されます。これは、静的リンクポインターを設定し、静的リンクポインターを使用してアクセスする実際の関数を呼び出す動的に構築されたコードです。外部関数の変数。

上向きのfunargs問題はより困難です。GCCは、外部関数がアクティブでなくなった後(呼び出しスタックにレコードがない場合)にトランポリンポインターを存在させることを妨げません。その場合、静的リンクポインターがガベージを指す可能性があります。アクティベーションレコードをスタックに割り当てることができなくなりました。通常の解決策は、それらをヒープに割り当て、ネストされた関数を表す関数オブジェクトが外部関数のアクティブ化レコードを指すようにすることです。このようなオブジェクトはクロージャと呼ばます。その場合、言語は通常、ガベージコレクションをサポートする必要があります。これにより、レコードを指すポインタがなくなったときにレコードを解放できます。

ラムダ(無名関数)は実際には別の問題ですが、通常、無名関数をその場で定義できる言語では、それらを関数値として返すこともできるため、最終的にはクロージャになります。


3

ラムダは、動的に定義された匿名の関数です。Cではそれを行うことはできません...クロージャ(または2つの組み合わせ)に関しては、典型的なlispの例は次のようになります。

(defun get-counter (n-start +-number)
     "Returns a function that returns a number incremented
      by +-number every time it is called"
    (lambda () (setf n-start (+ +-number n-start))))

C用語では、の字句環境(スタック)はget-counter無名関数によってキャプチャされ、次の例に示すように内部的に変更されていると言えます。

[1]> (defun get-counter (n-start +-number)
         "Returns a function that returns a number incremented
          by +-number every time it is called"
        (lambda () (setf n-start (+ +-number n-start))))
GET-COUNTER
[2]> (defvar x (get-counter 2 3))
X
[3]> (funcall x)
5
[4]> (funcall x)
8
[5]> (funcall x)
11
[6]> (funcall x)
14
[7]> (funcall x)
17
[8]> (funcall x)
20
[9]> 

2

クロージャは、関数定義の観点から、ミニオブジェクトをその場で宣言できるように、関数ロジックと一緒にバインドされている変数を意味します。

Cとクロージャに関する重要な問題の1つは、クロージャがそれらを指しているかどうかに関係なく、スタックに割り当てられた変数が現在のスコープを離れると破壊されることです。これは、ローカル変数へのポインタを不注意に返すときに発生する種類のバグにつながります。クロージャは基本的に、関連するすべての変数がヒープ上の参照カウントまたはガベージコレクションされたアイテムであることを意味します。

すべての言語のラムダがクロージャであるかどうかわからないため、ラムダをクロージャと同一視することに抵抗があります。ラムダは、変数をバインドせずにローカルで定義された無名関数であると思うことがあります(Python pre 2.1?)。


2

GCCでは、次のマクロを使用してラムダ関数をシミュレートできます。

#define lambda(l_ret_type, l_arguments, l_body)       \
({                                                    \
    l_ret_type l_anonymous_functions_name l_arguments \
    l_body                                            \
    &l_anonymous_functions_name;                      \
})

ソースからの例:

qsort (array, sizeof (array) / sizeof (array[0]), sizeof (array[0]),
     lambda (int, (const void *a, const void *b),
             {
               dump ();
               printf ("Comparison %d: %d and %d\n",
                       ++ comparison, *(const int *) a, *(const int *) b);
               return *(const int *) a - *(const int *) b;
             }));

もちろん、この手法を使用すると、アプリケーションが他のコンパイラで動作する可能性がなくなり、明らかに「未定義」の動作になるため、YMMVになります。


2

クロージャは、キャプチャ自由変数環境を。周囲のコードがアクティブでなくなったとしても、環境は引き続き存在します。

Common Lispの例でMAKE-ADDER、新しいクロージャを返します。

CL-USER 53 > (defun make-adder (start delta) (lambda () (incf start delta)))
MAKE-ADDER

CL-USER 54 > (compile *)
MAKE-ADDER
NIL
NIL

上記の関数の使用:

CL-USER 55 > (let ((adder1 (make-adder 0 10))
                   (adder2 (make-adder 17 20)))
               (print (funcall adder1))
               (print (funcall adder1))
               (print (funcall adder1))
               (print (funcall adder1))
               (print (funcall adder2))
               (print (funcall adder2))
               (print (funcall adder2))
               (print (funcall adder1))
               (print (funcall adder1))
               (describe adder1)
               (describe adder2)
               (values))

10 
20 
30 
40 
37 
57 
77 
50 
60 
#<Closure 1 subfunction of MAKE-ADDER 4060001ED4> is a CLOSURE
Function         #<Function 1 subfunction of MAKE-ADDER 4060001CAC>
Environment      #(60 10)
#<Closure 1 subfunction of MAKE-ADDER 4060001EFC> is a CLOSURE
Function         #<Function 1 subfunction of MAKE-ADDER 4060001CAC>
Environment      #(77 20)

DESCRIBE関数は、両方のクロージャの関数オブジェクトが同じであることを示していますが、環境が異なることに注意してください。

Common Lispは、クロージャと純粋関数オブジェクト(環境のないもの)の両方を関数にし、ここではを使用して、同じ方法で両方を呼び出すことができますFUNCALL


1

主な違いは、Cの字句スコープの欠如から生じます。

関数ポインタはまさにそれであり、コードのブロックへのポインタです。参照する非スタック変数は、グローバル、静的、または同様のものです。

クロージャOTOHには、「外部変数」または「アップバリュー」の形式で独自の状態があります。字句スコープを使用して、必要に応じてプライベートまたは共有することができます。同じ関数コードで、変数インスタンスが異なる多くのクロージャを作成できます。

いくつかのクロージャーはいくつかの変数を共有できるため、オブジェクトのインターフェースになる可能性があります(OOPの意味で)。これをCで行うには、構造体を関数ポインターのテーブルに関連付ける必要があります(これは、C ++が行うことであり、クラスvtableを使用します)。

要するに、クロージャは関数ポインタといくつかの状態です。それはより高いレベルの構成です


2
WTF?Cには間違いなく字句スコープがあります。
ルイスオリヴェイラ

1
「静的スコープ」があります。私が理解しているように、字句スコープは、動的に作成された関数を持つ言語で同様のセマンティクスを維持するためのより複雑な機能であり、クロージャと呼ばれます。
ハビエル

1

ほとんどの応答は、クロージャにはおそらく無名関数への関数ポインタが必要であることを示していますが、Markが書いたように、クロージャは名前付き関数とともに存在できます。Perlでの例を次に示します。

{
    my $count;
    sub increment { return $count++ }
}

クロージャは、$count変数を定義する環境です。これはincrementサブルーチンでのみ使用可能であり、呼び出し間で持続します。


0

Cでは、関数ポインターは関数を逆参照するときに関数を呼び出すポインターであり、クロージャーは関数のロジックと環境(変数とそれらがバインドされている値)を含む値であり、ラムダは通常、次のような値を参照します。実際には名前のない関数です。Cでは、関数はファーストクラスの値ではないため、渡すことができないため、代わりにポインターを渡す必要がありますが、関数型言語(Schemeなど)では、他の値を渡すのと同じ方法で関数を渡すことができます。

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