development

Common Lisp의 LET 대 LET *

big-blog 2020. 10. 9. 11:24
반응형

Common Lisp의 LET 대 LET *


LET와 LET * (병렬 바인딩 대 순차 바인딩)의 차이점을 이해하고 있으며 이론적으로는 완벽하게 이해됩니다. 하지만 실제로 LET가 필요한 경우가 있습니까? 최근에 살펴본 모든 Lisp 코드에서 모든 LET를 변경없이 LET *로 바꿀 수 있습니다.

편집 : 좋아, 어떤 사람이 LET *를 매크로로 만들 었는지 이해 합니다. 내 질문은 LET *가 존재한다는 점을 감안할 때 LET가 주변에 머물러야하는 이유가 있습니까? LET *가 일반 LET만큼 잘 작동하지 않는 실제 Lisp 코드를 작성 했습니까?

나는 효율성 논쟁을 사지 않는다. 첫째, LET *가 LET만큼 효율적인 것으로 컴파일 될 수있는 경우를 인식하는 것은 그렇게 어렵지 않은 것 같습니다. 둘째, CL 사양에는 효율성을 중심으로 설계된 것처럼 보이지 않는 많은 것들이 있습니다. (유형 선언이있는 LOOP를 마지막으로 본 게 언제입니까? 그것들이 사용되는 것을 본 적이 없다는 것을 알아 내기가 너무 어렵습니다.) 1980 년대 후반에 Dick Gabriel의 벤치 마크 이전에 CL 완전히 느 렸습니다.

이것은 이전 버전과의 호환성의 또 다른 경우 인 것 같습니다. 현명하게도, 누구도 LET만큼 근본적인 것을 깨뜨릴 위험을 감수하고 싶지 않았습니다. 내 직감 이었지만, LET가 LET *보다 엄청나게 쉽게 많은 것들을 만들었던 내가 놓친 어리 석고 단순한 케이스가 아무도 없다는 소식을 들으면 위로가됩니다.


LET으로 대체 될 수 있기 때문에 그 자체는 함수형 프로그래밍 언어 에서 실제 원시가 아닙니다 LAMBDA. 이렇게 :

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

~와 비슷하다

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

그러나

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

~와 비슷하다

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

어느 것이 더 간단한 지 상상할 수 있습니다. LET및 아닙니다 LET*.

LET코드를 더 쉽게 이해할 수 있습니다. 하나는 여러 개의 바인딩을보고 하나는 '효과'(리 바인딩)의 하향식 / 왼쪽-오른쪽 흐름을 이해할 필요없이 각 바인딩을 개별적으로 읽을 수 있습니다. 사용 LET*프로그래머 바인딩 독립 아니라는 것을 (코드를 읽는 일)에 신호를하지만, 다운 상단의 어떤 흐름이있다 - 일을 복잡하게한다.

Common Lisp에는 바인딩 값 LET이 왼쪽에서 오른쪽으로 계산 된다는 규칙이 있습니다. 함수 호출의 값이 평가되는 방식-왼쪽에서 오른쪽으로. 그래서 LET개념적으로 더 간단한 문장이며 기본적으로 사용되어야합니다.

유형LOOP ? 꽤 자주 사용됩니다. 기억하기 쉬운 몇 가지 기본 형식 선언 형식이 있습니다. 예:

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

위의 변수 ifixnum.

Richard P. Gabriel은 1985 년에 Lisp 벤치 마크에 대한 저서를 출판했으며 당시 이러한 벤치 마크는 비 CL Lisps에서도 사용되었습니다. Common Lisp 자체는 1985 년에 완전히 새로워졌습니다 . 언어를 설명 하는 CLtL1 책은 1984 년에 막 출판되었습니다. 그 당시 구현이 그다지 최적화되지 않은 것은 당연합니다. 구현 된 최적화는 기본적으로 이전 구현 (예 : MacLisp)과 동일하거나 그 이하였습니다.

그러나 대한 LETLET*주요 차이점 사용하여 해당 코드는 LET이 측면으로 올바른 평가를 왼쪽 (설정하지 않는 변수를 활용하는 나쁜 스타일이 특히 때문에 - 바인딩 조항은 서로 독립적이기 때문에, 인간에 대한 이해하기 쉽게 효과).


LET 필요 하지 않지만 일반적으로 원합니다 .

LET는 까다로운 일이없이 표준 병렬 바인딩을 수행하고 있음을 제안합니다. LET *는 컴파일러에 대한 제한을 유도하고 사용자에게 순차 바인딩이 필요한 이유를 제안합니다. 스타일 측면에서 LET *에 의해 부과 된 추가 제한이 필요하지 않을 때 LET가 더 좋습니다.

LET *보다 LET를 사용하는 것이 더 효율적일 수 있습니다 (컴파일러, 최적화 프로그램 등에 따라 다름).

  • 병렬 바인딩 할 수 있습니다 병렬로 실행 (그러나 어떤 LISP 시스템이 실제로 이렇게 나도 몰라, 및 초기화 양식은 여전히 순차적으로 실행해야 함)
  • 병렬 바인딩은 모든 바인딩에 대해 하나의 새 환경 (범위)을 만듭니다. 순차 바인딩은 모든 단일 바인딩에 대해 새로운 중첩 환경을 만듭니다. 병렬 바인딩은 메모리를 적게 사용 하고 변수 조회더 빠릅니다 .

(위의 글 머리 기호는 Scheme, 다른 LISP 방언에 적용됩니다. clisp는 다를 수 있습니다.)


나는 인위적인 예를 들었다. 이 결과를 비교하십시오.

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

다음을 실행 한 결과 :

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

LISP에서는 가능한 가장 약한 구조를 사용하려는 욕구가 종종 있습니다. 예를 들어 일부 스타일 가이드는 비교 된 항목이 숫자라는 것을 알 =때보 다 사용하라고 알려줍니다 eql. 아이디어는 종종 컴퓨터를 효율적으로 프로그래밍하기보다는 의미하는 바를 지정하는 것입니다.

그러나 더 강력한 구조를 사용하지 않고 의미하는 바만 말하면 실제 효율성이 향상 될 수 있습니다. 를 사용한 초기화가있는 경우 LET병렬로 실행할 수 있지만 LET*초기화는 순차적으로 실행해야합니다. 어떤 구현이 실제로 그렇게할지 모르겠지만 일부는 미래에 잘 될 수 있습니다.


나는 최근에 두 인수의 함수를 작성했는데, 어떤 인수가 더 큰지 알면 알고리즘이 가장 명확하게 표현됩니다.

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

치죠는 b우리가 사용했던 경우, 크다 let*, 우리가 실수로 설정 한 것 ab같은 값에.


LET와 LET *의 공통 목록의 주요 차이점은 LET의 기호는 병렬로 바인딩되고 LET *의 기호는 순차적으로 바인딩된다는 것입니다. LET를 사용하면 init-form을 병렬로 실행할 수 없으며 init-form의 순서를 변경할 수도 없습니다. 그 이유는 Common Lisp가 함수가 부작용을 갖도록 허용하기 때문입니다. 따라서 평가 순서는 중요하며 양식 내에서 항상 왼쪽에서 오른쪽입니다. 따라서 LET에서는 init-form이 먼저 왼쪽에서 오른쪽으로 평가 된 다음 바인딩이 왼쪽 에서 오른쪽 으로 병렬로 생성됩니다. LET *에서는 init-form이 평가 된 다음 왼쪽에서 오른쪽으로 순서대로 기호에 바인딩됩니다.

CLHS : 특수 연산자 LET, LET *


나는 한 단계 더 사용 갈 바인드 통합하여 그 let, let*, multiple-value-bind, destructuring-bind등, 그리고 심지어 확장의를.

일반적으로 나는 "가장 약한 구조"를 사용하는 것을 좋아하지만 let& 친구들 과는 그렇지 않습니다. 왜냐하면 그들은 단지 코드에 소음을주기 때문입니다. (주관성 경고! 그 반대를 설득 할 필요가 없습니다 ...)


아마도 let컴파일러 를 사용 하면 공간이나 속도 향상을 위해 코드를 더 유연하게 재정렬 할 수 있습니다.

스타일 적으로 병렬 바인딩을 사용하면 바인딩이 함께 그룹화되는 의도를 알 수 있습니다. 이것은 때때로 동적 바인딩을 유지하는 데 사용됩니다.

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

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

물론 이것은 작동합니다.

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

이:

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

그러나 중요한 것은 생각입니다.


OP는 "실제로 필요한 LET"를 묻습니다.

Common Lisp가 만들어 졌을 때 여러 방언으로 된 기존 Lisp 코드의 보트로드가있었습니다. Common Lisp를 설계 한 사람들이 받아 들인 요약은 공통 기반을 제공 할 Lisp의 방언을 만드는 것이 었습니다. 그들은 기존 코드를 Common Lisp로 이식하는 것을 쉽고 매력적으로 만드는 것이 "필요"했습니다. LET 또는 LET *를 언어에서 제외하면 다른 장점이있을 수 있지만 그 핵심 목표는 무시했을 것입니다.

I use LET in preference to LET* because it tells the reader something about how the data flow is unfolding. In my code, at least, if you see a LET* you know that values bound early will be used in a later binding. Do I "need" to do that, no; but I think it's helpful. That said I've read, rarely, code that defaults to LET* and the appearance of LET signals that the author really wanted it. I.e. for example to swap meaning of two vars.

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

There is debatable scenario that approaches 'actual need'. It arises with macros. This macro:

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

works better than

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

since (M2 c b a) isn't going to work out. But those macros are pretty sloppy for assorted reasons; so that undermines the 'actual need' argument.


In addition to Rainer Joswig's answer, and from a purist or theoretical point of view. Let & Let* represent two programming paradigms; functional and sequential respectively.

As of to why should I just keep using Let* instead of Let, well, you are taking the fun out of me coming home and thinking in pure functional language, as opposed to sequential language where I spend most of my day working with :)


With Let you use parallel binding,

(setq my-pi 3.1415)

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

And with Let* serial binding,

(setq my-pi 3.1415)

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

The let operator introduces a single environment for all the bindings which it specifies. let*, at least conceptually (and if we ignore declarations for a moment) introduces multiple environments:

That is to say:

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

is like:

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

So in a sense, let is more primitive, whereas let* is a syntactic sugar for writing a cascade of let-s.

But never mind that. (And I will give justification later below for why we should "never mind"). Fact is there are two operators, and in "99%" of the code, it does not matter which one you use. The reason to prefer let over let* is simply that it does not have the * dangling at the end.

Whenever you have two operators, and one has a * dangling on its name, use the one without the * if it works in that situation, to keep your code less ugly.

That is all there is to it.

That being said, I suspect that if let and let* exchanged their meanings, it would likely be more rare to use let* than now. The serial binding behavior will not bother most code that doesn't require it: rarely would the let* have to be used to request parallel behavior (and such situations could also be fixed by renaming variables to avoid shadowing).

Now for that promised discussion. Although let* conceptually introduces multiple environments, it's very easy to compile the let* construct so that a single environment is generated. So even though (t least if we ignore ANSI CL declarations), there is an algebraic equality that a single let* corresponds to multiple nested let-s, which makes let look more primitive, there is no reason t actually expand let* into let to compile it, and even a bad idea.

One more thing: note that Common Lisp's lambda actually uses let*-like semantics! Example:

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

here, the x init-form for y accesses the earlier x parameter, and similarly (+1 y) refers to the earlier y optional. In this area of the language, a clear preference for sequential visibility in binding shows through. It would be less useful if the forms in a lambda could not see the parameters; an optional parameter couldn't be succinctly defaulted in terms of the value of previous parameters.


Under let, all of the variable initializing expressions see exactly the same lexical environment: that which surrounds the let. If those expressions happen to capture lexical closures, they can all share the same environment object.

Under let*, every initializing expression is in a different environment. For each successive expression, the environment must be extended to create a new one. At least in the abstract semantics, if closures are captured, they have different environment objects.

A let* must be well-optimized to collapse the unnecessary environment extensions in order to suitable as an everyday replacement for let. There has to be a compiler which works which forms are accessing what and then converts all of the independent ones into larger, combined let.

(This is true even if let* is just a macro operator that emits cascaded let forms; the optimization is done on those cascaded lets).

You cannot implement let* as a single naive let, with hidden variable assignments to do the initializations because the lack of proper scoping will be revealed:

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

If this is turned into

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

it will not work in this case; the inner b is shadowing the outer b so we end up adding 2 to nil. This sort of transformation can be done if we alpha-rename all of these variables. The environment is then nicely flattened:

(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

For that we need to consider the debug support; if the programmer steps into this lexical scope with a debugger, do we want them dealing with #:g01 instead of a.

So basically, let* is the complicated construct which has to be optimized well to perform as well as let in cases when it could reduce to let.

That alone wouldn't justify favoring let over let*. Let's assume we have a good compiler; why not use let* all the time?

As a general principle, we should favor higher-level constructs that make us productive and reduce mistakes, over error-prone lower-level constructs and rely as much as possible on good implementations of the higher-level constructs so that we rarely have to sacrifice their use for the sake of performance. That's why we are working in a language like Lisp in the first place.

That reasoning doesn't nicely apply to let versus let*, because let* is not clearly a higher level abstraction relative to let. They are about "equal level". With let*, you can introduce a bug that is solved by simply switching to let. And vice versa. let* really is just a mild syntactic sugar for visually collapsing let nesting, and not a significant new abstraction.


I mostly use LET, unless I specifgically need LET*, but sometimes I write code that explicitly needs LET, usually when doing assorted (usually complicated) defaulting. Unfortunately, I do not have any handy code example at hand.


who feels like re-writing letf vs letf* again? the number of unwind-protect calls?

easier to optimize sequential bindings.

maybe it effects the env?

allows continuations with dynamic extent?

sometimes (let (x y z) (setq z 0 y 1 x (+ (setq x 1) (prog1 (+ x y) (setq x (1- x))))) (values () ))

[ I think that works ] point is, simpler is easier to read sometimes.

참고URL : https://stackoverflow.com/questions/554949/let-versus-let-in-common-lisp

반응형