web-dev-qa-db-ja.com

関数ポインター、クロージャー、およびLambda

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

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

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

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

ありがとう。

84
None

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

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

そこで、匿名デリゲートをクロージャーとして使用し(構文はラムダの同等物よりも少し明確でCに近い)、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がスコープ内にない別の関数に渡したい場合は、チェーン内の各関数に渡すか、グローバルにプロモートすることにより、手動でそれを維持する必要があります。

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

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

107
Mark Brackett

「実際の」クロージャの有無にかかわらず、言語用のコンパイラを書いた人として、私は上記の答えのいくつかに敬意を表して反対します。 LISP、Scheme、ML、またはHaskellクロージャは、新しい関数を動的に作成しません。代わりに、既存の関数を再利用しますが、新しい自由変数で再利用します。自由変数のコレクションは、多くの場合、プログラミング言語の理論家によってenvironmentと呼ばれます。

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

このすべてをCでシミュレートできますが、それはロバの苦痛です。 2つの手法が一般的です。

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

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

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

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

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

40
Norman Ramsey

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」にアクセスできますが、それらはグローバルスコープ内にありません。

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

9
Herms

閉鎖=論理+環境。

たとえば、次のC#3メソッドを検討してください。

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

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

これについて詳しくは、C クロージャーに関する記事 をご覧ください。C#1、2、および3を使用して、クロージャーがどのように物事を容易にするかを示します。

6
Jon Skeet

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

ネストされた関数へのポインターはどのようなオブジェクトですか?コードのアドレスだけにすることはできません。それを呼び出すと、外部関数の変数にどのようにアクセスするのですか? (再帰のために、一度にアクティブな外部関数のいくつかの異なる呼び出しが存在する可能性があることを覚えておいてください。)これは funarg問題 と呼ばれ、2つの副次的な問題があります。 funargs問題と上向きfunargs問題。

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

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

ラムダ( 匿名関数 )は実際には別の問題ですが、通常、その場で匿名関数を定義できる言語では、関数値として関数を返すこともできるため、最終的には閉鎖である。

4

ラムダは、匿名の動的に定義された関数です。あなたは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]> 
3
dsm

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

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

すべての言語のラムダがクロージャであることを確信していないため、ラムダをクロージャと同一視するのは不安です。時々、ラムダは変数のバインドなしでローカルに定義された匿名関数だと思います(Python pre 2.1?)。

2
Andy Dent

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;                      \
})

source の例:

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
secretformula

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

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関数は、両方のclosuresfunction objectsは同じですが、environmentは異なることを示していることに注意してください。

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

2
Rainer Joswig

ほとんどの応答は、クロージャーには匿名関数への関数ポインターが必要であることを示していますが、 Mark written クロージャーには名前付き関数が存在する可能性があります。 Perlの例を次に示します。

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

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

1
Michael Carman

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

関数ポインターは、コードブロックへのポインターです。参照する非スタック変数は、グローバル、静的、または類似しています。

クロージャOTOHには、「外部変数」または「上位値」の形式で独自の状態があります。レキシカルスコープを使用して、必要に応じてプライベートまたは共有できます。同じ関数コードで異なる変数インスタンスを使用して、多くのクロージャーを作成できます。

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

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

1
Javier

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

0
SpaceghostAli