Czy ktoś mógłby wyjaśnić? Rozumiem podstawowe pojęcia, które się za nimi kryją, ale często widzę, że są używane zamiennie i się mylę.
A skoro już tu jesteśmy, czym różnią się od zwykłej funkcji?
Czy ktoś mógłby wyjaśnić? Rozumiem podstawowe pojęcia, które się za nimi kryją, ale często widzę, że są używane zamiennie i się mylę.
A skoro już tu jesteśmy, czym różnią się od zwykłej funkcji?
Odpowiedzi:
Lambda jest tylko anonimowa funkcja - funkcja określona bez nazwy. W niektórych językach, takich jak Schemat, są one równoważne nazwanym funkcjom. W rzeczywistości definicja funkcji jest przepisywana jako wewnętrzne powiązanie lambda ze zmienną. W innych językach, takich jak Python, istnieją pewne (raczej niepotrzebne) różnice między nimi, ale zachowują się w ten sam sposób inaczej.
Zamknięcie jest dowolną funkcją, która zamyka się w środowisku , w którym została ona zdefiniowana. Oznacza to, że może uzyskać dostęp do zmiennych, których nie ma na liście parametrów. Przykłady:
def func(): return h
def anotherfunc(h):
return func()
Spowoduje to błąd, ponieważ func
nie zamyka środowiska w anotherfunc
- h
jest niezdefiniowany. func
zamyka się tylko w globalnym środowisku. To zadziała:
def anotherfunc(h):
def func(): return h
return func()
Ponieważ tutaj func
jest zdefiniowany w anotherfunc
Pythonie 2.3 i nowszych (lub jakaś liczba takich jak ta), kiedy prawie poprawne są zamknięcia (mutacja nadal nie działa), oznacza to, że zamyka się w anotherfunc
środowisku i może uzyskiwać dostęp do zmiennych wewnątrz to. W Pythonie 3.1 i nowsze, mutacja działa też przy korzystaniu z nonlocal
słowa kluczowego .
Kolejna ważna kwestia - func
będzie nadal zamykać anotherfunc
środowisko, nawet jeśli nie jest już oceniane anotherfunc
. Ten kod będzie również działać:
def anotherfunc(h):
def func(): return h
return func
print anotherfunc(10)()
Spowoduje to wydrukowanie 10.
Jak zauważyłeś, nie ma to nic wspólnego z lambda - są to dwa różne (choć powiązane) pojęcia.
Istnieje wiele zamieszania wokół lambd i zamknięć, nawet w odpowiedziach na pytanie StackOverflow tutaj. Zamiast pytać przypadkowych programistów, którzy nauczyli się o zamknięciach z praktyki z niektórymi językami programowania lub innymi programistami bez pojęcia, wybierz się do źródła (gdzie wszystko się zaczęło). A ponieważ lambda i zamknięcia pochodzą z Lambda Calculus wynalezionego przez Alonzo Church w latach 30., zanim jeszcze istniały pierwsze komputery elektroniczne, to jest to źródło , o którym mówię.
Lambda Calculus to najprostszy język programowania na świecie. Jedyne, co możesz w nim zrobić: ►
f x
. f
jest funkcja i x
jest jej jedynym parametrem)λ
(lambda), następnie nazwy symbolicznej (np. x
), A następnie kropki .
przed wyrażeniem. To następnie przekształca wyrażenie w funkcję oczekującą jednego parametru . λx.x+2
bierze wyrażenie x+2
i mówi, że symbol x
w tym wyrażeniu jest zmienną powiązaną - można go zastąpić wartością podaną jako parametr. (λx.x+2) 7
. Następnie wyrażenie (w tym przypadku wartość dosłowna) 7
jest podstawiane tak, jak x
w podwyrażeniu x+2
zastosowanej lambdy, więc otrzymujesz 7+2
, który następnie zmniejsza się do 9
wspólnych reguł arytmetycznych.Więc mamy rozwiązać jedną z zagadek:
lambda jest anonimowa funkcja z powyższym przykładzie λx.x+2
.
function(x) { return x+2; }
i możesz natychmiast zastosować go do niektórych parametrów takich jak ten:
(function(x) { return x+2; })(7)
lub możesz zapisać tę anonimową funkcję (lambda) w jakiejś zmiennej:
var f = function(x) { return x+2; }
co skutecznie nadaje mu nazwę f
, dzięki czemu możesz się do niej odwoływać i wywoływać wiele razy później, np .:
alert( f(7) + f(10) ); // should print 21 in the message box
Ale nie musiałeś tego nazywać. Możesz to nazwać natychmiast:
alert( function(x) { return x+2; } (7) ); // should print 9 in the message box
W LISP lambdas są tworzone w następujący sposób:
(lambda (x) (+ x 2))
i możesz wywołać taką lambda, stosując ją natychmiast do parametru:
( (lambda (x) (+ x 2)) 7 )
Jak powiedziałem, abstrakcja lambda wiąże symbol w podwyrażeniu, tak że staje się parametrem zastępowalnym . Taki symbol nazywa się związany . Ale co, jeśli w wyrażeniu są inne symbole? Na przykład: λx.x/y+2
. W tym wyrażeniu symbol x
jest związany λx.
poprzedzającą go abstrakcją lambda . Ale drugi symbol y
nie jest związany - jest bezpłatny . Nie wiemy, co to jest i skąd pochodzi, więc nie wiemy, co to znaczy i co wartość reprezentuje, i dlatego nie możemy ocenić tego wyrażenia, dopóki nie ustalimy, co y
to znaczy.
W rzeczywistości to samo dotyczy pozostałych dwóch symboli, 2
i +
. Po prostu tak dobrze znamy te dwa symbole, że zwykle zapominamy, że komputer ich nie zna i musimy powiedzieć im, co mają na myśli, definiując je gdzieś, np. W bibliotece lub w samym języku.
Możesz myśleć o wolnych symbolach zdefiniowanych gdzie indziej, poza wyrażeniem, w jego „otaczającym kontekście”, który nazywa się jego środowiskiem . Środowisko może być większym wyrażeniem, którego częścią jest to wyrażenie (jak powiedział Qui-Gon Jinn: „Zawsze jest większa ryba”;)) lub w bibliotece lub w samym języku (jako prymityw ).
To pozwala nam podzielić wyrażenia lambda na dwie kategorie:
Możesz ZAMKNIJ otwarte wyrażenie lambda, podając środowisko , które definiuje wszystkie te wolne symbole, wiążąc je z pewnymi wartościami (które mogą być liczbami, łańcuchami, anonimowymi funkcjami aka lambdas, cokolwiek…).
I tutaj jest zamknięcie części: zamknięcie z ekspresją lambda jest ten konkretny zbiór symboli zdefiniowane w kontekście zewnętrznej (otoczenia), które dają wartości do wolnych symboli w tym wyrażeniu, przez co więcej ich wolna. Zamienia otwarte wyrażenie lambda, które wciąż zawiera pewne „niezdefiniowane” wolne symbole, w
zamknięte , które nie ma już żadnych wolnych symboli.
Na przykład, jeśli masz następujące wyrażenie lambda:, λx.x/y+2
symbol x
jest związany, podczas gdy symbol y
jest wolny, dlatego wyrażenie jest open
i nie może być ocenione, chyba że powiesz co y
oznacza (i to samo z +
i 2
, które są również wolne). Ale załóżmy, że masz również takie środowisko :
{ y: 3,
+: [built-in addition],
2: [built-in number],
q: 42,
w: 5 }
To środowisko zaopatrzenie definicje dla wszystkich „niezdefiniowana” (wolnych) symboli z naszego wyrażenia lambda ( y
, +
, 2
), a także kilka dodatkowych symboli ( q
, w
). Symbole, które musimy zdefiniować to następujący podzbiór środowiska:
{ y: 3,
+: [built-in addition],
2: [built-in number] }
i to jest właśnie zamknięcie naszego wyrażenia lambda:>
Innymi słowy, zamyka otwarte wyrażenie lambda. Stąd właśnie wzięło się zamknięcie nazwy i dlatego odpowiedzi tak wielu osób w tym wątku nie są całkiem poprawne: P
Cóż, korporacyjne marketoidy Sun / Oracle, Microsoft, Google itp. Są winne, ponieważ tak nazywają te konstrukcje w swoich językach (Java, C #, Go itp.). Często nazywają „zamknięciami” czymś, co ma być po prostu lambdas. Lub nazywają „zamknięcia” szczególną techniką stosowaną w celu zaimplementowania zakresu leksykalnego, to znaczy fakt, że funkcja może uzyskać dostęp do zmiennych, które zostały zdefiniowane w jej zewnętrznym zakresie w momencie jej definicji. Często mówią, że funkcja „zamyka” te zmienne, tzn. Przechwytuje je w jakiejś strukturze danych, aby uchronić je przed zniszczeniem po zakończeniu wykonywania funkcji zewnętrznej. Ale to tylko wymyślone post factum „etymologia folklorystyczna” i marketing, co tylko bardziej komplikuje sytuację,
I jest jeszcze gorzej, ponieważ w tym, co mówią, zawsze jest trochę prawdy, co nie pozwala łatwo odrzucić to jako fałszywe: P Pozwól mi wyjaśnić:
Jeśli chcesz zaimplementować język, w którym lambdas są pierwszorzędnymi obywatelami, musisz pozwolić im używać symboli zdefiniowanych w ich otaczającym kontekście (to znaczy, aby używać wolnych zmiennych w twoich lambdach). Symbole te muszą istnieć nawet po powrocie otaczającej funkcji. Problem polega na tym, że te symbole są powiązane z pewnym lokalnym przechowywaniem funkcji (zwykle na stosie wywołań), którego już nie będzie, gdy funkcja powróci. Dlatego, aby lambda działała w oczekiwany sposób, musisz jakoś „uchwycić” wszystkie te zmienne wolne z zewnętrznego kontekstu i zachować je na później, nawet gdy zewnętrzny kontekst zniknie. Oznacza to, że musisz znaleźć zamknięcietwojej lambda (wszystkie te zmienne zewnętrzne, których używa) i przechowuj ją gdzie indziej (albo przez wykonanie kopii, albo przez przygotowanie dla nich miejsca z góry, gdzie indziej niż na stosie). Rzeczywistą metodą stosowaną do osiągnięcia tego celu jest „szczegół implementacji” Twojego języka. Ważne jest tutaj zamknięcie , czyli zbiór wolnych zmiennych z środowiska twojej lambda, które trzeba gdzieś zapisać.
Nie trzeba było długo czekać, aby ludzie zaczęli nazywać rzeczywistą strukturę danych, której używają w implementacjach swojego języka, aby zaimplementować zamknięcie jako „zamknięcie”. Struktura zwykle wygląda mniej więcej tak:
Closure {
[pointer to the lambda function's machine code],
[pointer to the lambda function's environment]
}
a te struktury danych są przekazywane jako parametry do innych funkcji, zwracane z funkcji i przechowywane w zmiennych, w celu reprezentowania lambdas i umożliwiając im dostęp do otaczającego środowiska, a także kodu maszynowego do działania w tym kontekście. Ale to tylko sposób (jeden z wielu) na wdrożenie zamknięcia, a nie samego zamknięcia.
Jak wyjaśniłem powyżej, zamknięcie wyrażenia lambda jest podzbiorem definicji w jego środowisku, które nadają wartości wolnym zmiennym zawartym w tym wyrażeniu lambda, skutecznie zamykając wyrażenie (przekształcając otwarte wyrażenie lambda, którego nie można jeszcze ocenić, w za zamknięty ekspresji lambda, które następnie mogą być ocenione, ponieważ wszystkie symbole zawarte w nim obecnie zdefiniowane).
Wszystko inne to tylko „kult ładunku” i „magia voo-doo” programistów i sprzedawców językowych nieświadomych prawdziwych korzeni tych pojęć.
Mam nadzieję, że to odpowiada na twoje pytania. Ale jeśli masz dodatkowe pytania, zadaj je w komentarzach, a ja postaram się to lepiej wyjaśnić.
Kiedy większość ludzi myśli o funkcjach , myśli o nazwanych funkcjach :
function foo() { return "This string is returned from the 'foo' function"; }
Są one oczywiście nazywane po imieniu:
foo(); //returns the string above
Dzięki wyrażeniom lambda możesz mieć anonimowe funkcje :
@foo = lambda() {return "This is returned from a function without a name";}
W powyższym przykładzie możesz wywołać lambda poprzez zmienną, do której została przypisana:
foo();
Bardziej przydatne niż przypisywanie funkcji anonimowych do zmiennych, jednak przekazują je do lub z funkcji wyższego rzędu, tj. Funkcji, które akceptują / zwracają inne funkcje. W wielu przypadkach nazywanie funkcji jest niepotrzebne:
function filter(list, predicate)
{ @filteredList = [];
for-each (@x in list) if (predicate(x)) filteredList.add(x);
return filteredList;
}
//filter for even numbers
filter([0,1,2,3,4,5,6], lambda(x) {return (x mod 2 == 0)});
Zamknięcie może być nazwane lub funkcja anonimowa, ale są znane jako aktywny przy „zamykane na” zmienne w zakresie, w którym funkcja jest określona, to znaczy zamknięcie nadal znajduje się w środowisku z żadnym z parametrów zewnętrznych, które są stosowane w samo zamknięcie. Oto nazwane zamknięcie:
@x = 0;
function incrementX() { x = x + 1;}
incrementX(); // x now equals 1
Nie wydaje się to wiele, ale co, jeśli to wszystko było w innej funkcji i przeszedłeś incrementX
na funkcję zewnętrzną?
function foo()
{ @x = 0;
function incrementX()
{ x = x + 1;
return x;
}
return incrementX;
}
@y = foo(); // y = closure of incrementX over foo.x
y(); //returns 1 (y.x == 0 + 1)
y(); //returns 2 (y.x == 1 + 1)
W ten sposób uzyskujesz obiekty stanowe w programowaniu funkcjonalnym. Ponieważ nazwanie „incrementX” nie jest potrzebne, w tym przypadku możesz użyć lambda:
function foo()
{ @x = 0;
return lambda()
{ x = x + 1;
return x;
};
}
Nie wszystkie zamknięcia są lambdas i nie wszystkie lambdas są zamknięciami. Obie są funkcjami, ale niekoniecznie w sposób, w jaki jesteśmy przyzwyczajeni.
Lambda jest zasadniczo funkcją zdefiniowaną wewnętrznie, a nie standardową metodą deklarowania funkcji. Baranki można często przekazywać jako przedmioty.
Zamknięcie jest funkcją, która otacza stan otaczający, odwołując się do pól zewnętrznych względem jego ciała. Stan zamknięty pozostaje w obrębie inwokacji zamknięcia.
W języku zorientowanym obiektowo zamknięcia są zwykle dostarczane przez obiekty. Jednak niektóre języki OO (np. C #) implementują specjalną funkcjonalność, która jest bliższa definicji zamknięć zapewnianych przez języki wyłącznie funkcjonalne (takie jak lisp), które nie mają obiektów do zamknięcia stanu.
Co ciekawe, wprowadzenie Lambdas i Closures w C # zbliża funkcjonalne programowanie do głównego nurtu.
To takie proste: lambda jest konstrukcją językową, tj. Po prostu składnią anonimowych funkcji; zamknięcie jest techniką jego wdrożenia - lub dowolnymi funkcjami najwyższej klasy, w tym przypadku nazwanymi lub anonimowymi.
Dokładniej, zamknięcie polega na tym, jak reprezentowana jest funkcja pierwszej klasy w czasie wykonywania, jako para jej „kodu” i środowisko „zamykające się” na wszystkie zmienne nielokalne używane w tym kodzie. W ten sposób zmienne te są nadal dostępne, nawet jeśli zewnętrzne zakresy, z których pochodzą, zostały już opuszczone.
Niestety istnieje wiele języków, które nie obsługują funkcji jako wartości najwyższej klasy lub obsługują je tylko w okaleczonej formie. Dlatego ludzie często używają terminu „zamknięcie”, aby rozróżnić „rzeczywistość”.
Z punktu widzenia języków programowania są to dwie różne rzeczy.
Zasadniczo dla pełnego języka Turinga potrzebujemy tylko bardzo ograniczonych elementów, np. Abstrakcji, aplikacji i redukcji. Abstrakcja i aplikacja zapewniają sposób budowania wyrażenia lamdba, a redukcja określa znaczenie wyrażenia lambda.
Lambda zapewnia sposób na streszczenie procesu obliczeniowego. na przykład, aby obliczyć sumę dwóch liczb, można wyodrębnić proces, który przyjmuje dwa parametry x, y i zwraca x + y. W schemacie możesz to zapisać jako
(lambda (x y) (+ x y))
Możesz zmienić nazwę parametrów, ale zadanie, które wykonuje, nie ulega zmianie. W prawie wszystkich językach programowania można nadać wyrażeniu lambda nazwę, która nazywa się funkcją. Ale nie ma dużej różnicy, można je koncepcyjnie traktować jako cukier składniowy.
OK, teraz wyobraź sobie, jak można to zaimplementować. Ilekroć stosujemy wyrażenie lambda do niektórych wyrażeń, np
((lambda (x y) (+ x y)) 2 3)
Możemy po prostu zastąpić parametry wyrażeniem do oceny. Ten model jest już bardzo wydajny. Ale ten model nie pozwala nam zmieniać wartości symboli, np. Nie możemy naśladować zmiany statusu. Potrzebujemy zatem bardziej złożonego modelu. Krótko mówiąc, ilekroć chcemy obliczyć znaczenie wyrażenia lambda, umieszczamy parę symboli i odpowiednią wartość w środowisku (lub tabeli). Następnie reszta (+ xy) jest oceniana poprzez wyszukiwanie odpowiednich symboli w tabeli. Teraz, jeśli dostarczymy prymitywów do bezpośredniego działania w środowisku, możemy modelować zmiany statusu!
Na tym tle sprawdź tę funkcję:
(lambda (x y) (+ x y z))
Wiemy, że kiedy ocenimy wyrażenie lambda, xy zostanie powiązane w nowej tabeli. Ale jak i gdzie możemy szukać? W rzeczywistości z jest nazywany zmienną swobodną. Musi istnieć środowisko zewnętrzne, które zawiera z. W przeciwnym razie znaczenia wyrażenia nie można ustalić, wiążąc tylko xiy. Aby to wyjaśnić, możesz napisać w schemacie coś w następujący sposób:
((lambda (z) (lambda (x y) (+ x y z))) 1)
Więc z będzie związany z 1 w tabeli zewnętrznej. Nadal otrzymujemy funkcję, która akceptuje dwa parametry, ale jej prawdziwe znaczenie zależy również od środowiska zewnętrznego. Innymi słowy, środowisko zewnętrzne zamyka wolne zmienne. Za pomocą set! Możemy sprawić, by funkcja była stanowa, tzn. Nie jest to funkcja w sensie matematycznym. To, co zwraca, zależy nie tylko od danych wejściowych, ale również od Z.
Jest to coś, co już znasz bardzo dobrze, metoda obiektów prawie zawsze opiera się na stanie obiektów. Dlatego niektórzy twierdzą, że „zamknięcia są obiektami biednego człowieka”. Możemy jednak uznać przedmioty za zamknięcia biednego człowieka, ponieważ naprawdę lubimy funkcje pierwszej klasy.
Używam schematu do zilustrowania pomysłów, ponieważ ten schemat jest jednym z najwcześniejszych języków, który ma prawdziwe zamknięcia. Wszystkie materiały tutaj przedstawione są znacznie lepiej przedstawione w rozdziale 3 SICP.
Podsumowując, lambda i zamknięcie to naprawdę różne pojęcia. Lambda jest funkcją. Zamknięcie to para lambda i odpowiednie środowisko, które zamyka lambda.
Koncepcja jest taka sama, jak opisano powyżej, ale jeśli pochodzisz z PHP, wyjaśnij to dalej za pomocą kodu PHP.
$input = array(1, 2, 3, 4, 5);
$output = array_filter($input, function ($v) { return $v > 2; });
funkcja ($ v) {return $ v> 2; } to definicja funkcji lambda. Możemy nawet przechowywać go w zmiennej, aby można go było ponownie użyć:
$max = function ($v) { return $v > 2; };
$input = array(1, 2, 3, 4, 5);
$output = array_filter($input, $max);
Co teraz, jeśli chcesz zmienić maksymalną liczbę dozwoloną w filtrowanej tablicy? Musisz napisać inną funkcję lambda lub utworzyć zamknięcie (PHP 5.3):
$max_comp = function ($max) {
return function ($v) use ($max) { return $v > $max; };
};
$input = array(1, 2, 3, 4, 5);
$output = array_filter($input, $max_comp(2));
Zamknięcie jest funkcją ocenianą we własnym środowisku, które ma jedną lub więcej zmiennych powiązanych, do których można uzyskać dostęp po wywołaniu funkcji. Pochodzą ze świata programowania funkcjonalnego, w którym występuje wiele koncepcji. Zamknięcia są jak funkcje lambda, ale mądrzejsze w tym sensie, że mają możliwość interakcji ze zmiennymi ze środowiska zewnętrznego, w którym zdefiniowano zamknięcie.
Oto prostszy przykład zamknięcia PHP:
$string = "Hello World!";
$closure = function() use ($string) { echo $string; };
$closure();
To pytanie jest stare i ma wiele odpowiedzi.
Teraz, gdy Java 8 i Official Lambda są nieoficjalnymi projektami zamykającymi, ożywia to pytanie.
Odpowiedź w kontekście Java (za pomocą Lambdas i zamknięć - jaka jest różnica? ):
„Zamknięcie jest wyrażeniem lambda sparowanym ze środowiskiem, które wiąże każdą z wolnych zmiennych z wartością. W Javie wyrażenia lambda będą implementowane za pomocą zamknięć, więc te dwa terminy zaczęły być używane zamiennie w społeczności”.
Mówiąc wprost, zamknięcie jest sztuczką dotyczącą zakresu, lambda jest funkcją anonimową. Możemy bardziej elegancko zrealizować zamknięcie za pomocą lambda, a lambda jest często używana jako parametr przekazywany do wyższej funkcji
Wyrażenie Lambda jest tylko funkcją anonimową. na przykład w zwykłej Javie możesz napisać to w następujący sposób:
Function<Person, Job> mapPersonToJob = new Function<Person, Job>() {
public Job apply(Person person) {
Job job = new Job(person.getPersonId(), person.getJobDescription());
return job;
}
};
gdzie funkcja klasy jest właśnie wbudowana w kod Java. Teraz możesz zadzwonić mapPersonToJob.apply(person)
gdzieś, aby z niego skorzystać. to tylko jeden przykład. To lambda, zanim do tego doszło. Lambdas to skrót.
Zamknięcie:
Lambda staje się zamknięciem, gdy może uzyskać dostęp do zmiennych poza tym zakresem. Myślę, że można powiedzieć, że jest magiczny, może magicznie owijać się wokół środowiska, w którym został stworzony, i używać zmiennych poza swoim zakresem (zakres zewnętrzny. tak więc, dla jasności, zamknięcie oznacza, że lambda może uzyskać dostęp do ZAKRESU ZEWNĘTRZNEGO.
w Kotlinie lambda zawsze może uzyskać dostęp do swojego zamknięcia (zmienne, które są w zewnętrznym zakresie)
Zależy to od tego, czy funkcja używa zmiennej zewnętrznej, czy nie wykonuje operacji.
Zmienne zewnętrzne - zmienne zdefiniowane poza zakresem funkcji.
Wyrażenia lambda są bezstanowe, ponieważ wykonywanie operacji zależy od parametrów, zmiennych wewnętrznych lub stałych.
Function<Integer,Integer> lambda = t -> {
int n = 2
return t * n
}
Zamknięcia utrzymują stan, ponieważ używa zmiennych zewnętrznych (tj. Zmiennych zdefiniowanych poza zakresem treści funkcji) wraz z parametrami i stałymi do wykonywania operacji.
int n = 2
Function<Integer,Integer> closure = t -> {
return t * n
}
Gdy Java tworzy zamknięcie, zachowuje zmienną n z funkcją, dzięki czemu można się do niej odwoływać, gdy jest przekazywana do innych funkcji lub używana w dowolnym miejscu.