LET so với LET * trong Common Lisp


80

Tôi hiểu sự khác biệt giữa LET và LET * (liên kết song song so với liên kết tuần tự), và về mặt lý thuyết, nó có ý nghĩa hoàn hảo. Nhưng có trường hợp nào bạn thực sự cần LET không? Trong tất cả mã Lisp mà tôi đã xem gần đây, bạn có thể thay thế mọi LET bằng LET * mà không cần thay đổi.

Chỉnh sửa: OK, tôi hiểu tại sao một số người phát minh ra LET *, có lẽ là macro, quay lại khi nào. Câu hỏi của tôi là, cho rằng LET * tồn tại, có lý do gì để LET ở lại không? Bạn đã viết bất kỳ mã Lisp thực tế nào mà LET * sẽ không hoạt động tốt như LET thuần túy chưa?

Tôi không mua đối số hiệu quả. Đầu tiên, việc nhận ra các trường hợp LET * có thể được biên dịch thành một thứ gì đó hiệu quả như LET dường như không khó lắm. Thứ hai, có rất nhiều thứ trong thông số kỹ thuật CL đơn giản là có vẻ như chúng không được thiết kế xoay quanh tính hiệu quả. (Lần cuối cùng bạn nhìn thấy LOOP với khai báo kiểu là khi nào? Thật khó để tìm ra tôi chưa bao giờ thấy chúng được sử dụng.) Trước các điểm chuẩn của Dick Gabriel vào cuối những năm 1980, CL đã rất chậm chạp.

Có vẻ như đây là một trường hợp tương thích ngược khác: một cách khôn ngoan, không ai muốn mạo hiểm phá vỡ một thứ gì đó cơ bản như LET. Đó là linh cảm của tôi, nhưng thật an ủi khi biết rằng không ai có một trường hợp đơn giản đến mức ngu ngốc mà tôi đã bỏ sót khi LET tạo ra một loạt thứ dễ dàng hơn LET *.


song song là sự lựa chọn từ ngữ kém cỏi; chỉ các ràng buộc trước đó được hiển thị. ràng buộc song song sẽ giống như ràng buộc "... nơi ..." của Haskell.
jrockway

Tôi không nhằm mục đích gây nhầm lẫn; Tôi tin rằng đó là những từ được sử dụng bởi spec. :-)
Ken

12
Song song là chính xác. Có nghĩa là những ràng buộc đi vào cuộc sống cùng một lúc, không nhìn thấy nhau và không phủ bóng nhau. Tại thời điểm không tồn tại một môi trường người dùng có thể nhìn thấy bao gồm một số biến được định nghĩa trong LET, chứ không phải những biến khác.
Kaz

Haskells trong đó các ràng buộc giống letrec hơn. Họ có thể thấy tất cả các ràng buộc trên cùng một mức phạm vi.
Edgar Klerks

1
Hỏi 'có trường hợp nào letcần thiết không?' hơi giống như hỏi 'có trường hợp nào cần các hàm có nhiều hơn một đối số không?'. let& let*không tồn tại do một số khái niệm về hiệu quả mà chúng tồn tại vì chúng cho phép con người truyền đạt ý định với người khác khi lập trình.
tfb

Câu trả lời:


85

LETbản thân nó không phải là một nguyên thủy thực sự trong một Ngôn ngữ Lập trình Chức năng , vì nó có thể được thay thế bằng LAMBDA. Như thế này:

(let ((a1 b1) (a2 b2) ... (an bn))
  (some-code a1 a2 ... an))

tương tự như

((lambda (a1 a2 ... an)
   (some-code a1 a2 ... an))
 b1 b2 ... bn)

Nhưng

(let* ((a1 b1) (a2 b2) ... (an bn))
  (some-code a1 a2 ... an))

tương tự như

((lambda (a1)
    ((lambda (a2)
       ...
       ((lambda (an)
          (some-code a1 a2 ... an))
        bn))
      b2))
   b1)

Bạn có thể tưởng tượng đó là điều đơn giản hơn. LETvà không LET*.

LETgiúp hiểu mã dễ dàng hơn. Người ta nhìn thấy một loạt các ràng buộc và người ta có thể đọc từng ràng buộc riêng lẻ mà không cần phải hiểu luồng 'hiệu ứng' (rebindings) từ trên xuống / trái phải. Sử dụng các LET*tín hiệu cho lập trình viên (người đọc mã) rằng các ràng buộc không độc lập, nhưng có một số loại luồng từ trên xuống - điều này làm phức tạp mọi thứ.

Lisp chung có quy tắc rằng các giá trị cho các liên kết trong LETđược tính từ trái sang phải. Chỉ cách các giá trị cho một lệnh gọi hàm được đánh giá - từ trái sang phải. Vì vậy, LETlà câu lệnh đơn giản hơn về mặt khái niệm và nó nên được sử dụng theo mặc định.

Các loại trongLOOP ? Được sử dụng khá thường xuyên. Có một số dạng khai báo kiểu nguyên thủy rất dễ nhớ. Thí dụ:

(LOOP FOR i FIXNUM BELOW (TRUNCATE n 2) do (something i))

Ở trên khai báo biến ilà a fixnum.

Richard P. Gabriel đã xuất bản cuốn sách của mình về điểm chuẩn Lisp vào năm 1985 và vào thời điểm đó những điểm chuẩn này cũng được sử dụng với các Lisp không phải CL. Bản thân Common Lisp là thương hiệu mới vào năm 1985 - cuốn sách CLtL1 mô tả ngôn ngữ này vừa được xuất bản vào năm 1984. Không có gì ngạc nhiên khi việc triển khai không được tối ưu hóa cho lắm vào thời điểm đó. Các tối ưu hóa được triển khai về cơ bản giống (hoặc ít hơn) mà các triển khai trước đó đã có (như MacLisp).

Nhưng LETso với so với LET*sự khác biệt chính là việc sử dụng mã LETdễ hiểu hơn đối với con người, vì các mệnh đề ràng buộc độc lập với nhau - đặc biệt là vì việc tận dụng đánh giá từ trái sang phải là một kiểu xấu hiệu ứng).


3
Không không! Lambda không phải là một nguyên thủy thực sự vì nó có thể được thay thế bằng LET và lambda cấp thấp hơn chỉ cung cấp một API để truy cập các giá trị đối số: (low-level-lambda 2 (let ((x (car %args%)) (y (cadr args))) ...) :)
Kaz

36

Bạn không cần LET, nhưng bạn thường muốn nó.

LET gợi ý rằng bạn chỉ đang thực hiện ràng buộc song song tiêu chuẩn mà không có gì phức tạp xảy ra. LET * gây ra các hạn chế đối với trình biên dịch và gợi ý cho người dùng rằng có lý do mà các liên kết tuần tự là cần thiết. Về phong cách , LET tốt hơn khi bạn không cần thêm các hạn chế do LET * áp đặt.

Sử dụng LET có thể hiệu quả hơn LET * (tùy thuộc vào trình biên dịch, trình tối ưu hóa, v.v.):

  • các ràng buộc song song có thể được thực thi song song (nhưng tôi không biết liệu có hệ thống LISP nào thực sự làm điều này hay không và các biểu mẫu init vẫn phải được thực thi tuần tự)
  • các ràng buộc song song tạo ra một môi trường mới (phạm vi) cho tất cả các ràng buộc. Các liên kết tuần tự tạo ra một môi trường lồng nhau mới cho mọi liên kết đơn lẻ. Các ràng buộc song song sử dụng ít bộ nhớ hơntra cứu biến nhanh hơn .

(Các gạch đầu dòng trên áp dụng cho Scheme, một phương ngữ LISP khác. Clisp có thể khác.)


2
Lưu ý: Xem câu trả lời này (và / hoặc phần được liên kết của hyperspec) để biết một chút giải thích về lý do tại sao điểm pullet đầu tiên của bạn lại - sai lệch, giả sử. Các ràng buộc xảy ra song song, nhưng các biểu mẫu được thực thi tuần tự - theo thông số kỹ thuật.
lindes

6
thực thi song song không phải là thứ mà tiêu chuẩn Lisp chung xử lý theo bất kỳ cách nào. Việc tra cứu biến nhanh hơn cũng là một huyền thoại.
Rainer Joswig

1
Sự khác biệt không chỉ quan trọng đối với trình biên dịch. Tôi sử dụng let và let * như những gợi ý cho bản thân về những gì đang xảy ra. Khi tôi nhìn thấy let trong mã của mình, tôi biết các ràng buộc là độc lập và khi tôi nhìn thấy let *, tôi biết các ràng buộc phụ thuộc vào nhau. Nhưng tôi chỉ biết điều đó vì tôi đảm bảo sử dụng let và let * nhất quán.
Jeremiah

28

Tôi đến mang các ví dụ giả tạo. So sánh kết quả của điều này:

(print (let ((c 1))
         (let ((c 2)
               (a (+ c 1)))
           a)))

với kết quả của việc chạy này:

(print (let ((c 1))
         (let* ((c 2)
                (a (+ c 1)))
           a)))

3
Chăm sóc để phát triển tại sao lại như vậy?
John McAleely

4
@John: trong ví dụ đầu tiên, aràng buộc của đề cập đến giá trị bên ngoài của c. Trong ví dụ thứ hai, nơi let*cho phép các ràng buộc tham chiếu đến các ràng buộc trước đó, ràng buộc của tham chiếu đến agiá trị bên trong của c. Logan không nói dối về việc đây là một ví dụ giả tạo, và nó thậm chí không giả vờ là hữu ích. Ngoài ra, thụt đầu dòng là không chuẩn và gây hiểu lầm. Trong cả hai, aràng buộc 'phải có một khoảng trắng , để xếp hàng với c' s, và 'phần thân' của phần bên trong letchỉ nên cách letchính nó hai khoảng trắng .
zem 18/10/10

3
Câu trả lời này cung cấp một cái nhìn sâu sắc quan trọng. Người ta sẽ đặc biệt sử dụng letkhi ai muốn tránh có cam kết ràng buộc thứ cấp (bằng cách đó tôi chỉ đơn giản có nghĩa là không phải là người đầu tiên) đề cập đến đầu tiên ràng buộc, nhưng bạn làm muốn shadow một trước ràng buộc - sử dụng giá trị trước đó của nó để khởi tạo một trong những ràng buộc phụ của bạn.
lindes

2
Mặc dù thụt đầu dòng đã tắt (và tôi không thể chỉnh sửa nó), nhưng đây là ví dụ tốt nhất cho đến nay. Khi tôi nhìn vào (để ...) tôi biết rằng không có ràng buộc nào sẽ tạo ra lẫn nhau và có thể được xử lý riêng biệt. Khi tôi xem xét (let * ...), tôi luôn thận trọng tiếp cận và xem xét rất kỹ những ràng buộc nào đang được sử dụng lại. Chỉ vì lý do này, nên luôn sử dụng (let) trừ khi bạn thực sự cần lồng.
andrew

1
(6 năm sau ...) có phải là sự thụt lề sai và gây hiểu nhầm theo thiết kế, nhằm mục đích như một gotcha ? Tôi có khuynh hướng chỉnh sửa nó để sửa chữa nó ... Tôi có nên không?
Will Ness

11

Trong LISP, thường có mong muốn sử dụng các cấu trúc yếu nhất có thể. Ví dụ, một số hướng dẫn kiểu sẽ yêu cầu bạn sử dụng =thay vì eqlkhi bạn biết các mục được so sánh là số. Ý tưởng thường là chỉ rõ ý bạn hơn là lập trình máy tính một cách hiệu quả.

Tuy nhiên, có thể có những cải thiện về hiệu quả thực tế khi chỉ nói ý của bạn và không sử dụng các cấu trúc mạnh hơn. Nếu bạn có các lần khởi tạo LET, chúng có thể được thực thi song song, trong khi các lần LET*khởi tạo phải được thực hiện tuần tự. Tôi không biết liệu có bất kỳ triển khai nào thực sự làm được điều đó không, nhưng một số có thể tốt trong tương lai.


2
Điểm tốt. Mặc dù Lisp là một ngôn ngữ cấp cao như vậy, điều đó chỉ khiến tôi tự hỏi tại sao "cấu trúc yếu nhất có thể" lại là một phong cách mong muốn như vậy ở vùng đất Lisp. Bạn không thấy các lập trình viên Perl nói "tốt, chúng tôi không cần sử dụng regexp ở đây ..." :-)
Ken

1
Tôi không biết, nhưng có một sở thích phong cách nhất định. Nó bị phản đối phần nào bởi những người (như tôi), những người thích sử dụng cùng một hình thức càng nhiều càng tốt (tôi hầu như không bao giờ viết setq thay vì setf). Nó có thể liên quan đến một ý tưởng để nói lên ý của bạn.
David Thornley

Các =nhà điều hành không phải là mạnh hay yếu hơn eql. Nó là một bài kiểm tra yếu hơn vì 0bằng với 0.0. Nhưng nó cũng mạnh hơn vì các đối số không phải số bị bác bỏ.
Kaz

2
Nguyên tắc bạn đang đề cập đến là sử dụng nguyên thủy mạnh nhất có thể áp dụng được, không phải yếu nhất . Ví dụ, nếu những thứ được so sánh là biểu tượng, hãy sử dụng eq. Hoặc nếu bạn biết rằng bạn đang chỉ định sử dụng một địa điểm tượng trưng setq. Tuy nhiên, nguyên tắc này cũng bị từ chối bởi nhiều lập trình viên Lisp của tôi, những người chỉ muốn có một ngôn ngữ cấp cao mà không cần tối ưu hóa sớm.
Kaz

1
trên thực tế, CLHS nói "các ràng buộc [được thực hiện] song song" nhưng "các biểu thức init-form-1 , init-form-2 , v.v., [được đánh giá] trong [cụ thể, trái sang phải (hoặc trên cùng -xuống)] đặt hàng ”. Vì vậy các giá trị phải được tính toán tuần tự (các ràng buộc được thiết lập sau khi tất cả các giá trị đã được tính toán). Cũng có lý, vì đột biến cấu trúc giống RPLACD là một phần của ngôn ngữ và với tính song song thực sự, nó sẽ trở nên không xác định.
Will Ness

9

Gần đây tôi đã viết một hàm gồm hai đối số, trong đó thuật toán được thể hiện rõ ràng nhất nếu chúng ta biết đối số nào lớn hơn.

(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)
  ...)

Giả sử blớn hơn, nếu chúng tôi đã sử dụng let*, chúng tôi đã vô tình đặt abvề cùng một giá trị.


1
Trừ khi bạn cần các giá trị của x và y sau đó ở bên trong bên ngoài let, điều này có thể được thực hiện đơn giản hơn (và rõ ràng) với: (rotatef x y)- không phải là một ý tưởng tồi, nhưng nó vẫn có vẻ như là một sự căng thẳng.
Ken

Đúng. Nó có thể hữu ích hơn nếu x và y là các biến đặc biệt.
Samuel Edwin Ward

9

Sự khác biệt chính trong Danh sách chung giữa LET và LET * là các ký hiệu trong LET được ràng buộc song song và trong LET * được ràng buộc tuần tự. Việc sử dụng LET không cho phép thực hiện song song các biểu mẫu init cũng như không cho phép thay đổi thứ tự của các biểu mẫu init. Lý do là Common Lisp cho phép các chức năng có tác dụng phụ. Do đó, thứ tự đánh giá là quan trọng và luôn từ trái sang phải trong một biểu mẫu. Do đó, trong LET, các init-form được đánh giá đầu tiên, từ trái sang phải, sau đó các ràng buộc được tạo song song từ trái sang phải . Trong LET *, init-form được đánh giá và sau đó liên kết với biểu tượng theo thứ tự, từ trái sang phải.

CLHS: Toán tử đặc biệt LET, LET *


2
Có vẻ như câu trả lời này có lẽ đang rút ra một số năng lượng từ phản ứng cho câu trả lời này ? Ngoài ra, theo thông số kỹ thuật được liên kết, các ràng buộc được cho là được thực hiện song song LET, mặc dù bạn đúng khi gọi rằng các biểu mẫu init được thực thi theo chuỗi. Cho dù điều đó có bất kỳ sự khác biệt thực tế nào trong bất kỳ triển khai hiện tại nào hay không, tôi không biết.
lindes

8

tôi đi thêm một bước nữa và sử dụng ràng buộc mà thống nhất let, let*, multiple-value-bind, destructuring-bindvv, và nó thậm chí còn mở rộng.

nói chung tôi thích sử dụng "cấu trúc yếu nhất", nhưng không phải với let& bạn bè vì họ chỉ gây nhiễu cho mã (cảnh báo chủ quan! không cần phải thử thuyết phục tôi về điều ngược lại ...)


Ồ, gọn gàng. Bây giờ tôi sẽ chơi với BIND. Cảm ơn các liên kết!
Ken

4

Có lẽ bằng cách sử dụng lettrình biên dịch sẽ linh hoạt hơn trong việc sắp xếp lại mã, có lẽ để cải thiện không gian hoặc tốc độ.

Về mặt phong cách, việc sử dụng các ràng buộc song song cho thấy ý định rằng các ràng buộc được nhóm lại với nhau; điều này đôi khi được sử dụng để giữ lại các ràng buộc động:

(let ((*PRINT-LEVEL* *PRINT-LEVEL*)
      (*PRINT-LENGTH* *PRINT-LENGTH*))
  (call-functions that muck with the above dynamic variables)) 

Trong 90% mã, nó không có sự khác biệt cho dù bạn sử dụng LEThay LET*. Vì vậy, nếu bạn sử dụng *, bạn đang thêm một glyph không cần thiết. Nếu LET*là chất kết dính song song và LETlà chất kết dính nối tiếp, các lập trình viên sẽ vẫn sử dụng LETvà chỉ rút ra LET*khi muốn liên kết song song. Điều này có thể sẽ LET*hiếm.
Kaz

trên thực tế, CLHS chỉ định thứ tự đánh giá của let init-form s.
Will Ness

4
(let ((list (cdr list))
      (pivot (car list)))
  ;quicksort
 )

Tất nhiên, điều này sẽ hoạt động:

(let* ((rest (cdr list))
       (pivot (car list)))
  ;quicksort
 )

Và điều này:

(let* ((pivot (car list))
       (list (cdr list)))
  ;quicksort
 )

Nhưng đó là suy nghĩ quan trọng.


2

OP hỏi "bao giờ thực sự cần LET"?

Khi Common Lisp được tạo ra, có một lượng thuyền chứa mã Lisp hiện có bằng các loại phương ngữ. Tóm tắt ngắn gọn mà những người thiết kế Common Lisp chấp nhận là tạo ra một phương ngữ của Lisp để cung cấp điểm chung. Họ "cần" làm cho việc chuyển mã hiện có vào Common Lisp dễ dàng và hấp dẫn. Bỏ LET hoặc LET * ra khỏi ngôn ngữ có thể phục vụ một số đức tính khác, nhưng nó sẽ bỏ qua mục tiêu chính đó.

Tôi sử dụng LET thay vì LET * vì nó cho người đọc biết điều gì đó về cách luồng dữ liệu đang diễn ra. Trong mã của tôi, ít nhất, nếu bạn thấy LET *, bạn biết rằng các giá trị được ràng buộc sớm sẽ được sử dụng trong một ràng buộc sau này. Tôi có "cần" làm điều đó không, không; nhưng tôi nghĩ nó hữu ích. Điều đó nói rằng tôi hiếm khi đọc mã mặc định là LET * và sự xuất hiện của LET báo hiệu rằng tác giả thực sự muốn nó. Ví dụ, để hoán đổi ý nghĩa của hai vars.

(let ((good bad)
     (bad good)
...)

Có một kịch bản gây tranh cãi tiếp cận 'nhu cầu thực tế'. Nó phát sinh với macro. Macro này:

(defmacro M1 (a b c)
 `(let ((a ,a)
        (b ,b)
        (c ,c))
    (f a b c)))

hoạt động tốt hơn

(defmacro M2 (a b c)
  `(let* ((a ,a)
          (b ,b)
          (c ,c))
    (f a b c)))

vì (M2 cba) sẽ không hoạt động. Nhưng những macro đó khá cẩu thả vì nhiều lý do; để làm suy yếu đối số 'nhu cầu thực tế'.


1

Ngoài câu trả lời của Rainer Joswig , và theo quan điểm thuần túy hoặc lý thuyết. Hãy & Để * đại diện cho hai mô hình lập trình; chức năng và tuần tự tương ứng.

Về lý do tại sao tôi nên tiếp tục sử dụng Let * thay vì Let, tốt, bạn đang tận hưởng niềm vui khi tôi trở về nhà và suy nghĩ bằng ngôn ngữ chức năng thuần túy, trái ngược với ngôn ngữ tuần tự nơi tôi dành phần lớn thời gian trong ngày để làm việc :)


1

Với Cho phép bạn sử dụng liên kết song song,

(setq my-pi 3.1415)

(let ((my-pi 3) (old-pi my-pi))
     (list my-pi old-pi))
=> (3 3.1415)

Và với ràng buộc nối tiếp Let *,

(setq my-pi 3.1415)

(let* ((my-pi 3) (old-pi my-pi))
     (list my-pi old-pi))
=> (3 3)

Vâng, đó là cách chúng được định nghĩa. Nhưng khi nào bạn cần cái trước? Tôi cho rằng bạn không thực sự viết một chương trình cần thay đổi giá trị của số pi theo một thứ tự cụ thể. :-)
Ken

1

Các letnhà điều hành giới thiệu một môi trường duy nhất cho tất cả các ràng buộc mà nó chỉ định. let*, ít nhất là về mặt khái niệm (và nếu chúng ta bỏ qua các khai báo trong giây lát) giới thiệu nhiều môi trường:

Điều đó có nghĩa là:

(let* (a b c) ...)

giống như:

(let (a) (let (b) (let (c) ...)))

Vì vậy, theo một nghĩa nào đó, letlà nguyên thủy hơn, trong khi let*là một đường cú pháp để viết một dãy let-s.

Nhưng đừng bận tâm đến điều đó. (Và tôi sẽ đưa ra lời biện minh ở phần sau cho lý do tại sao chúng ta nên "bỏ qua"). Sự thật là có hai toán tử và trong "99%" của mã, không quan trọng bạn sử dụng toán tử nào. Lý do để thích lethơn let*đơn giản là nó không có *dây treo ở cuối.

Bất cứ khi nào bạn có hai toán tử và một toán tử có tên *lủng lẳng, hãy sử dụng toán tử không có *nếu nó hoạt động trong tình huống đó, để giữ cho mã của bạn bớt xấu xí hơn.

Đó là tất cả để có nó.

Điều đó đang được nói, tôi nghi ngờ rằng nếu letlet*trao đổi ý nghĩa của chúng, nó có thể sẽ hiếm được sử dụng let*hơn bây giờ. Hành vi liên kết nối tiếp sẽ không làm phiền hầu hết các mã không yêu cầu nó: hiếm khi let*phải được sử dụng để yêu cầu hành vi song song (và các tình huống như vậy cũng có thể được khắc phục bằng cách đổi tên các biến để tránh bị bóng).

Bây giờ cho cuộc thảo luận đã hứa. Mặc dù let*về mặt khái niệm giới thiệu nhiều môi trường, nhưng rất dễ dàng để biên dịch let*cấu trúc để tạo ra một môi trường duy nhất. Vì vậy, mặc dù (ít nhất là nếu chúng ta bỏ qua các khai báo ANSI CL), có một đẳng thức đại số mà một đơn vị let*tương ứng với nhiều let-s lồng nhau , điều này làm cho lettrông nguyên thủy hơn, không có lý do gì để thực sự mở rộng let*thành letđể biên dịch nó, và thậm chí ý kiến ​​tồi.

Một điều nữa: lưu ý rằng Common Lisp lambdathực sự sử dụng let*ngữ nghĩa giống như vậy! Thí dụ:

(lambda (x &optional (y x) (z (+1 y)) ...)

ở đây, xinit-form để ytruy cập xtham số trước đó và tương tự (+1 y)tham chiếu đến ytùy chọn trước đó . Trong lĩnh vực ngôn ngữ này, ưu tiên rõ ràng về khả năng hiển thị tuần tự trong ràng buộc thể hiện qua. Sẽ ít hữu ích hơn nếu các biểu mẫu trong a lambdakhông thể thấy các tham số; một tham số tùy chọn không thể được mặc định ngắn gọn theo giá trị của các tham số trước đó.


0

Dưới đây let, tất cả các biểu thức khởi tạo biến đều nhìn thấy chính xác cùng một môi trường từ vựng: môi trường bao quanh let. Nếu những biểu thức đó xảy ra để nắm bắt các bao đóng từ vựng, tất cả chúng có thể chia sẻ cùng một đối tượng môi trường.

Dưới đây let*, mọi biểu thức khởi tạo đều nằm trong một môi trường khác nhau. Đối với mỗi biểu thức kế tiếp, môi trường phải được mở rộng để tạo ra một biểu thức mới. Ít nhất trong ngữ nghĩa trừu tượng, nếu các bao đóng được nắm bắt, chúng có các đối tượng môi trường khác nhau.

A let*phải được tối ưu hóa tốt để thu gọn các phần mở rộng môi trường không cần thiết để phù hợp như một sự thay thế hàng ngày cho let. Phải có một trình biên dịch hoạt động mà các biểu mẫu đang truy cập những gì và sau đó chuyển đổi tất cả các biểu mẫu độc lập thành lớn hơn, kết hợp let.

(Điều này đúng ngay cả khi let*chỉ là một toán tử macro phát ra các letdạng xếp tầng ; việc tối ưu hóa được thực hiện trên các dạng xếp tầng letđó).

Bạn không thể triển khai let*như một người đơn thuần let, với các phép gán biến ẩn để thực hiện khởi tạo vì việc thiếu phạm vi phù hợp sẽ được tiết lộ:

(let* ((a (+ 2 b))  ;; b is visible in surrounding env
       (b (+ 3 a)))
  forms)

Nếu điều này được biến thành

(let (a b)
  (setf a (+ 2 b)
        b (+ 3 a))
  forms)

nó sẽ không hoạt động trong trường hợp này; bên trong phủ bbóng bên ngoài, bvì vậy chúng tôi kết thúc thêm 2 vào nil. Loại biến đổi này có thể được thực hiện nếu chúng ta đổi tên alpha cho tất cả các biến này. Môi trường sau đó được làm phẳng một cách độc đáo:

(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

Vì vậy, chúng tôi cần xem xét hỗ trợ gỡ lỗi; nếu lập trình viên bước vào phạm vi từ vựng này bằng trình gỡ lỗi, chúng tôi có muốn họ xử lý #:g01thay vì a.

Về cơ bản, let*cấu trúc phức tạp phải được tối ưu hóa tốt để thực hiện cũng như lettrong các trường hợp khi nó có thể giảm xuống let.

Điều đó một mình sẽ không biện minh cho sự ưu ái lethơn let*. Giả sử chúng ta có một trình biên dịch tốt; tại sao không sử dụng let*tất cả các thời gian?

Về nguyên tắc chung, chúng ta nên ưu tiên các cấu trúc cấp cao hơn giúp chúng ta làm việc hiệu quả và giảm sai sót, hơn các cấu trúc cấp thấp dễ mắc lỗi và dựa nhiều nhất có thể vào việc triển khai tốt các cấu trúc cấp cao hơn để chúng ta hiếm khi phải hy sinh sử dụng chúng vì lợi ích của hiệu suất. Đó là lý do tại sao chúng tôi đang làm việc bằng một ngôn ngữ như Lisp ngay từ đầu.

Suy luận đó không áp dụng cho letso với so với let*, bởi vì let*rõ ràng không phải là mức trừu tượng cao hơn so với let. Họ về "đẳng cấp ngang hàng". Với let*, bạn có thể giới thiệu một lỗi được giải quyết bằng cách chuyển sang let. Và ngược lại . let*thực sự chỉ là một đường cú pháp nhẹ để letlàm tổ thu gọn trực quan , và không phải là một sự trừu tượng mới đáng kể.


-1

Tôi chủ yếu sử dụng LET, trừ khi tôi đặc biệt cần LET *, nhưng đôi khi tôi viết mã cần LET một cách rõ ràng , thường là khi thực hiện mặc định các loại (thường phức tạp). Thật không may, tôi không có bất kỳ ví dụ mã hữu ích nào trong tay.


-1

ai cảm thấy muốn viết lại letf vs letf * một lần nữa? số lượng cuộc gọi bảo vệ thư giãn?

dễ dàng hơn để tối ưu hóa các ràng buộc tuần tự.

có thể nó ảnh hưởng đến env ?

cho phép liên tục với mức độ động?

đôi khi (let (xyz) (setq z 0 y 1 x (+ (setq x 1) (prog1 (+ xy) (setq x (1- x))))) (giá trị ()))

[Tôi nghĩ điều đó hiệu quả] là, đôi khi đơn giản hơn thì dễ đọc hơn.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.