İşlev işaretçileri, Kapanışlar ve Lambda


86

Şimdi işlev işaretlerini öğreniyorum ve konuyla ilgili K&R bölümünü okurken, bana ilk çarpan şey, "Hey, bu bir tür kapanış gibi" oldu. Bu varsayımın bir şekilde temelde yanlış olduğunu biliyordum ve çevrimiçi bir aramadan sonra bu karşılaştırmanın gerçekten herhangi bir analizini bulamadım.

Öyleyse neden C tarzı işlev işaretçileri, kapanışlardan veya lambdalardan temelde farklıdır? Anlayabildiğim kadarıyla, işlev göstericisinin, işlevi anonim olarak tanımlama uygulamasının aksine, hala tanımlanmış (adlandırılmış) bir işleve işaret etmesiyle ilgisi var.

Neden bir işlevi, yalnızca normal, gündelik bir işlev olan ve geçmekte olan ikinci durumda, adının açık olmadığı ikinci durumda daha güçlü görülen bir işleve geçirmek?

Lütfen ikisini bu kadar yakından karşılaştırmakla nasıl ve neden hatalı olduğumu söyleyin.

Teşekkürler.

Yanıtlar:


108

Bir lambda (veya kapanış ) hem işlev göstericisini hem de değişkenleri kapsüller. Bu nedenle, C # ile şunları yapabilirsiniz:

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

Kapanış olarak orada anonim bir temsilci kullandım (sözdizimi lambda eşdeğerinden biraz daha net ve C'ye daha yakın), kapanışa lessThan (bir yığın değişkeni) yakalayan. Kapanma değerlendirildiğinde, lessThan (yığın çerçevesi yok edilmiş olabilir) referans alınmaya devam edecektir. Daha az değiştirirsem, karşılaştırmayı değiştiririm:

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

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

C'de, bu yasa dışı olacaktır:

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

lessThanTest = &LessThan;

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

2 argüman alan bir işlev işaretçisi tanımlayabilsem de:

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

Ama şimdi değerlendirdiğimde 2 argümanı geçmek zorundayım. Bu işlev işaretçisini, kapsam dahilinde olmayan başka bir işleve geçirmek isteseydim, ya zincirdeki her bir işleve aktararak ya da bir global olarak yükselterek onu elle canlı tutmam gerekirdi.

Kapanmaları destekleyen çoğu ana dil anonim işlevler kullansa da, buna gerek yoktur. Anonim işlevler olmadan kapanışlara ve kapatmalar olmadan anonim işlevlere sahip olabilirsiniz.

Özet: bir kapanış, işlev göstericisi + yakalanan değişkenlerin bir kombinasyonudur.


teşekkürler, diğer insanların ulaşmaya çalıştıkları fikri gerçekten eve götürdün.
Hiçbiri

Muhtemelen bunu yazarken C'nin daha eski bir sürümünü kullanıyordunuz veya ileriye doğru işlevi bildirmeyi hatırlamadınız, ancak bunu test ettiğimde bahsettiğiniz aynı davranışı gözlemlemiyorum. ideone.com/JsDVBK
smac89

@ smac89 - lessThan değişkenini global yaptınız - Bunu bir alternatif olarak açıkça belirttim.
Mark Brackett

42

'Gerçek' kapanışları olan ve olmayan diller için derleyiciler yazan biri olarak, yukarıdaki cevapların bazılarına saygıyla katılmıyorum. Bir Lisp, Scheme, ML veya Haskell kapanışı dinamik olarak yeni bir işlev oluşturmaz . Bunun yerine mevcut bir işlevi yeniden kullanır, ancak bunu yeni serbest değişkenlerle yapar . Serbest değişkenlerin toplanması , en azından programlama dili kuramcıları tarafından genellikle çevre olarak adlandırılır .

Kapanış, yalnızca bir işlevi ve bir ortamı içeren bir kümedir. New Jersey'deki Standard ML derleyicisinde, birini rekor olarak temsil ettik; bir alan koda bir işaretçi içeriyordu ve diğer alanlar serbest değişkenlerin değerlerini içeriyordu. Derleyici , aynı koda bir işaretçi içeren , ancak serbest değişkenler için farklı değerler içeren yeni bir kayıt tahsis ederek dinamik olarak yeni bir kapanış (işlev değil) yarattı .

Tüm bunları C'de simüle edebilirsiniz, ama bu baş belasıdır. İki teknik popülerdir:

  1. İşleve (kod) bir işaretçi ve serbest değişkenlere ayrı bir işaretçi iletin, böylece kapanış iki C değişkenine bölünür.

  2. Yapı serbest değişkenlerin değerlerini ve ayrıca koda bir işaretçi içerdiği bir yapıya bir işaretçi iletin.

Teknik # 1, C'de bir tür polimorfizmi simüle etmeye çalışırken ve ortamın türünü ortaya çıkarmak istemediğinizde idealdir - ortamı temsil etmek için bir boşluk * işaretçisi kullanırsınız. Örnekler için, Dave Hanson'ın C Arayüzleri ve Uygulamaları'na bakın . İşlevsel diller için yerel kod derleyicilerinde olanlara daha çok benzeyen Teknik # 2, başka bir tanıdık tekniğe benziyor ... Sanal üye işlevli C ++ nesneleri. Uygulamalar neredeyse aynıdır.

Bu gözlem Henry Baker'ın bir esprili olmasına yol açtı:

Algol / Fortran dünyasındaki insanlar, gelecek için verimli programlamada işlev kapanışlarının ne gibi olası kullanımlarını anlamadıklarından yıllarca şikayet ettiler. Sonra 'nesne yönelimli programlama' devrimi oldu ve şimdi herkes işlev kapanışlarını kullanarak program yapıyor, ancak onlara hala böyle demeyi reddediyorlar.


1
Açıklama ve OOP'nin gerçekten kapanış olduğu alıntı için +1 - mevcut bir işlevi yeniden kullanır ancak bunu yeni serbest değişkenlerle yapar - ortamı alan işlevler (yöntemler) (yeni durumlardan başka bir şey olmayan nesne örnek verilerine bir yapı işaretçisi) üzerinde çalışmak.
legends2k

8

C'de işlevi satır içi tanımlayamazsınız, bu nedenle gerçekten bir kapanış yaratamazsınız. Yaptığınız tek şey, önceden tanımlanmış bir yönteme referans vermektir. Anonim yöntemleri / kapanmaları destekleyen dillerde, yöntemlerin tanımı çok daha esnektir.

En basit ifadeyle, işlev işaretçilerinin kendileriyle ilişkili bir kapsamı yoktur (genel kapsamı saymazsanız), oysa kapanışlar onları tanımlayan yöntemin kapsamını içerir. Lambdas ile, bir yöntem yazan bir yöntem yazabilirsiniz. Kapanışlar, "bazı argümanları bir işleve bağlamanıza ve sonuç olarak daha düşük bir işlev elde etmenize" izin verir. (Thomas'ın yorumundan alınmıştır). Bunu C'de yapamazsın.

DÜZENLEME: Bir örnek eklemek (Actionscript-ish sözdizimini kullanacağım çünkü şu anda aklımda olan şey bu):

Başka bir yöntemi bağımsız değişken olarak alan, ancak çağrıldığında bu yönteme herhangi bir parametre aktarmanın bir yolunu sağlamayan bir yönteminiz olduğunu varsayalım. Örneğin, geçirdiğiniz yöntemi çalıştırmadan önce gecikmeye neden olan bir yöntem gibi (aptal bir örnek, ancak basit tutmak istiyorum).

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

Şimdi runLater () kullanıcısının bir nesnenin bazı işlemlerini geciktirmesini istediğinizi söyleyin:

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

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

Process () 'e ilettiğiniz işlev artık statik olarak tanımlanmış bir işlev değildir. Dinamik olarak oluşturulur ve yöntem tanımlandığında kapsamda olan değişkenlere referanslar içerebilir. Böylece, genel kapsamda olmasalar bile 'o' ve 'objectProcessor'a erişebilir.

Umarım bu mantıklıdır.


Yorumunuza göre cevabımı değiştirdim. Şartların ayrıntıları konusunda hala% 100 net değilim, bu yüzden sizden doğrudan alıntı yaptım. :)
Herms

Anonim işlevlerin satır içi yeteneği, (çoğu?) Genel programlama dillerinin bir uygulama ayrıntısıdır - kapanışlar için bir gereklilik değildir.
Mark Brackett

6

Kapanış = mantık + ortam.

Örneğin, şu C # 3 yöntemini düşünün:

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

Lambda ifadesi yalnızca mantığı ("adı karşılaştır") değil, aynı zamanda parametre (yani yerel değişken) "ad" dahil olmak üzere ortamı da kapsüller.

Bununla ilgili daha fazla bilgi için, kapanışların işleri nasıl kolaylaştırdığını gösteren C # 1, 2 ve 3'ü anlatan kapanışlar hakkındaki makaleme bir göz atın .


void'i IEnumerable <Person> ile değiştirmeyi düşünün
Amy B

1
@David B: Şerefe, bitti. @edg: Bence durumdan daha fazlası, çünkü bu değişken bir durum Başka bir deyişle, yerel bir değişkeni değiştiren bir kapanışı yürütürseniz (hala yöntem içindeyken), yerel değişken de değişir. "Çevre" bunu bana daha iyi yansıtıyor gibi görünüyor, ama tüylü.
Jon Skeet

Cevabı takdir ediyorum ama bu benim için hiçbir şeyi netleştirmiyor, insanlar sadece bir nesne ve sizin onun üzerinde bir yöntem çağırmanız gibi görünüyor. Belki de C # bilmiyorumdur.
Hiçbiri

Evet, üzerinde bir yöntem çağırıyor - ancak geçtiği parametre kapanış.
Jon Skeet

4

C'de işlev işaretçileri, işlevlere bağımsız değişkenler olarak aktarılabilir ve işlevlerden değerler olarak döndürülebilir, ancak işlevler yalnızca üst düzeyde bulunur: işlev tanımlarını iç içe yerleştiremezsiniz. C'nin, dış işlevin değişkenlerine erişebilen iç içe geçmiş işlevleri desteklemesinin yanı sıra, çağrı yığınında yukarı ve aşağı işlev işaretçileri gönderebilmesi için ne gerektiğini düşünün. (Bu açıklamayı takip etmek için, işlev çağrılarının C ve çoğu benzer dilde nasıl uygulandığının temellerini bilmelisiniz: Wikipedia'daki çağrı yığını girişine göz atın .)

İç içe geçmiş bir işleve işaretçi ne tür bir nesnedir? Yalnızca kodun adresi olamaz, çünkü onu çağırırsanız, dış işlevin değişkenlerine nasıl erişir? (Özyineleme nedeniyle, bir seferde aktif olan dış işlevin birkaç farklı çağrısı olabileceğini unutmayın.) Buna funarg problemi denir ve iki alt problem vardır: aşağı doğru funargs problemi ve upward funargs problemi.

Aşağı doğru funargs problemi, yani çağırdığınız bir işleve argüman olarak "yığından aşağı" bir fonksiyon işaretçisi göndermek aslında C ile uyumsuz değildir ve GCC , aşağıya doğru funargs olarak iç içe geçmiş fonksiyonları destekler . GCC'de, iç içe geçmiş bir işleve işaretçi oluşturduğunuzda, statik bağlantı işaretçisini ayarlayan ve ardından erişmek için statik bağlantı işaretçisini kullanan gerçek işlevi çağıran dinamik olarak oluşturulmuş bir kod parçası olan tramboline gerçekten bir işaretçi elde edersiniz. dış fonksiyonun değişkenleri.

Yukarı doğru funargs problemi daha zordur. GCC, dış işlev artık etkin olmadığında (çağrı yığınında kaydı yoksa) bir trambolin işaretçisinin var olmasına izin vermenizi engellemez ve sonra statik bağlantı işaretçisi çöpü işaret edebilir. Aktivasyon kayıtları artık bir yığın üzerinde tahsis edilemez. Olağan çözüm, onları öbek üzerinde tahsis etmek ve iç içe geçmiş bir işlevi temsil eden bir işlev nesnesinin, dış işlevin etkinleştirme kaydını işaret etmesine izin vermektir. Böyle bir nesneye kapatma denir . Daha sonra, dilin çöp toplamayı desteklemesi gerekir, böylece kayıtlar, kendilerine işaret eden başka işaretler kalmadığında serbest bırakılabilir.

Lambdalar ( anonim işlevler ) gerçekten ayrı bir konudur, ancak genellikle anonim işlevleri anında tanımlamanıza izin veren bir dil, bunları işlev değerleri olarak döndürmenize de izin verir, böylece kapanış olurlar.


3

Lambda, anonim, dinamik olarak tanımlanmış bir işlevdir. Bunu C'de yapamazsınız ... kapanışlarda (veya ikisinin mahkumiyetinde) olduğu gibi, tipik lisp örneği şu satırlarda bir şeye benzeyecektir:

(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 terimlerinde, sözlü ortamının (yığın) get-counteranonim işlev tarafından yakalandığını ve aşağıdaki örnekte gösterildiği gibi dahili olarak değiştirildiğini söyleyebilirsiniz :

[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

Kapanışlar, bir mini nesneyi anında bildirebilme gibi, işlev tanımı açısından bazı değişkenlerin işlev mantığıyla birbirine bağlı olduğu anlamına gelir.

C ve kapanışlarla ilgili önemli bir sorun, yığına tahsis edilen değişkenlerin, bir kapanmanın onlara işaret edip etmediğine bakılmaksızın, mevcut kapsamı terk ettiklerinde yok edilecek olmasıdır. Bu, işaretçileri dikkatsizce yerel değişkenlere döndürdüklerinde insanların aldığı türden hatalara yol açar. Kapanışlar temelde tüm ilgili değişkenlerin ya yeniden sayılmış ya da bir yığın üzerinde çöp olarak toplanmış öğeler olduğunu ima eder.

Lambda'yı kapanışla eşitlemek konusunda rahat değilim çünkü tüm dillerdeki lambdaların kapanış olduğundan emin değilim, bazen lambdaların değişkenlerin bağlanması olmadan yerel olarak tanımlanmış anonim işlevler olduğunu düşünüyorum (Python öncesi 2.1?).


2

GCC'de aşağıdaki makroyu kullanarak lambda işlevlerini simüle etmek mümkündür:

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

Kaynaktan örnek :

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

Bu tekniğin kullanılması, uygulamanızın diğer derleyicilerle çalışma olasılığını ortadan kaldırır ve görünüşe göre YMMV'nin "tanımsız" davranışıdır.


2

Kapatma yakalar serbest değişkenleri bir in çevre . Çevreleyen kod artık etkin olmasa bile ortam hala var olacaktır.

MAKE-ADDERYeni bir kapanış döndüren Common Lisp'de bir örnek .

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

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

Yukarıdaki işlevi kullanarak:

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)

Not bu DESCRIBEfonksiyon gösterir fonksiyonu nesneleri hem de kapakların aynıdır, ancak ortam farklıdır.

Ortak Lisp olmak kapanması ve saf fonksiyon nesneleri (bir ortamda olmayanlar) hem hem kılan fonksiyonları ve birini kullanarak burada, aynı şekilde her iki çağırabilir FUNCALL.


1

Temel fark, C'deki sözcük kapsamının olmamasından kaynaklanmaktadır.

Bir işlev işaretçisi sadece bir kod bloğuna bir göstericidir. Referans verdiği yığın dışı değişkenler genel, statik veya benzerdir.

Bir kapanış, OTOH, 'dış değişkenler' veya 'yukarı değerler' şeklinde kendi durumuna sahiptir. sözcüksel kapsam kullanarak istediğiniz kadar özel veya paylaşılabilirler. Aynı işlev koduyla, ancak farklı değişken örnekleriyle çok sayıda kapanış oluşturabilirsiniz.

Birkaç kapanış bazı değişkenleri paylaşabilir ve dolayısıyla bir nesnenin arayüzü olabilir (OOP anlamında). Bunu C'de yapmak için, bir yapıyı işlev işaretçileri tablosuyla ilişkilendirmeniz gerekir (C ++, bir sınıf vtable ile bunu yapar).

kısacası, bir kapanma bir işlev göstergesi ARTI durumudur. üst düzey bir yapı


2
O NE LAN? C kesinlikle sözcük kapsamına sahiptir.
Luís Oliveira

1
"statik kapsama" sahiptir. Anladığım kadarıyla, sözcüksel kapsam belirleme, dinamik olarak oluşturulmuş işlevlere sahip bir dilde benzer anlambilimini sürdürmek için daha karmaşık bir özelliktir ve bu özellikler daha sonra kapanış olarak adlandırılır.
Javier

1

Yanıtların çoğu, kapanışların, muhtemelen anonim işlevler için işlev işaretçileri gerektirdiğini, ancak Mark'ın yazdığı gibi , adlandırılmış işlevlerle kapanmalar olabileceğini belirtir . İşte Perl'de bir örnek:

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

Kapanış, $countdeğişkeni tanımlayan ortamdır . Yalnızca incrementalt programda mevcuttur ve aramalar arasında devam eder.


0

C'de bir işlev işaretçisi, bir işlevin referansını kaldırdığınızda bir işlevi çağıran bir göstericidir; bir kapanış, bir işlevin mantığını ve ortamını (değişkenler ve bağlı oldukları değerler) içeren bir değerdir ve bir lambda genellikle aslında isimsiz bir işlevdir. C'de bir işlev birinci sınıf bir değer değildir, bu nedenle etrafta iletilemez, bu nedenle bunun yerine ona bir işaretçi iletmeniz gerekir, ancak işlevsel dillerde (Şema gibi) işlevleri başka herhangi bir değeri ilettiğiniz şekilde iletebilirsiniz.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.