[Racket] Local Binding
Binding
Racket에서 바인딩이란 식별자와 값, 혹은 함수와 같은 표현식 간 연결을 의미합니다.
Binding은 특정 범위 내에서 유효하며, 이 범위 내에서 식별자는 해당 값 혹은
함수를 참조할 수 있습니다.
(define x 10) ; x는 10에 바인딩됩니다.
(+ x 5) ; 결과: 15
define 함수를 이용한 식별자 x에, 값 10을 바인딩하는 예시입니다.
(lambda (y) (+ y 5)) ; y는 함수 내에서 바인딩됩니다.
lambda를 이용한 바인딩 예시입니다.
y는 매개 변수로 바인딩 되어, 함수 내 expression에서 y라는 식별자를 참조할 수 있게 됩니다.
occurs-free? 함수를 이용하여 해당 lambda 표현식 내 특정 식별자가 바인딩되어 있는지
확인할 수 있습니다.
[Racket] Eopl Library define-datatype 및 cases 문법
Racket이란? 아래 포스트를 참조해주세요. [Racket] Racket IDE 세팅Racket이란? Scheme언어 계열의 함수형 프로그래밍 언어입니다. 주로 범용 어플리케이션 프로그래밍, 교육 및 연구용으로 쓰입니다.
swc0317.tistory.com
간단하게, Racket에서 "바인딩"이라는 것은 특정 스코프 내에서,
어떤 변수가 정의 및 초기화되어, 참조할 수 있는지에 대한 내용입니다.
반대로 free, 자유 변수는 어떤 변수가 정의 혹은 초기화되지 않아,
참조할 수 없는 변수입니다.
Local Binding
이러한 바인딩을 지역적으로 할 수 있게 도와주는 함수가 셋 존재합니다.
let, let*, letrec입니다.
셋 모두 구조적으로
(함수명 ([iden1 val1]
[iden2 val2]
...
[idenn valn])
함수 expression)
와 같은 구조를 가집니다.
예를 들어,
(let ([x 1]
[y 2])
(+ x y)) ; 결과: 3
같은 형태를 가집니다.
이는 지역적으로 x라는 identifier를 1, y라는 identifier를 2로 가지는 환경에서,
(+ x y)를 연산하는 것입니다.
세 함수 모두 지역적으로 임시 식별자를 정의하는 것은 같으나,
각각의 식별자를 정의하거나 바인딩하는 순서가 다릅니다.
let 함수
가장 기본인 let 명령어입니다.
- let은 지역적으로 정의되는 식별자들의 값을 먼저 계산합니다.
- 모든 식별자들의 값의 계산이 끝난 뒤 식별자들을 바인딩합니다.
먼저 계산한 뒤, 바인딩하는 게 무슨 의미냐,
#lang racket
(let ([x 1]
[y 1])
(+ x y))
이런 코드에 대해

이런 결과를 출력하는 건 당연할 겁니다.
그러면 이제, 각 지역 식별자들 간 참조를 봅시다.
#lang racket
(let ([x 1]
[y x])
(+ x y))
이번엔, 한 지역 식별자 정의에서, 또 다른 지역 식별자를 참조합니다.
그러면?

에러가 납니다. 에러를 보면,
x가 unbound, 즉 y를 정의하는 scope에서 x가 바인딩된 변수가 아닙니다.
let은, 즉 먼저 모든 값들을 계산합니다.
x = 1, y = x.
이때 y가 계산하는 x의 값은 정의되지 않았으므로,
에러가 납니다. 즉, let은 각 지역 식별자 간 참조가 불가능합니다.
let*
Local Binding 함수들은 이런 지역 식별자를 정의 및 바운딩하는 순서에
차이가 있습니다.
let* 함수의 경우
- 가장 첫 줄 식별자의 값을 계산하고 바운드합니다.
- 다음 줄 식별자의 값을 계산하고 바운드합니다.
- ...
- 가장 마지막 줄 식별자의 값을 계산하고 바운드합니다.
즉, 이런 흐름을 따르면 let*은 현재 지역 식별자에서, 이전 지역 식별자를 참조할 수 있습니다.
예를 들어,
#lang racket
(let* ([x 1]
[y x])
(+ x y))

이런 식으로, y에서 이미 정의된 지역 식별자인 x를 참조할 수 있는 모습입니다.
하지만, 아까 말했던 대로, 각 구문의 순서대로 지역 식별자를 정의하고 바운딩하고를
반복하기 때문에, let*은 현재 식별자 이후에 정의되는 지역 식별자를 참조할 수 없습니다.
#lang racket
(let* ([y x]
[x 1])
(+ x y))
이런 식으로 y가 이후에 정의되는 지역 식별자 x를 참조하면
unbound error가 발생합니다.

letrec
마지막으로, letrec입니다.
letrec은 다음과 같은 흐름을 따릅니다.
- 먼저 모든 지역 식별자를 바운딩합니다.
- 이후 모든 지역 식별자의 값을 계산합니다.
보시면, let의 식별자 바운딩 및 값 계산 순서와 반대입니다.
이 말은, 즉 모든 지역 식별자에서, 다른 지역 식별자를 참조할 수 있음을 의미합니다.
(letrec ([is-even? (lambda (n) (or (zero? n) (is-odd? (sub1 n))))]
[is-odd? (lambda (n) (and (not (zero? n)) (is-even? (sub1 n))))])
(is-odd? 11)) ; 결과: #t
이런 지역 식별자를 둘 정의하면, is-even? 지역 식별자에 담긴 함수와, is-odd? 식별자에 담긴
함수 간 서로 참조가 가능합니다.

그러나 이전 예시의
#lang racket
(letrec ([y x]
[x 1])
(+ x y)) ; 결과: 오류 발생
이 코드는 여전히 에러를 발생합니다.
이번 에러 코드는 undefined입니다.

즉, y라는 지역 식별자를 정의할 때 바운딩 되어 있는 x를 참조할 순 있어도,
x가 정의되어 있지 않아, undefined 에러가 납니다.
지역 식별자는 먼저 모두 바운딩되고 값을 계산하더라도,
지역 식별자는, 여전히 구문 순서대로 "defined" 하기 때문에
이러한 undefined error를 조심해야 합니다.
letrec의 유용성
letrec은, 먼저 모든 지역 식별자를 바운딩 한 뒤 값을 계산하는
구조적 특징 덕에, 다른 로컬 바운딩 함수들이 구현하지 못하는
재귀 함수를 지역 식별자에 정의할 수 있습니다.
#lang racket
(letrec ([sum-of-list (lambda (lst)
(if (null? lst)
0
(+ (car lst) (sum-of-list (cdr lst)))))])
(sum-of-list '(1 2 3 4 5))) ; 결과: 15
list를 인자로 받아, head entry + sum-of-list(cdr lst)
구조로 재귀적으로 리스트의 모든 값을 더하는 함수의 경우,
letrec의 구조에서 구현이 가능합니다.