레슨 06. 바인딩(binding) vs 할당(Assignment)

바인딩은 값을 담을 새로운 공간을 만든다

리스프는 종종, 변수의 값을 담기 위해 새로이 메모리를 할당하여 바인딩을 만듭니다. 바인딩은 변수의 렉시컬 스코프(lexical scope) 구현을 위한 매우 일반적인 매커니즘이지만, 바인딩의 라이프타임에 따라 다른 용도로도 사용됩니다. 8장[p 126]에서 라이프타임(lifetime)과 가시성(visibility)를 공부할때 이를 다시 논할 것입니다.

리스프는 새로운 바인딩을 위해 저장공간을 할당합니다. 이것이 끔찍하게 비효율적인 것처럼 보이지만, 아직 리스프가 어디에 저장공간을 할당하는지에 대해서는 알 수 가 없습니다. 예를들어, 리스프는 함수 인자를 값으로 바인드할때, 다른 프로그래밍 언어들처럼 스택(stack)에 저장공간을 할당합니다. 리스프는 바인딩의 라이프타임을 판단할 수 없으면 해당 바인딩을 힙(heap)에 생성합니다.

바인딩은 이름을 가진다

리스프는 각 바인딩마다 이름을 부여합니다. 만일 그렇지 않다면, 프로그램은 어떻게 바인딩을 참조할 수 있을까요? 간단하게, 어? 잠시만요...

바인딩은 동시에 다른 값을 가질 수 있다

중첩 바인딩에서 동일한 이름을 공유하는 것은 매우 일반적인 일입니다. 예를들어:

(let ((a 1))
   (let ((a 2))
      (let ((a 3))
         ...)))

여기서, (...으로 표시된) 가장 안쪽의 let에 도착할 쯤에는, a는 3개의 서로 다른 바인딩을 가지게 됩니다.

단, 위 예제가 전형적인 리스프 코드라고 말하는 것은 결코 아닙니다.

가장 가까운 바인딩

;; 여기서는, A는 바인딩 되지 않습니다.

(let ((a 1))
   ;; 여기서 A의 가장 가까운 바인딩은 값 1을 갖습니다.

   (let ((a 2))
      ;; 여기서 A의 가장 가까운 바인딩은 값 2을 갖습니다.

      (let ((a 3))
         ;; 여기서 A의 가장 가까운 바인딩은 값 3을 갖습니다.
         ...)))

보시다시피, 가장 가까운 바인딩이란 상대적인 위치를 가집니다. 바인딩 폼이 어떻게 중첩되었는지 살펴보면 (위에서 보인것처럼 여러분의 코드를 들여쓰기하였다면 이를 하기에 쉬울 것입니다), 어떻게 프로그램이 바인딩에 접근하는지 알 수 있습니다.

한가지 더 여러분이 알아야 할것은, 내부 바인딩 폼이 동일한 심볼을 바인드하지 않는 한, 내부 바인딩 폼에서도 외부 바인딩이 여전히 유효하다는 점 입니다:

;; 여기서, A와 B는 바인딩 되지 않습니다.

(let ((a 1)
      (b 9))
   ;; 여기서, A의 가장 가까운 바인딩은 값 1을 지니며,
   ;; B의 바인딩은 값 9를 지닙니다.

   (let ((a 2))
      ;; 여기서, A의 가장 가까운 바인딩은 값 2를 지닙니다.
      ;; B의 바인딩은 여전히 값 9를 지닙니다.
      
      (let ((a 3))
         ;; 여기서, A의 가장 가까운 바인딩은 값 2를 지닙니다.
         ;; B는 여전히 외부의 LET 폼에서의 값 9를 지닙니다.
         ...)))

이미 만든 바인딩에만 프로그램이 접근할 수 있습니다.

바인딩 폼이 새로운 값을 이미 존재하는 심볼에 바인딩하면, 이전의 값은 가려지게 됩니다. 프로그램이 내부 바인딩 폼을 실행하는 동안에, 외부 바인딩 값이 감춰집니다 (단, 사라지지는 않습니다). 그러나, 프로그램이 내부 바인딩 폼을 빠저나가면, 외부 바인딩 값이 복구됩니다. 예를들어:

(let ((z 1))
   ;; 여기서, Z의 가장 가까운 바인딩은 값 1을 지닙니다.

   (let ((z 2))
      ;; 여기서, Z의 가장 가까운 바인딩은 값 2을 지닙니다.
      ...)
   
   ;; 이제 여러분은 내부 바인딩 폼을 빠져나왔으며,
   ;; 다시 바인딩 값 1을 보게됩니다.
   ...)

할당은 오래된 장소에 새로운 값을 줍니다. gives an old place a new value

setq 폼은 이미 존재하는 바인딩 값을 바꿉니다:

(let ((z 1))
   ;; 여기서, Z의 가장 가까운 바인딩은 값 1을 지닙니다.

   (setq z 9)
   ;; 이제 값 Z는 9입니다.

   (let ((z 2))
      ;; 여기서, Z의 가장 가까운 바인딩은 값 2을 지닙니다.
      ...)
   
   ;; 이제 여러분은 내부 바인딩 폼을 빠져나왔으며,
   ;; 다시 Z의 외부 바인딩 값 9을 보게됩니다.
   ...)

위의 setq 폼은, 바깥쪽 let 폼에서 정의된 z의 바인딩값을 바꿉니다. 이는 종종 좋지 않은 일을 불러옵니다. 문제라고 생각되는 점은 z의 값을 확인하기 위해 살펴봐야만 하는 곳이 이제 두 곳으로 늘어났다는 것입니다 - 첫번째는 바인딩 폼, 그 다음은 setq로 할당한 코드. 들여쓰기를 활용하는 바인딩 폼과는 달리, 프로그램의 본체 부분에서 할당하는 폼에는 따로 들여쓰기가 없습니다; 프로그램을 읽을때 이러한 부분을 찾아내는것이 어렵습니다.

이전 예제에서 봤던것과 같이 새로운 바인딩을 도입하여 매우 쉽게 할당하는 코드를 우회할 수 있습니다.

(let ((z 1))
   ;; 여기서, Z의 가장 가까운 바인딩은 값 1을 지닙니다.

   (let ((z 9))
      ;; Z의 새 값은 이제 9.

      (let ((z 2))
         ;; 여기서, Z의 가장 가까운 바인딩은 값 2을 지닙니다.
         ...)

      ;; 이제 여러분은 내부 바인딩 폼을 빠져나왔으며,
      ;; 다시 중간에 있는 바인딩 값 9가 Z의 바인딩 값이 됩니다.
      ...)

   ;; 여기서, 가장 바깥쪽 바인딩 값 1이 Z의 바인딩 값이 됩니다.
   ...)

이제 let폼의 들여쓰기로 인해 z의 모든 바인딩을 알아보기 쉬워졌습니다. 프로그램의 어느 지점에서(예제에서는 ...에서) z의 올바른 바인딩을 찾기 위해 여러분이 해야할 일은 프로그램을 읽을 시 들여쓰기를 주의하여 상위 레벨에 있는 let 폼을 찾아보는 것입니다.

let 폼으로 둘러싸여 있지 않는 곳에서 setq 폼이 어떠한 변수를 할당하고 있다면, 이는 전역 변수이거나 특수(special) 변수일 것입니다.

전역 변수는 다른 바인딩으로 가려지지 않는한 어느곳에서나 접근이 가능하며, 리스프 시스템이 동작하는 동안 사용할 수 있습니다. 특수(special) 변수는 8장[p 126]에서 살펴볼 것입니다.

(setq a 987)
;; 여기서, A는 전역 값 987를 지녔습니다.

(let ((a 1))
   ;; 여기서, A의 바인딩 값 1이 전역 변수의 값을 가리게 됩니다.
   ...)

;; 이제 A의 전역 값이 다시 살아났습니다.

...

짚고 넘어가기

  • 바인딩(binding)