함수

발행: (2026년 1월 5일 오후 04:04 GMT+9)
12 min read
원문: Dev.to

Source: Dev.to

죄송합니다. 번역하려는 실제 텍스트를 제공해 주시면, 해당 내용을 한국어로 번역해 드리겠습니다. 현재는 링크만으로는 본문을 확인할 수 없습니다. 텍스트를 복사해서 보내 주시면 바로 도와드리겠습니다.

스크린샷

내가 만든 것

Commits 3da669d, 930014e, 5adb148, and 9d534f3

내가 이해한 내용

1️⃣ 문법 업데이트

  • 우선순위 – 함수 호출이 이제 단항 연산자 규칙보다 높은 가장 높은 우선순위를 가집니다.
  • CallableCallable 클래스(실제로는 LoxCallable)를 도입하여 엔티티가 호출 가능한지 판단합니다.
    • 예시: 문자열은 호출할 수 없지만, 클래스와 함수(사용자 정의 혹은 네이티브)는 호출할 수 있습니다.
    • calleeLoxCallable을 구현하지 않으면 인터프리터가 런타임 오류를 발생시킵니다.
  • 커링 / 체인 호출 – 파서는 이제 (( "(" arguments? ")" )* 와 같은 구문을 허용하여 function(arg)(arg)(arg) 와 같은 호출을 가능하게 합니다.

Grammar diagram

2️⃣ 인자

  • Arity 검사 – 인터프리터는 전달된 인자의 개수가 정의된 매개변수의 개수와 일치하는지 확인합니다.

Arity check screenshot

3️⃣ 네이티브 함수

  • Java에서 clock() 네이티브 함수를 구현했습니다.
  • 이는 LoxCallable을 구현하는 익명 클래스로, 인터프리터의 전역 환경에 식별자 clock으로 바인딩됩니다.
  • 현재 시스템 시간을 초 단위로 반환하며, 벤치마킹에 유용합니다.

4️⃣ 함수 선언

  • fun을 만나면 호출 가능한 객체(LoxCallable)를 생성하고 새로운 환경에 함수 이름을 바인딩합니다.
  • 함수의 매개변수들은 그 로컬 환경에 바인딩됩니다.
  • 각 함수 호출도 새로운 환경을 생성하여 재귀를 가능하게 합니다.
  • 함수 본문이 끝나면 로컬 환경이 폐기되고 실행은 호출자 환경으로 돌아갑니다.

5️⃣ 반환

  • 인터프리터는 선택적으로 표현식을 뒤에 둘 수 있는 return 문을 찾습니다.
  • 표현식이 없으면 암묵적으로 nil을 반환합니다.
  • return이 실행되면 인터프리터는 현재 호출 스택을 즉시 unwind(풀어)해야 합니다.
  • 이를 위해 반환 값을 담는 간단한 예외 클래스 Return을 사용합니다.
  • 호출 지점에서는 함수 실행을 try‑catch 블록으로 감싸 이 예외를 잡으며, 예외가 발생하지 않으면 함수는 정상 종료하고 nil을 반환합니다.

6️⃣ 스코핑

  • 이전 Lox는 동적 스코핑을 사용했습니다: 호출된 함수의 스코프는 항상 전역 환경이었고, 함수가 정의된 환경이 아니라 호출된 위치에서 이름을 해석했습니다.
  • 이 때문에 정의 시점에 캡처된 변수를 함수가 끝난 뒤에도 접근할 수 없었습니다.
  • 해결책은 레키컬(정적) 환경을 캡처하는 것으로, 함수가 생성될 때 존재하던 환경을 저장하고 호출 시점에 이름 해석을 미루지 않는 것입니다.

문제와 해결을 보여주는 예시

fun getName(name) {
    var person = name;
    fun greet() {
        print "Hello, " + person + "!";
    }
    return greet;
}

var momo    = getName("Momo");
var chutney = getName("Chutney");

momo();    // "Hello, Momo!" 출력
chutney(); // "Hello, Chutney!" 출력
  • 수정 전에는 person 변수를 전역 스코프에서 찾으려 하므로 오류가 발생합니다.
  • 레키컬 스코핑을 구현한 후에는 반환된 각 greet 클로저가 자신만의 person 변수를 올바르게 캡처하여 기대한 출력을 생성합니다.

Source:

클로저, 스코핑, 그리고 리졸버

1. 클로저 예시

function getName(name) {
  var person = "Hello, " + name + "!";
  return function () {
    console.log(person);
  };
}

var momo = getName("Momo");   // `person`을 클로즈하는 함수를 반환
momo();                       // → “Hello, Momo!”

function chutney() {
  console.log("Hello, Chutney!");
}
chutney();                    // → “Hello, Chutney!”

2. 첫 번째 예시가 실패하는 이유

  • getName("Momo")가 끝나면 변수 person파괴됩니다. 함수 스코프가 종료되기 때문입니다.
  • 나중에 momo()를 호출하면, person을 부모 스코프에서 찾으려 하는데 이제 그 스코프는 전역 스코프가 됩니다(전역 스코프에는 person이 없음). → 오류!

3. 렉시컬(정적) 스코핑

  • 우리는 렉시컬 스코핑을 사용해 이 문제를 해결합니다.
  • 함수가 선언될 때, 그 함수는 주변 환경을 클로즈(캡처)합니다.
  • 내부 함수는 외부 환경에 대한 실시간 참조를 유지하므로, 외부 함수가 반환된 뒤에도 가비지 컬렉터가 이를 파괴하지 못합니다.
  • 내부 함수가 나중에 호출될 때, 캡처된 환경이 전역 스코프가 아니라 부모 스코프가 됩니다.

4. 리졸버

렉시컬 스코핑을 통해 클로저를 구현하면 또 다른 문제가 생깁니다:

  • 클로저는 현재 스코프를 나타내는 가변 맵에 대한 실시간 참조를 보유합니다.
  • 같은 블록 안의 이후 코드에 따라 스코프에 대한 캡처된 뷰가 바뀔 수 있는데, 이는 일어나서는 안 되는 일입니다.

Lox는 렉시컬/정적 스코프를 갖기 때문에, 변수 사용은 프로그램 실행 내내 같은 선언으로 해석되어야 합니다. 변수가 나중에 다시 선언되거나 재정의되더라도 말이죠.

예시

var a = "one";

{
  fun showA() {
    print a;
  }

  showA();          // → "one"

  var a = "two";

  showA();          // 여전히 "one"을 출력해야 하지만, 리졸버가 없으면 "two"를 출력하게 됨
}
  • a는 값 "one"을 가진 전역 스코프에 속합니다.
  • showA는 선언된 블록/환경을 캡처하는 클로저를 생성합니다.
  • showA()를 처음 호출하면 블록 안에 a가 없으므로 전역 스코프를 탐색해 "one"을 출력합니다.
  • 같은 블록 안에서 var a = "two"가 다시 선언되면, 가변 스코프 때문에 두 번째 showA() 호출 시 "two"가 출력됩니다—잘못된 동작이죠.

리졸버가 이를 해결하는 방법

파서 다음, 인터프리터 이전에 추가된 리졸버는 불변 데이터 구조를 사용합니다:

  • 원본 데이터 구조는 직접 수정될 수 없습니다.
  • 어떤 변경이 일어나면, 원본 정보를 포함하고 추가된 변경을 담은 새로운 구조가 생성됩니다.

이는 각 블록이 이전 블록의 해시를 참조하면서 새로운 변화를 담는 블록체인과 유사합니다.

리졸버가 블록을 처리하면서 변수가 사용되는(선언 이후) 위치를 만나면 정적 패스를 수행합니다:

Scope 0 (함수 스코프): a를 찾을 수 없음
Scope 1 (블록 스코프): a를 찾을 수 없음 (var a = "two" 아직 정의되지 않음)
Scope 2 (전역 스코프): a를 찾음
  • 변수가 가장 안쪽 스코프에서 2단계 떨어진 곳에 존재하므로, 리졸버는 해당 사용에 숫자 2를 주석으로 달아 둡니다.
  • 런타임에 인터프리터는 이 홉 카운트를 사용해 바로 올바른 선언으로 점프하므로, 가변 스코프에 의한 놀라움을 피할 수 있습니다.

5. 다음은?

우리는 클래시하게 계속 나아갈 겁니다! 💅
(다음 주제는 클래스와 상속입니다.)

6. 생각들

지난 몇 달 동안 가장 멋지고 당황스러운 일들을 직접 목격할 기회가 있었습니다. 마치 셰익스피어의 인생 일곱 단계처럼 삶의 스펙트럼을 모두 본 느낌이었어요. 어느 정도는 저를 겸손하게 만들었습니다.

오랫동안 저는 믿음, 관심사, 그리고 일부 꿈들을 놓는 것이 두려웠습니다. 놓으면 정체성을 잃을까 봐 걱정했거든요. 이제는 함수처럼, 절대 타협하지 않을 원칙이라는 템플릿을 가지고, 그 위에 새로운 것을 쌓아도 괜찮다는 것을 배웠습니다.

et life는 자체적인 “implementations”를 계속 추가하고 있다. (나는 반드시 그 비유를 사용해야 했어—양해해 주세요.)

변화와 예측 불가능함이 더 이상 그렇게 위협적으로 보이지 않고, 어쩌면 (0.05 % 정도) 좋은 것일지도 모른다. 나는 c’est la vie 라고 생각한다.

Back to Blog

관련 글

더 보기 »