質問

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

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

2番目の場合(名前が付けられていない場合)に、より強力な関数に関数を渡すのは、通常の通常の関数が渡される場合よりも高いのですか?

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

ありがとう。

役に立ちましたか?

解決

ラムダ(またはクロージャー)は、関数ポインターと変数の両方をカプセル化します。これが、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がスコープ内にない別の関数に渡したい場合は、チェーン内の各関数に渡すか、グローバルに昇格して、手動でそれを維持する必要があります。

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

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

他のヒント

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

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

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

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

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

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

この観察結果は、ヘンリー・ベイカーによる賢明なクラックにつながりました。

  

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

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

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

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

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

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

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

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

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

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

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

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

Lambdas(匿名関数)は実際には別の問題ですが、通常は匿名関数をオンザフライで定義すると、関数値として返すことができるため、最終的にクロージャになります。

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

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

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

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

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の動作。

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

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 関数は、両方の closures functionオブジェクトは同じですが、 environment >は異なります。

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

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

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

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

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

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

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

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

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

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

ライセンス: CC-BY-SA帰属
所属していません StackOverflow
scroll top