Scope rules 에 대해서

CCCC·2023년 10월 24일

Static Scoping

컴파일 할 때 scope가 결정이 된다. 따라서 실행 중의 순서에는 연연할 필요가 없다.

  • 컴파일 시에 순서가 다 결정이 된다.

Static scope의 현재 바인딩

지금 위치에서 가장 가까운 블록이다. 쉽게 말하면, 지금 자신이 속해있는 block 부터 바인딩을 찾아나가면 된다. 자신이 속해있는 block에 존재하지 않는다면, static link를 따라 부모 block으로 나간다.
예를 들어,

int x; // 전역 변수
void set_x(int n) { x = n } void print_x() {
    write_integer(x);
}
void second(func f) {
    void first() {
      print_x();
      set_x(7);
    }
    int x = 4;
    print_x();
    first();
    f();
}
void main() {
    set_x(5);
    print_x();
    second(print_x);
    print_x();
}

코드가 있다고 하자.

1. set_x(5)

x = 5 가 전해지면서 전역변수 x는 5가 된다.
여기서 중요한 점이 있는데, 어떤 스코프 룰이든 (다이나믹이든 스테틱이든) 전역변수와 지역변수는 상관이 없다는 점이다.

  • 전역 변수는 인스턴스가 프로그램 통틀어서 하나이기 때문이다.
  • 지역 변수는 어차피 그때 그때 새로 선언할 때 생겨나고 함수가 종료되면 죽기 때문이다.

2. print_x(); -> 결과: 5

write_integer(x); 이므로 전역변수 5가 출력된다.

3. second(print_x);

second 함수에 print_x함수가 전달된다.
그러면 second 함수를 살펴보자.

void second(func f) {
    void first() {
      print_x();
      set_x(7); 
    }
    int x = 4;
    print_x();
    first();
    f();
}

3-1.int x = 4가 실행된다.

여기서 선언을 다시 했으므로 전역 변수 x가 아니라, second 함수 내의 지역변수 x 가 된다.

3-2. print_x(); -> 결과: 5

Static scope는 컴파일 시 순서가 다 결정된다고 했다. 따라서, 어디서 호출을 했던 흐름에 상관없이 print_x()는 전역변수 x(=5) 밖에 모른다. 왜냐면 저 함수는 second 함수 밖에 선언되어 있고, 서로를 모르기 때문이다.

  • static scope 는 static link를 따라 부모 바인딩을 찾는다고 했는데, static link는 컴파일 시 만들어지는 링크이며, 여기서 부모는 print_x를 감싸고 있는 블록이다. 따라서 순서는 이미 다 정해진 대로 따라가면 된다.

3-3. first(); -> 결과: 5

first() 안에 함수 print_x() 가 실행되는데, 똑같이 전역변수 5가 출력될 것이다.
그 다음 set_x(7)를 했기 때문에 전역변수 x는 7로 바뀐다.

3-4. f(); == print_x() -> 결과: 7

아까 7로 전역변수가 바뀌었기 때문에 7로 바뀐 전역변수가 출력된다.

4. print_x(); -> 결과: 7

마찬가지로 아까 바뀐 전역변수 7이 출력된다.

⚡️static scope의 실행 결과: 5 5 5 7 7

Dynamic scope

static scope와는 엄연히 다르게, run time상 순서가 결정되므로 실행 순서에 연연한다.

Dynamic Scope의 현재 바인딩

스코프가 종료되면서 바인딩이 사라지지 않았다는 가정 하에, 실행 중에 가장 최근에 만난 바인딩을 사용한다.
-> 가장 중요한 포인트다...나는 여기서 매우 매우 헷갈렸기 때문이다...

Dynamic Scope의 종류

동적 스코프에는 두가지 종류가 있다.
1. 얕은 바인딩 (shallow-binding)
2. 깊은 바인딩 (deep-binding)

일부 언어들은 함수에 대한 참조가 가능하다.

int a;
function f(d){
	a =6;
   d;
}
function d(){
	a =3;
}

대충 예를 들어보겠다...이런식으로 함수가 있다 치자.
그러면 함수가 호출되었을 때, 'd;' 라고 불리었을 때 순간이 있을 것이고, f(d)로 참조 되었을 때의 순간이 있을 것이다.
다시 말해서,

함수가 호출 되었을 때의 reference environment,
함수가 참조 되었을 때의 reference environment

두개로 나뉜다는 것을 알 수 있다.

  • 여기서 Reference environment 라는 것은, 어느 시점에서 프로그램을 멈췄을 때 사용할 수 있는 모든 바인딩을 뜻한다.

함수가 호출 되었을 때를 shallow-binding, 함수가 참조 되었을 때를 deep-binding이라고 한다.

shallow binding

함수가 호출 되었을 때의 reference environment를 사용한다.

int a=0;
function f(d){
	a =6;
   	d;
}
function d(){
	a =3;
    print(a);
}

이 코드에서는 6이 호출된다. 함수 d가 불려졌을 때의 a값은 6이기 때문이다.

  • 😱 여기서!!! dynamic scope룰을 꼬옥 잊지 말아야한다. dynamic scope는 가장 최근에 만난 바인딩을 사용한다고 했다. static scope 같았으면 지역변수 3이 호출되었곘지만 (가장 안쪽 블록을 찾으니까), 여기서 가장 최근에 만난 바인딩은 6이다.

❌ shallow binding의 문제점

d() 함수 안의 변수 값이 만약 바뀐다면, a에서 그것을 반영할 수 없기 때문에 좋지 않다. 위의 코드같은 경우에도 a=3이라고 값이 바뀌었는데도 6을 호출하고 있기 때문이다.

-> 😲 그래서 딥 바인딩이 쓰이는 것이다.

Deep binding

딥 바인딩은 함수가 참조되었을 떄의 함수가 자신의 reference environment를 그대로 가져간다고 생각하면 되는데, 그것을 closure 이라고 부른다.

closure

그 서브루틴이 사용된다고 가정했을 때, 그 서브루틴의 Reference environment와 서브루틴을 같이 가져가서 사용하는 것이다.

  • closure 안에서 서브 루틴에 대한 참조는 포인터 개념으로 들어가 있다.
  • 약간 참조된 함수가 가방을 매고 자신의 변수들을 가져간다는 개념으로 보면 쉽다. 👜
def A(I,P):
	def B():
    	print(I)
    if I > 1:
    	P();
    else: 
    	A(2,B)
def C:
	pass
//main//
A(1,C)

deep binding으로 보겠다.
1. 처음에 I=1, P=C로 들어간다.
2. else문으로 들어가서 A(2,B)를 실행한다.
3. 여기서 I=2,P=B 가 된다.
4. 그런데, I=2 이므로 if문을 통과한다.

  • P(); 실행. = B() = print(I)
  • 여기서, A(2,B)로 B가 참조되었을 때 B 함수는 만들어진 것이라고 보면 편한데, 그렇다면I=2로 바뀐 것을 모를 것이다.
  • 그렇다면 저 함수가 있을 때의 I=1이었으므로, 1이 출력된다.

shallow binding의 경우 호출되었을 때, 즉 P()가 불렸을 때의 값으로 보므로 I=2가 나온다.

Subroutine Closures

static scope를 사용하는데 함수 안의 함수가 가능한, 즉 nested subroutines 이 가능한 언어들은 deep binding을 사용한다.

서브루틴 S가 사용가능하다고 생각했을 때, S에 대한 포인터와 reference environment를 합쳐서 closures 을 만든다.

Static chain을 따라서, 전역변수거나 nested된 인스턴스들을 찾아간다.

Js에서의 closures

Js는 함수를 리턴할 때 Closures 개념으로 하는데, 살펴보자.

여기서 testClosure() 함수가 window.onload() 에서 다시 var f에 담겨 사용되는 것을 볼 수 있다.
testClosure()안의 var localVariable은 함수에 있는 지역 변수인데, 죽지 않고 다시 사용되고 있다.

그 이유는 testClosure() 함수가 Closure을 리턴하기 때문이다.

저 함수는 자신이 가지고 있는 지역 변수, 즉 reference environment를 자신과 함께 리턴하는 방식으로 하고 있기 때문에 지역변수가 함수가 끝나고 바인딩과 생명이 유지되어 사라지지 않는다.

dangling references

=> 없는 레퍼런스를 사용한다는 뜻이다.
예시를 보자.

define plus-x
  (lambda (x)
    (lambda (y) (+ x y))))
     ...

(let ((f (plus-x 2))) 
(f 3))) //5

plus -x함수는 이미 반환되며 종료되었지만, 그 함수에서의 지역변수 x를 x+y 값에 더하며 계속 사용하고 있다.

  • 함수가 종료되면 지역변수도 사라지는 것이 맞다.
  • 따라서 원래대로라면 dangling reference 가 발생하게 된다.(오브젝트의 라이프 타임보다 바인딩이 오래 남는 경우.)
    이것이 함수형 언어에서 가능한 이유는 unlimited extent 때문이다.

unlimited extent

함수형 언어들은 지역변수에 대해서 스택에 두지 않는다.
실제로 사용이 끝날 때까지 제거하지 않고, 힙에 둔다.

limited extent

이와 반대로 보통 스택에 두어서 함수가 끝날 때 종료시키는 것을 말한다.
대부분의 언어들은 이 방법을 사용한다.


교수님 죄송해요 미리미리 공부할게요.........

profile
항상 졸려

0개의 댓글