함수
Source: Dev.to
죄송합니다. 번역하려는 실제 텍스트를 제공해 주시면, 해당 내용을 한국어로 번역해 드리겠습니다. 현재는 링크만으로는 본문을 확인할 수 없습니다. 텍스트를 복사해서 보내 주시면 바로 도와드리겠습니다.
스크린샷
내가 만든 것
Commits 3da669d, 930014e, 5adb148, and 9d534f3
내가 이해한 내용
1️⃣ 문법 업데이트
- 우선순위 – 함수 호출이 이제 단항 연산자 규칙보다 높은 가장 높은 우선순위를 가집니다.
- Callable –
Callable클래스(실제로는LoxCallable)를 도입하여 엔티티가 호출 가능한지 판단합니다.- 예시: 문자열은 호출할 수 없지만, 클래스와 함수(사용자 정의 혹은 네이티브)는 호출할 수 있습니다.
- callee가
LoxCallable을 구현하지 않으면 인터프리터가 런타임 오류를 발생시킵니다.
- 커링 / 체인 호출 – 파서는 이제
(( "(" arguments? ")" )*와 같은 구문을 허용하여function(arg)(arg)(arg)와 같은 호출을 가능하게 합니다.

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

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 라고 생각한다.

