3

Lox 언어

누군가에게 아침 식사를 만들어 주는 것보다 더 좋은 일이 있을까요?

앤서니 보댕

우리는 이 책의 남은 부분을 Lox 언어의 모든 구석구석을 밝히는 데 보낼 것입니다. 하지만 통역기를 위한 코드를 당장 만들기 시작하면서, 우리가 어떤 결과를 얻게 될지 미리 엿보지 못하게 하는 것은 너무 가혹한 일 같습니다.

동시에, 여러분이 코드 편집기를 손에 넣기 전에 언어 규칙과 사양에 대한 수많은 내용을 끌어들이고 싶지는 않습니다. 그래서 이 장은 Lox에 대한 부드럽고 친근한 소개가 될 것입니다. 많은 세부 사항과 예외 사항은 생략합니다. 그런 것들은 나중에 다룰 시간이 충분합니다.

3 . 1Hello, Lox

여기 여러분의 첫 번째 Lox 맛보기입니다:

// 당신의 첫 번째 Lox 프로그램!
print "Hello, world!";

// 주석과 뒤따르는 세미콜론이 의미하듯이, Lox의 문법은 C 계열에 속합니다. (print는 내장 문장이지 라이브러리 함수가 아니므로 문자열 주위에 괄호가 없습니다.)

C가 훌륭한 문법을 가지고 있다고 주장하지는 않겠습니다. 만약 우리가 우아한 것을 원했다면, 아마도 Pascal이나 Smalltalk를 모방했을 것입니다. 만약 우리가 스칸디나비아 가구 미니멀리즘을 추구했다면, Scheme을 사용했을 것입니다. 모두 나름의 장점이 있습니다.

C와 같은 문법이 대신 가지고 있는 것은 언어에서 더 가치 있다고 여겨질 만한 것, 바로 익숙함입니다. Lox를 구현하는 데 사용할 두 언어인 Java와 C 또한 이 스타일을 계승하기 때문에 여러분은 이미 이 스타일에 익숙할 것이라고 생각합니다. Lox에 비슷한 문법을 사용하면 여러분이 배워야 할 것이 하나 줄어듭니다.

3 . 2고급 언어

이 책은 제가 기대했던 것보다 더 커졌지만, Java와 같은 거대한 언어를 담을 만큼은 아닙니다. 이 페이지 안에 Lox의 두 가지 완전한 구현체를 담기 위해 Lox 자체는 상당히 간결해야 합니다.

작지만 유용한 언어를 생각할 때, JavaScript, Scheme, Lua와 같은 고급 "스크립팅" 언어가 떠오릅니다. 이 세 가지 중 Lox는 JavaScript와 가장 비슷해 보입니다. 이는 주로 대부분의 C 문법 언어들이 그렇기 때문입니다. 나중에 배우겠지만, Lox의 스코프 접근 방식은 Scheme과 밀접하게 관련되어 있습니다. Part III에서 만들 Lox의 C 버전은 Lua의 깔끔하고 효율적인 구현체에 많은 영향을 받았습니다.

Lox는 이 세 언어와 두 가지 다른 측면을 공유합니다:

3 . 2 . 1동적 타이핑

Lox는 동적으로 타입을 지정합니다. 변수는 어떤 타입의 값이든 저장할 수 있으며, 단일 변수가 다른 시점에 다른 타입의 값을 저장할 수도 있습니다. 잘못된 타입의 값에 대해 연산을 시도하면(예: 숫자를 문자열로 나누는 경우), 오류가 런타임에 감지되어 보고됩니다.

정적 타입이 좋은 이유는 많지만, Lox에 동적 타입을 선택하는 실용적인 이유가 더 중요합니다. 정적 타입 시스템은 배우고 구현하는 데 엄청난 노력이 필요합니다. 이를 건너뛰면 더 간단한 언어와 더 짧은 책을 얻을 수 있습니다. 타입 검사를 런타임으로 미루면 인터프리터를 더 빨리 가동하여 코드를 실행할 수 있습니다.

3 . 2 . 2자동 메모리 관리

고급 언어는 오류가 발생하기 쉬운 저수준의 고된 작업을 없애기 위해 존재하며, 저장소 할당 및 해제를 수동으로 관리하는 것보다 더 지루한 일이 어디 있겠습니까? 어느 누구도 아침 해를 맞으며 "오늘 할당할 모든 메모리 바이트에 대해 free()를 호출할 올바른 위치를 알아내고 싶어!"라고 외치지 않습니다.

메모리 관리에는 두 가지 주요 기술이 있습니다: 참조 카운팅(reference counting)트레이싱 가비지 컬렉션(tracing garbage collection) (일반적으로 줄여서 가비지 컬렉션 또는 GC라고 부릅니다). 참조 카운터는 구현하기 훨씬 간단합니다. 이것이 Perl, PHP, Python이 모두 처음에는 참조 카운팅을 사용한 이유라고 생각합니다. 하지만 시간이 지남에 따라 참조 카운팅의 한계가 너무 성가시게 됩니다. 이 모든 언어는 결국 완전한 트레이싱 GC를 추가하거나, 적어도 객체 순환을 정리할 수 있을 만큼의 GC를 추가하게 되었습니다.

트레이싱 가비지 컬렉션은 무시무시한 평판을 가지고 있습니다. 실제로 원시 메모리 수준에서 작업하는 것은 다소 힘든 일입니다. GC 디버깅은 때때로 꿈에서 16진수 덤프를 보게 만들기도 합니다. 하지만 기억하세요, 이 책은 마법을 없애고 괴물을 물리치는 것에 관한 것이므로, 우리는 우리만의 가비지 컬렉터를 작성할 것입니다. 이 알고리즘이 매우 간단하고 구현하기 재미있을 것이라고 생각합니다.

3 . 3데이터 타입

Lox의 작은 우주에서 모든 물질을 구성하는 원자는 내장 데이터 타입입니다. 몇 가지 밖에 없습니다:

3 . 4표현식

내장 데이터 타입과 리터럴이 원자라면, 표현식은 분자일 것입니다. 대부분은 익숙할 것입니다.

3 . 4 . 1산술 연산

Lox는 C와 다른 언어에서 알고 있는 기본적인 산술 연산자를 제공합니다:

add + me;
subtract - me;
multiply * me;
divide / me;

연산자의 양쪽에 있는 하위 표현식은 피연산자(operands)입니다. 두 개의 피연산자가 있기 때문에 이를 이항(binary) 연산자라고 부릅니다. ("이진(binary)"의 1과 0 사용과는 관련이 없습니다.) 연산자가 피연산자 가운데에 고정되어 있기 때문에, 이를 중위(infix) 연산자라고도 부릅니다 (연산자가 피연산자 앞에 오는 전위(prefix) 연산자나 뒤에 오는 후위(postfix) 연산자와 대비됩니다).

하나의 산술 연산자는 실제로 중위 연산자이자 전위 연산자입니다. - 연산자는 숫자를 음수로 만드는 데도 사용될 수 있습니다.

-negateMe;

이 모든 연산자는 숫자에서 작동하며, 다른 유형의 값을 전달하는 것은 오류입니다. 예외는 + 연산자인데, 두 문자열을 연결하는 데도 사용할 수 있습니다.

3 . 4 . 2비교 및 동등성

계속해서, 항상 부울 결과를 반환하는 몇 가지 연산자가 더 있습니다. 우리는 전통적인 비교 연산자를 사용하여 숫자를 (그리고 숫자만) 비교할 수 있습니다.

less < than;
lessThan <= orEqual;
greater > than;
greaterThan >= orEqual;

어떤 종류의 두 값에 대해서도 동등성 또는 비동등성을 테스트할 수 있습니다.

1 == 2;         // false입니다.
"cat" != "dog"; // true입니다.

다른 타입의 값도 가능합니다.

314 == "pi"; // false입니다.

다른 타입의 값은 절대 동등하지 않습니다.

123 == "123"; // false입니다.

저는 일반적으로 암시적 변환에 반대하는 편입니다.

3 . 4 . 3논리 연산자

전위 연산자 !인 not 연산자는 피연산자가 true이면 false를 반환하고, 그 반대도 마찬가지입니다.

!true;  // false입니다.
!false; // true입니다.

나머지 두 논리 연산자는 사실 표현식의 형태를 띤 제어 흐름 구조입니다. and 표현식은 두 값이 모두 참인지 여부를 결정합니다. 만약 왼쪽 피연산자가 거짓이면 왼쪽 피연산자를 반환하고, 그렇지 않으면 오른쪽 피연산자를 반환합니다.

true and false; // false입니다.
true and true;  // true입니다.

or 표현식은 두 값 중 하나라도 (또는 둘 다) 참인지 여부를 결정합니다. 왼쪽 피연산자가 참이면 왼쪽 피연산자를 반환하고, 그렇지 않으면 오른쪽 피연산자를 반환합니다.

false or false; // false입니다.
true or false;  // true입니다.

andor가 제어 흐름 구조와 같은 이유는 단락 평가(short-circuit)를 하기 때문입니다. and는 왼쪽 피연산자가 false이면 왼쪽 피연산자를 반환할 뿐만 아니라, 그 경우에는 오른쪽 피연산자를 평가조차 하지 않습니다. 반대로 (대우적으로?), or의 왼쪽 피연산자가 true이면 오른쪽은 건너뛰어집니다.

3 . 4 . 4우선순위와 그룹화

이 모든 연산자는 C에서 기대하는 것과 동일한 우선순위와 결합성(associativity)을 가집니다. (파싱에 도달하면 이것에 대해 훨씬 더 정확하게 다룰 것입니다.) 원하는 우선순위가 아닌 경우에는 ()를 사용하여 묶을 수 있습니다.

var average = (min + max) / 2;

기술적으로 그다지 흥미롭지 않기 때문에, 우리 작은 언어에서는 일반적인 연산자 모음의 나머지를 제외했습니다. 비트 연산자, 시프트 연산자, 모듈로 연산자, 조건 연산자는 없습니다. 여러분의 점수를 매기는 것은 아니지만, 여러분이 Lox의 구현에 이러한 연산자들을 추가한다면 제 마음속에서 보너스 점수를 얻을 것입니다.

이것들이 표현식의 형태입니다 (나중에 다룰 특정 기능과 관련된 몇 가지를 제외하고). 이제 한 단계 위로 올라가 봅시다.

3 . 5문장

이제 문장입니다. 표현식의 주된 역할이 을 생성하는 것이라면, 문장의 역할은 효과를 생성하는 것입니다. 정의에 따르면 문장은 값으로 평가되지 않으므로, 유용하려면 어떤 식으로든 세상을 바꿔야 합니다. 일반적으로는 상태를 수정하거나, 입력을 읽거나, 출력을 생성하는 방식입니다.

여러분은 이미 몇 가지 종류의 문장을 보았습니다. 첫 번째는 다음과 같습니다:

print "Hello, world!";

print 문장은 단일 표현식을 평가하고 그 결과를 사용자에게 표시합니다. 또한 다음과 같은 문장도 보셨습니다:

"some expression"; // 어떤 표현식

세미콜론(;)이 뒤따르는 표현식은 표현식을 문장으로 승격시킵니다. 이것은 (상상력이 풍부하게도) 표현식 문장(expression statement)이라고 불립니다.

단일 문장이 예상되는 곳에 일련의 문장을 넣고 싶다면, 블록(block)으로 묶을 수 있습니다.

{
  print "One statement.";   // 하나의 문장.
  print "Two statements.";  // 두 개의 문장.
}

블록은 또한 스코프에 영향을 미치는데, 이는 다음 섹션으로 이어집니다 . . . 

3 . 6변수

변수는 var 문장을 사용하여 선언합니다. 초기화자를 생략하면 변수의 값은 기본적으로 nil이 됩니다.

var imAVariable = "here is my value"; // 저는 변수이고 여기 제 값이 있습니다.
var iAmNil; // 저는 nil입니다.

선언된 후에는 당연히 변수 이름으로 접근하고 할당할 수 있습니다.

var breakfast = "bagels";
print breakfast; // "bagels"를 출력합니다.
breakfast = "beignets";
print breakfast; // "beignets"를 출력합니다.

여기서 변수 스코프 규칙에 대해 자세히 설명하지는 않겠습니다. 나중에 장에서 규칙의 모든 세부 사항을 파악하는 데 놀라울 정도로 많은 시간을 할애할 것이기 때문입니다. 대부분의 경우, C나 Java에서 기대하는 방식으로 작동합니다.

3 . 7제어 흐름

일부 코드를 건너뛰거나 여러 번 실행할 수 없다면 유용한 프로그램을 작성하기 어렵습니다. 이는 제어 흐름을 의미합니다. 이미 다룬 논리 연산자 외에 Lox는 C에서 세 가지 문장을 그대로 가져왔습니다.

if 문은 어떤 조건에 따라 두 문장 중 하나를 실행합니다.

if (condition) {
  print "yes"; // "yes"를 출력합니다.
} else {
  print "no";  // "no"를 출력합니다.
}

while 루프는 조건 표현식이 참으로 평가되는 한 본문을 반복적으로 실행합니다.

var a = 1;
while (a < 10) {
  print a;
  a = a + 1;
}

마지막으로, for 루프가 있습니다.

for (var a = 1; a < 10; a = a + 1) {
  print a;
}

이 루프는 이전 while 루프와 동일한 작업을 수행합니다. 대부분의 최신 언어는 다양한 시퀀스 타입을 명시적으로 반복하기 위한 일종의 for-in 또는 foreach 루프도 가지고 있습니다. 실제 언어에서는 여기 있는 조잡한 C 스타일 for 루프보다 더 좋습니다. Lox는 기본적인 것을 유지합니다.

3 . 8함수

함수 호출 표현식은 C에서와 동일하게 생겼습니다.

makeBreakfast(bacon, eggs, toast);

아무것도 전달하지 않고 함수를 호출할 수도 있습니다.

makeBreakfast();

예를 들어 Ruby와 달리 이 경우에는 괄호가 필수입니다. 괄호를 생략하면 이름이 함수를 호출하는 것이 아니라 단지 함수를 참조만 합니다.

자신만의 함수를 정의할 수 없다면 언어는 그리 재미있지 않습니다. Lox에서는 fun을 사용하여 그렇게 합니다.

fun printSum(a, b) {
  print a + b;
}

이제 몇 가지 용어를 명확히 할 좋은 시기입니다. 어떤 사람들은 "매개변수(parameter)"와 "인수(argument)"를 상호 교환적으로 사용하고, 많은 사람들에게는 그렇습니다. 우리는 의미론에 대해 가장 미세한 부분까지 파고드는 데 많은 시간을 할애할 것이므로, 용어를 명확히 합시다. 지금부터는 다음과 같습니다:

함수의 본문은 항상 블록입니다. 그 안에서 return 문을 사용하여 값을 반환할 수 있습니다.

fun returnSum(a, b) {
  return a + b;
}

실행이 return을 만나지 않고 블록의 끝에 도달하면, 암시적으로 nil을 반환합니다.

3 . 8 . 1클로저

함수는 Lox에서 일급 객체입니다. 이는 함수가 실제 값이며 참조를 얻고, 변수에 저장하고, 전달하는 등이 가능하다는 것을 의미합니다. 다음은 작동합니다:

fun addPair(a, b) {
  return a + b;
}

fun identity(a) {
  return a;
}

print identity(addPair)(1, 2); // "3"을 출력합니다.

함수 선언도 문장이므로, 다른 함수 안에 지역 함수를 선언할 수 있습니다.

fun outerFunction() {
  fun localFunction() {
    print "I'm local!"; // "저는 지역 함수입니다!"를 출력합니다.
  }

  localFunction();
}

지역 함수, 일급 함수, 블록 스코프를 결합하면 다음과 같은 흥미로운 상황에 직면하게 됩니다:

fun returnFunction() {
  var outside = "outside"; // 바깥 변수

  fun inner() {
    print outside;
  }

  return inner;
}

var fn = returnFunction();
fn(); // "outside"를 출력합니다.

여기서 inner()는 자신의 본문 바깥, 즉 둘러싸는 함수에서 선언된 지역 변수에 접근합니다. 이게 가능한 걸까요? 많은 언어들이 Lisp에서 이 기능을 빌려왔기 때문에, 여러분은 아마도 답이 '예'라는 것을 알고 있을 것입니다.

이것이 작동하려면, inner()는 자신이 사용하는 모든 주변 변수에 대한 참조를 "붙잡고" 있어야 합니다. 그래야 바깥 함수가 반환된 후에도 변수들이 계속 존재할 수 있습니다. 이런 함수들을 클로저(closures)라고 부릅니다. 요즘에는 이 용어가 모든 일급 함수에 사용되기도 하지만, 함수가 변수를 캡처하지 않는다면 사실 잘못된 명칭입니다.

짐작하시겠지만, 이를 구현하는 것은 일부 복잡성을 추가합니다. 왜냐하면 우리는 더 이상 변수 스코프가 함수가 반환되는 순간 지역 변수가 사라지는 스택처럼 엄격하게 작동한다고 가정할 수 없기 때문입니다. 우리는 이것들이 올바르고 효율적으로 작동하도록 만드는 방법을 배우는 즐거운 시간을 가질 것입니다.

3 . 9클래스

Lox는 동적 타이핑, 어휘적(대략 "블록") 스코프, 클로저를 가지고 있기 때문에, 절반 정도는 함수형 언어입니다. 하지만 보시다시피, 절반 정도는 객체 지향 언어이기도 합니다. 두 패러다임 모두 많은 장점이 있으므로, 각각의 일부를 다룰 가치가 있다고 생각했습니다.

클래스가 과대평가되었다는 비난을 받고 있는 만큼, 제가 Lox와 이 책에 클래스를 포함시킨 이유를 먼저 설명하겠습니다. 실제로 두 가지 질문이 있습니다:

3 . 9 . 1어떤 언어든 객체 지향이 되고 싶은 이유는 무엇일까?

이제 Java와 같은 객체 지향 언어들이 대중화되어 아레나 공연만 할 정도로 인기 상품이 되었으니, 더 이상 그것들을 좋아하는 것이 멋지지 않게 되었습니다. 왜 누군가 새로운 언어를 객체로 만들려고 할까요? 8트랙으로 음악을 발매하는 것과 같은 일이 아닐까요?

1990년대의 "모든 상속" 열풍이 괴물 같은 클래스 계층 구조를 만들어낸 것은 사실이지만, 객체 지향 프로그래밍(OOP)은 여전히 꽤 괜찮습니다. 수십억 줄의 성공적인 코드가 OOP 언어로 작성되어 수백만 개의 앱이 행복한 사용자에게 제공되었습니다. 오늘날 일하는 프로그래머의 대다수가 객체 지향 언어를 사용하고 있을 것입니다. 그들 모두가 그렇게 틀렸을 리는 없습니다.

특히 동적 타입 언어의 경우, 객체는 매우 유용합니다. 우리는 다양한 종류의 데이터를 묶기 위해 복합 데이터 타입(compound data types)을 정의할 어떤 방법이 필요합니다.

만약 우리가 이러한 데이터 타입에 메서드를 붙일 수 있다면, 다른 타입에 대한 유사한 함수들과 충돌을 피하기 위해 모든 함수 이름 앞에 데이터 타입 이름을 붙일 필요가 없습니다. 예를 들어 Racket에서는 hash-copy (해시 테이블 복사)와 vector-copy (벡터 복사)처럼 함수 이름을 지어야 서로 겹치지 않습니다. 메서드는 객체에 스코프되어 있으므로 이러한 문제는 사라집니다.

3 . 9 . 2Lox가 객체 지향인 이유는?

객체가 멋지지만 이 책의 범위 밖이라고 주장할 수도 있습니다. 대부분의 프로그래밍 언어 책, 특히 전체 언어를 구현하려고 시도하는 책들은 객체를 다루지 않습니다. 저에게는 이는 해당 주제가 잘 다루어지지 않았다는 것을 의미합니다.

우리 중 많은 사람들이 하루 종일 OOP 언어를 사용하는 것을 고려할 때, 세상을 위해서는 OOP 언어를 만드는 방법에 대한 약간의 문서가 필요하다고 생각합니다. 보시다시피, 이는 꽤 흥미로운 일로 밝혀졌습니다. 예상만큼 어렵지는 않지만, 예상만큼 간단하지도 않습니다.

3 . 9 . 3클래스 또는 프로토타입

객체에 관해서는 실제로 클래스프로토타입이라는 두 가지 접근 방식이 있습니다. 클래스가 먼저 나왔고 C++, Java, C# 등의 언어 덕분에 더 흔합니다. 프로토타입은 JavaScript가 우연히 세상을 장악할 때까지 거의 잊힌 파생물이었습니다.

클래스 기반 언어에는 인스턴스와 클래스라는 두 가지 핵심 개념이 있습니다. 인스턴스는 각 객체의 상태를 저장하고 해당 인스턴스의 클래스에 대한 참조를 가집니다. 클래스는 메서드와 상속 체인을 포함합니다. 인스턴스에서 메서드를 호출하려면 항상 한 단계의 간접 참조가 필요합니다. 인스턴스의 클래스를 찾은 다음 거기서 메서드를 찾습니다:

클래스와 인스턴스에서 필드와 메서드가 조회되는 방식

프로토타입 기반 언어는 이 두 개념을 병합합니다. 오직 객체만 존재하며(클래스는 없음), 각 개별 객체는 상태와 메서드를 포함할 수 있습니다. 객체는 서로 직접 상속할 수 있습니다(또는 프로토타입 용어로는 "위임(delegate to)"할 수 있습니다):

프로토타입 시스템에서 필드와 메서드가 조회되는 방식

이는 어떤 면에서 프로토타입 언어가 클래스보다 더 근본적이라는 것을 의미합니다. 그것들은 매우 간단하기 때문에 구현하기 정말 좋습니다. 또한 클래스가 멀리하게 하는 많은 특이한 패턴을 표현할 수 있습니다.

하지만 저는 프로토타입 언어로 작성된 많은 코드를 보아왔습니다(제가 직접 고안한 일부를 포함해서). 사람들은 프로토타입의 모든 강력함과 유연성을 가지고 일반적으로 무엇을 하는지 아십니까?  . . . 그들은 그것들을 사용하여 클래스를 재발명합니다.

왜 그런지는 모르겠지만, 사람들은 자연스럽게 클래스 기반(고전적? 고급스러운?) 스타일을 선호하는 것 같습니다. 프로토타입은 언어에서는 더 간단하지만, 이는 복잡성을 사용자에게 전가함으로써 얻어지는 것 같습니다. 그래서 Lox에서는 사용자에게 그런 수고를 덜어주고 클래스를 바로 내장하기로 했습니다.

3 . 9 . 4Lox의 클래스

충분한 근거 설명은 여기까지 하고, 실제로 어떤 기능을 가지고 있는지 봅시다. 클래스는 대부분의 언어에서 여러 기능의 집합을 포함합니다. Lox의 경우, 제가 생각하기에 가장 밝은 별들을 선택했습니다. 클래스와 메서드는 다음과 같이 선언합니다:

class Breakfast {
  cook() {
    print "Eggs a-fryin'!"; // "계란이 지글지글!"을 출력합니다.
  }

  serve(who) {
    print "Enjoy your breakfast, " + who + "."; // "who님, 아침 식사를 즐기세요."를 출력합니다.
  }
}

클래스의 본문은 메서드를 포함합니다. 메서드는 fun 키워드 없이 함수 선언처럼 보입니다. 클래스 선언이 실행되면 Lox는 클래스 객체를 생성하고 클래스 이름으로 명명된 변수에 저장합니다. 함수와 마찬가지로 클래스는 Lox에서 일급 객체입니다.

// 변수에 저장합니다.
var someVariable = Breakfast;

// 함수에 전달합니다.
someFunction(Breakfast);

다음으로 인스턴스를 생성하는 방법이 필요합니다. new 키워드를 추가할 수도 있지만, 간단함을 유지하기 위해 Lox에서는 클래스 자체가 인스턴스를 위한 팩토리 함수입니다. 클래스를 함수처럼 호출하면 자체의 새 인스턴스를 생성합니다.

var breakfast = Breakfast();
print breakfast; // "Breakfast instance"를 출력합니다.

3 . 9 . 5인스턴스화와 초기화

행동만 있는 클래스는 그다지 유용하지 않습니다. 객체 지향 프로그래밍의 아이디어는 행동과 상태를 함께 캡슐화하는 것입니다. 이를 위해 필드가 필요합니다. Lox는 다른 동적 타입 언어와 마찬가지로 객체에 자유롭게 속성을 추가할 수 있습니다.

breakfast.meat = "sausage";
breakfast.bread = "sourdough";

필드에 값을 할당하면 해당 필드가 존재하지 않을 경우 생성됩니다.

메서드 내에서 현재 객체의 필드나 메서드에 접근하려면 익숙한 this를 사용합니다.

class Breakfast {
  serve(who) {
    print "Enjoy your " + this.meat + " and " +
        this.bread + ", " + who + "."; // "who님, 당신의 고기와 빵을 즐기세요."를 출력합니다.
  }

  // ...
}

데이터를 객체 안에 캡슐화하는 것의 일부는 객체가 생성될 때 유효한 상태에 있도록 하는 것입니다. 이를 위해 초기화자를 정의할 수 있습니다. 클래스에 init()이라는 메서드가 있으면 객체가 생성될 때 자동으로 호출됩니다. 클래스에 전달된 모든 매개변수는 해당 초기화자로 전달됩니다.

class Breakfast {
  init(meat, bread) {
    this.meat = meat;
    this.bread = bread;
  }

  // ...
}

var baconAndToast = Breakfast("bacon", "toast");
baconAndToast.serve("Dear Reader");
// "Dear Reader님, 베이컨과 토스트를 즐기세요."

3 . 9 . 6상속

모든 객체 지향 언어는 메서드를 정의할 뿐만 아니라 여러 클래스나 객체에서 재사용할 수 있도록 합니다. 이를 위해 Lox는 단일 상속을 지원합니다. 클래스를 선언할 때, less-than (<) 연산자를 사용하여 상속할 클래스를 지정할 수 있습니다.

class Brunch < Breakfast {
  drink() {
    print "How about a Bloody Mary?"; // "블러디 메리 한 잔 어때요?"를 출력합니다.
  }
}

여기서 Brunch는 파생 클래스(derived class) 또는 서브클래스(subclass)이며, Breakfast는 기본 클래스(base class) 또는 슈퍼클래스(superclass)입니다.

슈퍼클래스에 정의된 모든 메서드는 서브클래스에서도 사용할 수 있습니다.

var benedict = Brunch("ham", "English muffin");
benedict.serve("Noble Reader"); // "Noble Reader님, 햄과 잉글리시 머핀을 즐기세요."를 출력합니다.

init() 메서드조차도 상속됩니다. 실제로는 서브클래스도 자체 init() 메서드를 정의하는 경우가 많습니다. 그러나 슈퍼클래스가 자신의 상태를 유지할 수 있도록 원래의 init() 메서드도 호출해야 합니다. 우리 자신의 인스턴스에서 메서드를 호출하되 우리 자신의 메서드를 건드리지 않는 방법이 필요합니다.

Java에서와 마찬가지로 super를 사용합니다.

class Brunch < Breakfast {
  init(meat, bread, drink) {
    super.init(meat, bread);
    this.drink = drink;
  }
}

객체 지향에 대한 내용은 이 정도입니다. 기능 세트를 최소한으로 유지하려고 노력했습니다. 책의 구조상 한 가지 타협이 불가피했습니다. Lox는 순수한 객체 지향 언어가 아닙니다. 진정한 OOP 언어에서는 모든 객체가 클래스의 인스턴스이며, 숫자나 부울과 같은 기본 값조차도 그렇습니다.

내장 타입을 다루기 시작한 지 한참 후에야 클래스를 구현하기 때문에, 그렇게 하는 것이 어려웠을 것입니다. 따라서 기본 타입의 값은 클래스의 인스턴스라는 의미에서 실제 객체가 아닙니다. 메서드나 속성을 가지고 있지 않습니다. 만약 제가 Lox를 실제 사용자를 위한 실제 언어로 만들려고 했다면, 이 부분을 고쳤을 것입니다.

3 . 10표준 라이브러리

거의 다 왔습니다. 언어 자체는 여기까지이며, 이제 남은 것은 "코어" 또는 "표준" 라이브러리입니다. 즉, 인터프리터에서 직접 구현되고 모든 사용자 정의 동작이 그 위에 구축되는 기능 집합입니다.

이것이 Lox의 가장 슬픈 부분입니다. 표준 라이브러리는 미니멀리즘을 넘어 노골적인 허무주의에 가깝습니다. 책의 예제 코드에서는 코드가 실행되고 의도한 대로 작동하는 것을 보여줄 뿐입니다. 이를 위해 이미 내장 print 문장이 있습니다.

나중에 최적화를 시작할 때, 우리는 일부 벤치마크를 작성하고 코드를 실행하는 데 걸리는 시간을 볼 것입니다. 이는 시간을 추적해야 한다는 의미이므로, 프로그램 시작 이후 초 단위 숫자를 반환하는 하나의 내장 함수 clock()을 정의할 것입니다.

그리고 . . . 그게 다입니다. 저도 압니다, 그렇죠? 부끄러운 일입니다.

만약 Lox를 실제로 유용한 언어로 만들고 싶다면, 가장 먼저 해야 할 일은 이 부분을 보강하는 것입니다. 문자열 조작, 삼각 함수, 파일 I/O, 네트워킹, 심지어 사용자로부터 입력 읽기까지도 도움이 될 것입니다. 하지만 이 책에서는 그런 것들이 필요하지 않으며, 추가한다고 해서 흥미로운 것을 가르쳐주지도 않으므로 생략했습니다.

걱정 마세요, 언어 자체에 우리를 바쁘게 만들 충분히 흥미로운 내용이 많이 있을 것입니다.

도전 과제

  1. 몇 가지 Lox 샘플 프로그램을 작성하고 실행해보세요 (제 저장소에 있는 Lox 구현을 사용할 수 있습니다). 제가 여기서 지정하지 않은 예외적인 동작들을 생각해보세요. 여러분이 예상하는 대로 작동하나요? 왜 그렇거나 아닌가요?

  2. 이 비공식적인 소개는 많은 것을 불특정하게 남겨두었습니다. 언어의 문법과 의미론에 대해 궁금한 점을 여러 가지 나열해 보세요. 그 답은 무엇이라고 생각하나요?

  3. Lox는 상당히 작은 언어입니다. 실제 프로그램을 사용하는 데 불편함을 줄 만한 어떤 기능이 빠져 있다고 생각하나요? (물론 표준 라이브러리 외에.)

설계 참고: 표현식과 문장

Lox는 표현식과 문장을 모두 가지고 있습니다. 일부 언어는 후자를 생략합니다. 대신, 선언과 제어 흐름 구조도 표현식으로 취급합니다. 이러한 "모든 것이 표현식인" 언어는 함수형 언어의 뿌리를 가지고 있으며 대부분의 Lisp, SML, Haskell, Ruby, CoffeeScript가 포함됩니다.

이렇게 하려면, 언어의 각 "문장과 유사한" 구조에 대해 그것이 어떤 값으로 평가되는지 결정해야 합니다. 이 중 일부는 쉽습니다:

  • if 표현식은 선택된 분기의 결과로 평가됩니다. 마찬가지로 switch 또는 다른 다중 분기도 선택된 케이스로 평가됩니다.

  • 변수 선언은 변수의 값으로 평가됩니다.

  • 블록은 시퀀스의 마지막 표현식 결과로 평가됩니다.

일부는 조금 더 이상해집니다. 루프는 무엇으로 평가되어야 할까요? CoffeeScript의 while 루프는 본문이 평가한 각 요소를 포함하는 배열로 평가됩니다. 이는 유용할 수도 있고, 배열이 필요하지 않다면 메모리 낭비일 수도 있습니다.

또한 이러한 문장과 유사한 표현식들이 다른 표현식과 어떻게 결합되는지 결정해야 합니다. 즉, 문법의 우선순위 테이블에 맞춰야 합니다. 예를 들어, Ruby는 다음을 허용합니다:

puts 1 + if true then 2 else 3 end + 4

이것이 여러분이 기대하는 것인가요? 여러분의 사용자가 기대하는 것인가요? 이것이 여러분이 "문장"의 문법을 설계하는 방식에 어떻게 영향을 미칠까요? Ruby는 if 표현식이 끝나는 시점을 알리기 위해 명시적인 end를 가지고 있습니다. 이것이 없으면 + 4는 아마도 else 절의 일부로 파싱될 것입니다.

모든 문장을 표현식으로 바꾸는 것은 위와 같은 몇 가지 까다로운 질문에 답하도록 강요합니다. 그 대가로 일부 중복을 제거할 수 있습니다. C는 문장 시퀀싱을 위한 블록과 표현식 시퀀싱을 위한 콤마 연산자를 모두 가지고 있습니다. if 문과 ?: 조건 연산자도 모두 가지고 있습니다. 만약 C에서 모든 것이 표현식이었다면, 이들 각각을 통합할 수 있었을 것입니다.

문장을 없애는 언어들은 보통 암시적 반환(implicit returns) 기능도 특징으로 합니다. 즉, 함수는 명시적인 return 문법 없이도 본문이 평가하는 모든 값을 자동으로 반환합니다. 작은 함수와 메서드에는 이것이 매우 편리합니다. 실제로 문장을 가지고 있는 많은 언어들도 본문이 단일 표현식을 평가한 결과인 함수를 정의할 수 있도록 =>와 같은 문법을 추가했습니다.

그러나 모든 함수가 그렇게 작동하도록 만드는 것은 약간 이상할 수 있습니다. 주의하지 않으면, 부작용만 의도했음에도 불구하고 함수가 반환 값을 유출할 수 있습니다. 그러나 실제로는 이러한 언어 사용자들은 이를 문제로 여기지 않습니다.

Lox의 경우, 저는 평범한 이유로 문장을 부여했습니다. 저는 익숙함을 위해 C와 유사한 문법을 선택했는데, 기존 C 문장 문법을 표현식처럼 해석하려고 하면 꽤 빠르게 이상해집니다.