Rozumiem różnicę między LET i LET * (wiązanie równoległe i sekwencyjne) i jako kwestia teoretyczna ma to doskonały sens. Ale czy jest jakiś przypadek, w którym kiedykolwiek potrzebowałeś LET? W całym moim kodzie Lispa, który ostatnio przeglądałem, możesz zamienić każdy LET na LET * bez zmian.
Edycja: OK, rozumiem, dlaczego jakiś facet wymyślił LET *, prawdopodobnie jako makro, dawno temu. Moje pytanie brzmi, biorąc pod uwagę, że LET * istnieje, czy jest powód, dla którego LET ma pozostać? Czy napisałeś jakiś rzeczywisty kod Lispa, w którym LET * nie działałby tak dobrze, jak zwykły LET?
Nie kupuję argumentu wydajności. Po pierwsze, rozpoznawanie przypadków, w których LET * można skompilować w coś tak wydajnego jak LET, po prostu nie wydaje się takie trudne. Po drugie, w specyfikacji CL jest wiele rzeczy, które po prostu nie wydają się być zaprojektowane z myślą o wydajności. (Kiedy ostatnio widziałeś LOOP z deklaracjami typu? Tak trudno to rozgryźć, że nigdy ich nie widziałem.) Przed testami porównawczymi Dicka Gabriela z późnych lat osiemdziesiątych, CL było po prostu powolne.
Wygląda na to, że jest to kolejny przypadek wstecznej kompatybilności: mądrze, nikt nie chciał ryzykować złamania czegoś tak fundamentalnego jak LET. Takie było moje przeczucie, ale pocieszające jest usłyszeć, że nikt nie ma głupio prostego przypadku, którego mi brakowało, w którym LET sprawiło, że wiele rzeczy było śmiesznie łatwiejszych niż LET *.
źródło
let
jest to potrzebne?” przypomina to trochę pytanie „czy jest przypadek, w którym potrzebne są funkcje z więcej niż jednym argumentem?”.let
ilet*
nie istnieją z powodu pewnego pojęcia wydajności, istnieją, ponieważ pozwalają ludziom komunikować się z innymi ludźmi podczas programowania.Odpowiedzi:
LET
sam w sobie nie jest prawdziwym prymitywem w funkcjonalnym języku programowania , ponieważ można go zastąpićLAMBDA
. Lubię to:(let ((a1 b1) (a2 b2) ... (an bn)) (some-code a1 a2 ... an))
jest podobne do
((lambda (a1 a2 ... an) (some-code a1 a2 ... an)) b1 b2 ... bn)
Ale
(let* ((a1 b1) (a2 b2) ... (an bn)) (some-code a1 a2 ... an))
jest podobne do
((lambda (a1) ((lambda (a2) ... ((lambda (an) (some-code a1 a2 ... an)) bn)) b2)) b1)
Możesz sobie wyobrazić, co jest prostsze.
LET
a nieLET*
.LET
ułatwia zrozumienie kodu. Widzi się kilka powiązań i można czytać każde wiązanie indywidualnie bez potrzeby rozumienia przepływu „efektów” z góry na dół / od lewej do prawej (rebindings). UżywanieLET*
sygnałów do programisty (tego, który czyta kod), że powiązania nie są niezależne, ale istnieje pewien rodzaj przepływu z góry na dół - co komplikuje sprawę.Common Lisp ma regułę, że wartości dla powiązań w programie
LET
są obliczane od lewej do prawej. Po prostu, jak wartości dla wywołania funkcji są oceniane - od lewej do prawej. Jest to więcLET
koncepcyjnie prostsza instrukcja i powinna być używana domyślnie.Typy
LOOP
? Są używane dość często. Istnieje kilka prymitywnych form deklaracji typu, które są łatwe do zapamiętania. Przykład:(LOOP FOR i FIXNUM BELOW (TRUNCATE n 2) do (something i))
Powyżej deklaruje zmienną
i
jakofixnum
.Richard P. Gabriel opublikował swoją książkę o benchmarkach Lispa w 1985 i wtedy te benchmarki były również używane z Lispsami innymi niż CL. Sam Common Lisp był zupełnie nowy w 1985 roku - książka CLtL1, która opisywała język, została właśnie opublikowana w 1984 roku. Nic dziwnego, że implementacje nie były wówczas zbytnio zoptymalizowane. Zaimplementowane optymalizacje były w zasadzie takie same (lub mniej), co poprzednie implementacje (jak MacLisp).
Ale dla
LET
vs.LET*
Główną różnicą jest to, że za pomocą koduLET
jest łatwiejsze do zrozumienia dla ludzi, gdyż klauzule wiążące są niezależne od siebie - zwłaszcza, że to jest zły styl wykorzystać zmienne lewej do prawej oceny (nie ustawienie jako strony efekt).źródło
(low-level-lambda 2 (let ((x (car %args%)) (y (cadr args))) ...)
:)Nie potrzebujesz LET, ale zwykle tego chcesz .
LET sugeruje, że robisz standardowe wiązanie równoległe i nie dzieje się nic trudnego. LET * wprowadza ograniczenia w kompilatorze i sugeruje użytkownikowi, że istnieje powód, dla którego potrzebne są sekwencyjne wiązania. Pod względem stylu LET jest lepszy, gdy nie potrzebujesz dodatkowych ograniczeń nałożonych przez LET *.
Bardziej efektywne może być użycie LET niż LET * (w zależności od kompilatora, optymalizatora itp.):
(Powyższe punkty dotyczą Scheme, innego dialektu LISP. Clisp może się różnić).
źródło
Przychodzę z wymyślonymi przykładami. Porównaj wynik tego:
(print (let ((c 1)) (let ((c 2) (a (+ c 1))) a)))
z wynikiem uruchomienia tego:
(print (let ((c 1)) (let* ((c 2) (a (+ c 1))) a)))
źródło
a
wiązanie odnosi się do zewnętrznej wartościc
. W drugim przykładzie, gdzielet*
pozwala powiązaniom odwoływać się do poprzednich powiązań,a
powiązanie odnosi się do wewnętrznej wartościc
. Logan nie kłamie, że to wymyślony przykład i nawet nie udaje, że jest przydatny. Ponadto wcięcie jest niestandardowe i wprowadza w błąd. W obu przypadkacha
wiązanie powinno znajdować się o jedną spację powyżej, aby wyrównać je zc
„s”, a „ciało” wewnętrznegolet
powinno znajdować się zaledwie dwie spacje odlet
samego siebie.let
, gdy chce się uniknąć konieczności wiązania wtórne (przez co nie znaczy, ja po prostu pierwszy z nich) odnoszą się do pierwszego wiązania, ale nie chcą cień poprzedniego wiązania - używając poprzednią wartość to za inicjowanie jedną Twoje dodatkowe powiązania.W LISP-ie często istnieje chęć użycia najsłabszych możliwych konstrukcji. Niektóre przewodniki stylistyczne podpowiadają, aby używać
=
zamiasteql
, na przykład, gdy wiesz, że porównywane elementy są numeryczne. Często chodzi o to, aby sprecyzować, co masz na myśli, zamiast efektywnie programować komputer.Jednak rzeczywistą poprawę wydajności może przynieść mówienie tylko o tym, co masz na myśli, a nie używanie silniejszych konstrukcji. Jeśli masz inicjalizacje z
LET
, mogą być wykonywane równolegle, podczas gdyLET*
inicjalizacje muszą być wykonywane sekwencyjnie. Nie wiem, czy jakieś implementacje faktycznie to zrobią, ale niektóre mogą równie dobrze w przyszłości.źródło
=
Operator nie jest ani silniejszy, ani słabszy niżeql
. Jest to słabszy test, ponieważ0
jest równy0.0
. Ale jest też silniejszy, ponieważ odrzuca się argumenty nieliczbowe.eq
. Lub jeśli wiesz, że przypisujesz symboliczne miejsce, użyjsetq
. Jednak ta zasada jest również odrzucana przez wielu moich programistów Lispa, którzy chcą po prostu języka wysokiego poziomu bez przedwczesnej optymalizacji.Ostatnio pisałem funkcję dwóch argumentów, w której algorytm jest wyrażany najwyraźniej, jeśli wiemy, który argument jest większy.
(defun foo (a b) (let ((a (max a b)) (b (min a b))) ; here we know b is not larger ...) ; we can use the original identities of a and b here ; (perhaps to determine the order of the results) ...)
Przypuśćmy,
b
była większa, gdybyśmy używanylet*
, mielibyśmy przypadkowo ustawionea
ib
do tej samej wartości.źródło
let
, można to zrobić prościej (i wyraźnie) za pomocą:(rotatef x y)
- niezły pomysł, ale nadal wydaje się, że to rozciągnięcie.Główną różnicą w Common List pomiędzy LET i LET * jest to, że symbole w LET są połączone równolegle, aw LET * są powiązane sekwencyjnie. Użycie LET nie pozwala na równoległe wykonywanie formularzy init ani na zmianę kolejności formularzy init. Powodem jest to, że Common Lisp pozwala funkcjom wywoływać skutki uboczne. Dlatego kolejność ocen jest ważna i zawsze w formularzu jest od lewej do prawej. Tak więc w LET, formularze init są oceniane jako pierwsze, od lewej do prawej, a następnie tworzone są wiązania, równolegle od
lewej do prawej. W LET * obliczana jest forma inicjująca, a następnie wiązana z symbolem po kolei, od lewej do prawej.CLHS: Operator specjalny LET, LET *
źródło
LET
, nawet jeśli masz rację, mówiąc , że formularze init są wykonywane szeregowo. Nie wiem, czy ma to jakąś praktyczną różnicę w jakiejkolwiek istniejącej implementacji.I pójść o krok dalej i użyć wiążą która jednoczy
let
,let*
,multiple-value-bind
,destructuring-bind
itd., i to nawet rozszerzalny.ogólnie lubię używać „najsłabszej konstrukcji”, ale nie z
let
& przyjaciółmi, ponieważ po prostu zakłócają kod (ostrzeżenie o subiektywności! nie ma potrzeby przekonywania mnie o czymś przeciwnym ...)źródło
Przypuszczalnie użycie
let
kompilatora zapewnia większą elastyczność w zmianie kolejności kodu, być może w celu zwiększenia przestrzeni lub szybkości.Pod względem stylistycznym użycie równoległych powiązań wskazuje na zamiar zgrupowania powiązań; jest to czasami używane do zachowywania dynamicznych powiązań:
(let ((*PRINT-LEVEL* *PRINT-LEVEL*) (*PRINT-LENGTH* *PRINT-LENGTH*)) (call-functions that muck with the above dynamic variables))
źródło
LET
czyLET*
. Więc jeśli używasz*
, dodajesz niepotrzebny glif. GdybyLET*
był to segregator równoległy iLET
byłby to segregator szeregowy, programiści nadal używaliby goLET
i wyciągaliby tylkoLET*
wtedy , gdy chcą równoległego wiązania. To prawdopodobnie spowodowałobyLET*
rzadkość.(let ((list (cdr list)) (pivot (car list))) ;quicksort )
Oczywiście to zadziałałoby:
(let* ((rest (cdr list)) (pivot (car list))) ;quicksort )
I to:
(let* ((pivot (car list)) (list (cdr list))) ;quicksort )
Ale liczy się myśl.
źródło
OP pyta „kiedykolwiek potrzebował LET”?
Kiedy utworzono Common Lisp, załadowano istniejący kod Lisp w różnych dialektach. Założeniem, które zaakceptowali twórcy Common Lisp, było stworzenie dialektu Lispa, który zapewniłby wspólną płaszczyznę. „Potrzebowali”, aby portowanie istniejącego kodu do Common Lisp było łatwe i atrakcyjne. Pozostawienie LET lub LET * poza językiem mogłoby służyć innym cnotom, ale zignorowałoby ten kluczowy cel.
Używam LET zamiast LET *, ponieważ mówi czytelnikowi o tym, jak rozwija się przepływ danych. Przynajmniej w moim kodzie, jeśli widzisz LET *, wiesz, że wartości związane wcześnie zostaną użyte w późniejszym powiązaniu. Czy „muszę” to zrobić, nie; ale myślę, że to pomocne. To powiedziawszy, rzadko czytałem kod domyślny LET *, a pojawienie się LET sygnalizuje, że autor naprawdę tego chciał. To znaczy, na przykład, aby zamienić znaczenie dwóch zmiennych.
(let ((good bad) (bad good) ...)
Istnieje dyskusyjny scenariusz, który zbliża się do „rzeczywistych potrzeb”. Powstaje z makrami. To makro:
(defmacro M1 (a b c) `(let ((a ,a) (b ,b) (c ,c)) (f a b c)))
działa lepiej niż
(defmacro M2 (a b c) `(let* ((a ,a) (b ,b) (c ,c)) (f a b c)))
ponieważ (M2 cba) nie zadziała. Ale te makra są dość niechlujne z różnych powodów; co podważa argument „rzeczywistej potrzeby”.
źródło
Oprócz odpowiedzi Rainera Joswiga iz purystycznego lub teoretycznego punktu widzenia. Niech & Let * reprezentuje dwa paradygmaty programowania; odpowiednio funkcjonalne i sekwencyjne.
A jeśli chodzi o to, dlaczego powinienem po prostu używać Let * zamiast Let, no cóż, odbierasz mi radość z powrotu do domu i myślenia czysto funkcjonalnym językiem, w przeciwieństwie do języka sekwencyjnego, z którym spędzam większość dnia na pracy :)
źródło
Z Pozwól używać łączenia równoległego,
(setq my-pi 3.1415) (let ((my-pi 3) (old-pi my-pi)) (list my-pi old-pi)) => (3 3.1415)
A dzięki łączeniu szeregowemu Let *,
(setq my-pi 3.1415) (let* ((my-pi 3) (old-pi my-pi)) (list my-pi old-pi)) => (3 3)
źródło
let
Operator wprowadza jednolite środowisko dla wszystkich powiązań, które to określa.let*
, przynajmniej koncepcyjnie (i jeśli przez chwilę zignorujemy deklaracje) wprowadza wiele środowisk:To jest do powiedzenia:
(let* (a b c) ...)
jest jak:
(let (a) (let (b) (let (c) ...)))
Więc w pewnym sensie
let
jest bardziej prymitywny, podczas gdylet*
jest cukrem syntaktycznym do pisania kaskadylet
-s.Ale nieważne. (A poniżej podam uzasadnienie, dlaczego nie powinniśmy „nieważne”). Faktem jest, że istnieją dwa operatory iw „99%” kodu nie ma znaczenia, którego użyjesz. Powodem do preferowania
let
przezlet*
to po prostu, że nie ma*
wiszących na końcu.Jeśli masz dwa operatory, a jeden z nich ma
*
zwisającą nazwę, użyj tego bez,*
jeśli działa w takiej sytuacji, aby kod był mniej brzydki.To wszystko.
Biorąc to pod uwagę, podejrzewam, że gdyby
let
ilet*
wymienili ich znaczenia, prawdopodobnie byłoby to rzadziej używanelet*
niż teraz. Zachowanie wiązania szeregowego nie będzie przeszkadzać większości kodu, który tego nie wymaga: rzadko musiałobylet*
być używane do żądania zachowania równoległego (a takie sytuacje można również naprawić, zmieniając nazwy zmiennych, aby uniknąć cieniowania).A teraz ta obiecana dyskusja. Chociaż
let*
koncepcyjnie wprowadza wiele środowisk, bardzo łatwo jest skompilowaćlet*
konstrukcję, aby wygenerować jedno środowisko. Więc nawet jeśli (przynajmniej jeśli zignorujemy deklaracje ANSI CL), istnieje algebraiczna równość, że pojedynczylet*
odpowiada wielu zagnieżdżonymlet
-s, co sprawia, żelet
wygląda bardziej prymitywnie, nie ma powodu, aby faktycznie rozszerzaćlet*
golet
do kompilacji, a nawet zły pomysł.Jeszcze jedno: zauważ, że Common Lisp
lambda
faktycznie używalet*
semantyki -podobnie jak! Przykład:(lambda (x &optional (y x) (z (+1 y)) ...)
tutaj
x
init-form doy
dostępu do wcześniejszegox
parametru i podobnie(+1 y)
odwołuje się do wcześniejszegoy
opcjonalnego. W tym obszarze języka widać wyraźną preferencję dla sekwencyjnej widoczności w powiązaniach. Byłoby mniej przydatne, gdyby formularze w alambda
nie widziały parametrów; opcjonalny parametr nie mógł być zwięźle domyślny pod względem wartości poprzednich parametrów.źródło
W obszarze
let
wszystkie wyrażenia inicjalizujące zmienne widzą dokładnie to samo środowisko leksykalne: to, które otaczalet
. Jeśli te wyrażenia przechwytują domknięcia leksykalne, wszystkie mogą współużytkować ten sam obiekt środowiska.Poniżej
let*
każde wyrażenie inicjujące znajduje się w innym środowisku. Dla każdego kolejnego wyrażenia należy rozszerzyć środowisko, aby utworzyć nowe. Przynajmniej w semantyce abstrakcyjnej, jeśli przechwytuje się domknięcia, mają one różne obiekty środowiska.let*
Musi być dobrze zoptymalizowany, aby zwinąć niepotrzebne rozszerzenia środowiska w celu nadaje się jako zamiennik dla codziennegolet
. Musi istnieć kompilator, który działa, które formy uzyskują dostęp, a następnie konwertuje wszystkie niezależne na większe, połączonelet
.(Jest to prawdą, nawet jeśli
let*
jest to tylko operator makra, który emitujelet
formularze kaskadowe ; optymalizacja jest wykonywana na tych kaskadowychlet
).Nie możesz zaimplementować
let*
jako pojedynczy naiwnylet
, z ukrytymi przypisaniami zmiennych do wykonania inicjalizacji, ponieważ zostanie ujawniony brak odpowiedniego zakresu:(let* ((a (+ 2 b)) ;; b is visible in surrounding env (b (+ 3 a))) forms)
Jeśli to zostanie zamienione na
(let (a b) (setf a (+ 2 b) b (+ 3 a)) forms)
to nie zadziała w tym przypadku; wewnętrzna część
b
zacienia zewnętrzną,b
więc w końcu dodajemy 2 donil
. Tego rodzaju transformację można przeprowadzić, jeśli zmienimy alfabetycznie nazwy wszystkich tych zmiennych. Środowisko jest wtedy ładnie spłaszczone:(let (#:g01 #:g02) (setf #:g01 (+ 2 b) ;; outer b, no problem #:g02 (+ 3 #:g01)) alpha-renamed-forms) ;; a and b replaced by #:g01 and #:g02
W tym celu musimy rozważyć obsługę debugowania; jeśli programista wkracza do tego zakresu leksykalnego za pomocą debugera, czy chcemy, aby zajmował się nim
#:g01
zamiasta
.Zasadniczo
let*
jest to skomplikowana konstrukcja, którą należy dobrze zoptymalizować, aby działała, a takżelet
w przypadkach, gdy można ją zredukować dolet
.Że sam nie uzasadnia sprzyjanie
let
nadlet*
. Załóżmy, że mamy dobry kompilator; dlaczego nie używaćlet*
cały czas?Zgodnie z ogólną zasadą powinniśmy faworyzować konstrukcje wyższego poziomu, które czynią nas produktywnymi i redukują błędy, zamiast podatnych na błędy konstrukcje niższego poziomu i polegać w jak największym stopniu na dobrych implementacjach konstrukcji wyższego poziomu, aby rzadko musieliśmy poświęcać ich wykorzystanie dla celów wydajnościowych. Dlatego przede wszystkim pracujemy w języku takim jak Lisp.
To rozumowanie nie pasuje dobrze do
let
versuslet*
, ponieważlet*
nie jest wyraźnie wyższym poziomem abstrakcji w stosunku dolet
. Są mniej więcej na „równym poziomie”. Dziękilet*
, możesz wprowadzić błąd, który można rozwiązać, po prostu przełączając się nalet
. I odwrotnie .let*
tak naprawdę jest tylko łagodnym cukrem syntaktycznym do wizualnego zapadania sięlet
zagnieżdżenia, a nie znaczącą nową abstrakcją.źródło
Najczęściej używam LET, chyba że konkretnie potrzebuję LET *, ale czasami piszę kod, który wyraźnie potrzebuje LET, zwykle podczas wykonywania różnych (zwykle skomplikowanych) domyślnych ustawień . Niestety nie mam pod ręką żadnego przydatnego przykładu kodu.
źródło
kto ma ochotę ponownie napisać letf vs letf *? liczba połączeń typu „relax-protect”?
łatwiejsze do optymalizacji powiązań sekwencyjnych.
może to wpływa na env ?
pozwala na kontynuacje z dynamicznym zakresem?
czasami (let (xyz) (setq z 0 y 1 x (+ (setq x 1) (prog1 (+ xy) (setq x (1- x))))) (wartości ()))
[Myślę, że to działa] Rzecz w tym, że czasami prostsze jest łatwiejsze do odczytania.
źródło