리스프의 강점이 ‘호모아이코닉성’이 아니라, 스캐너-리더-파서로 나뉜 ‘양원제 구문’에 있음을 설명하고, 리더/파서 분리가 주는 이점과 이를 다른 언어 설계에 적용하는 방법을 논한다.
프로그래밍 언어에 관한 인터넷 토론을 충분히 읽다 보면, 리스프풍 언어들은 특별한 속성—호모아이코닉성—을 갖는다고 배우게 된다. 이 속성에는 신비한 힘이 깃들어 있어 리스프를 풍요롭게 하고 경쟁자들을 깎아내린다고까지 여겨진다.
나는 1980년대 후반부터 리스프를 사용하고, 만들기도 해 왔다. 내 블로그 이름도 “parenthetically speaking”이다. 그런데도 나는 이 용어가 대체로 헛소리에 가깝다고 말하려 한다. 그렇지만 리스프에는 특별한 무언가가 있다—훨씬 덜 신비적이면서도 매우 강력하고 유용한 무언가. 그게 무엇인지 이해할 가치가 있고, 그 정수를 다른 언어로 옮겨 심을 가치가 있다.
호모아이코닉성이란, 대체 무엇일까? 보통은 다음과 같이 들을 것이다. “일부 프로그래밍 언어의 속성으로, 프로그램을 그 언어 안에서 데이터로 표현할 수 있게 해 준다.” 혹은 “표현” 대신 “조작”을 넣거나, 더 간단히 “데이터로서의 코드”라고.
이를 조금만 더 세분해 보자. 다음 파이썬 코드를 보자:
hello = 1
이건 분명 하나의 프로그램이다. 그런데 이를 언어 내부의 데이터로 표현할 수 있을까? 물론이다:
'hello = 1'
은 아주 그럴듯한 표현이다. (음, 좋긴 하지만 아주 훌륭하진 않다; 이건 나중에 다시 이야기하자!) 조작할 수 있을까? 물론이다. 문자열을 이어 붙여서 만들 수 있다:
'hello' + ' = ' + '1'
은 그 프로그램을 만들어낼 것이고,
'hello = 1'.split(' ')
은 그것을 구성 요소로 쪼갤 것이다.
그렇다면 파이썬은 호모아이코닉한가?
물론, 여기서 파이썬이 특별한 건 전혀 없다. 자바스크립트로 자바스크립트 프로그램을 표현하고 조작할 수 있고, C로 C 프로그램에 대해 같은 일을 할 수 있다. 요컨대, 문자열 데이터 타입이 있는 거의 모든 프로그래밍 언어는 호모아이코닉해 보인다. 심지어 문자열조차 필요 없다. 프로그램을 숫자로 표현해도 된다(예: 괴델 번호화 사용).
좋은 정의의 특징 중 하나는 비자명해야 한다는 것이다. 어떤 것은 포착하되, 어떤 것은 배제해야 한다. 그런데 이 호모아이코닉성 개념은 거의 아무것도 배제하지 못하는 듯하다.
하지만 위에서 한 얘기에 대해 타당한 반론이 있다. 우리가 한 일은 문자열을 쓰고, 합치고, 쪼갠 것뿐이다. 그러나 문자열은 반드시 프로그램이 아니다. 문자열은 그저 데이터의 한 형태다. 데이터는 데이터고, 프로그램—실행할 수 있는 실체—은 별개의 것처럼 보인다.
그렇다면 데이터를 프로그램으로 바꾸려면? 이를 위해서는 언어 차원의 지원이 필요하다. 합의된 데이터 표현을 가져다가 그것을 프로그램처럼 취급하는 무언가, 즉 그 프로그램이 했을 일을 실제로 수행하는 무언가가 필요하다. 보통 이는 eval이라는 함수다. eval은 그 데이터를 평가하여, 마치 프로그램인 것처럼 그 데이터가 서술한 효과를 수행한다. (주: eval은 사실 “코드로서의 데이터”를 다루지, “데이터로서의 코드”를 다루는 게 아니다.)
그렇다면 eval이야말로 호모아이코닉 언어의 진짜 특징일까? 그럴 수도 있다. 분명 eval은 두드러진 기능이며, 어떤 언어에는 있고 어떤 언어에는 없다. 즉, 언어들 사이를 비자명하게 가른다. 다만 다음을 주목하자:
이건 실망스러운 결말처럼 보인다. 호모아이코닉 언어란, 복잡하고 지나치게 강력한 기능을 가진 언어인데, 그 기능은 아마 쓰지 말아야 하고, 게다가 리스프라고 할 수도 없는 많은 언어들에도 존재한다…—이게 리스프스러운 언어를 특징짓는 좋은 방식처럼 보이지 않는다.
하지만 바로 이것이 우리가 애초에 호모아이코닉성 따위를 이야기하면 안 되는 이유다. 대신 정말 흥미로운 것이 무엇인지 이야기해 보자.
고전적 파싱 파이프라인을 잠깐 이야기하자. 수십 년 동안 우리는 프로그램을 파싱하는 과정을 두 단계—토큰화(속칭 “렉싱”) 다음 파싱(속칭 “야킹”이라고는 안 하지만), 등등—로 생각하도록 배웠다. 이들은 무엇을 하는가?
프로그램은 문자들의 목록, 혹은 스트림이다. 언어 구현은 프로그램이 하려는 바를 실행하기 전에 먼저 그 의미를 이해해야 한다. 즉, 프로그램의 의미를 파악해야 한다. 이 의미 결정 과정은 보통 여러 단계로 이루어지며, 그 각각의 표현력을 이해하면 이 분할이 타당해진다.
언어 구현의 프런트엔드에서 흔한 데이터 흐름은 다음과 같다:
이게 무슨 뜻일까?
여러분이 이 글을 개별 문자 수준으로 읽지 않듯이, hello = 1 같은 프로그램을 보고 h, e, l, … 같은 문자 단위로 다루고 싶지 않다. 대신 이를 세 개의 토큰—hello, =, 1—으로 생각하고 싶다. 이 과정에서 = 주변에 공백이 있었는지는 무시할 수 있다(파이썬은 이를 요구하지 않지만, 어떤 언어는 요구한다). 방금 생략한 공백 얘기는 나중에 다시 하자! 이게 스캐너의 역할이다.
그래서 입력 문자 목록(또는 스트림)이 스캐너를 통해 토큰 목록(또는 스트림)으로 변환된다. 하지만 여전히 우리는 프로그램을 토큰 수준에서 읽지 않는다. 그 대신 의미를 부여한다. 즉, 위를 “hello에 1을 대입하라”로 읽을 것이다. 파이썬에서 =의 실제 의미는 훨씬 더 지저분하다. 이게 파서의 역할이다. 파서는 언어의 고수준 의미를 나타내는 데이터 구조를 만든다. 이 경우 “변수 대입” 유형의 노드를 만들고 두 개의 자식을 둘 수 있다. 하나는 대입되는 변수(여기서는 hello), 다른 하나는 값 표현식(여기서는 1). 만약 표현식이 더 복잡하다면—예: 프로그램이 hello = 1 + 2라면—덧셈을 나타내는 더 복잡한 트리(두 자식을 가진)가 만들어질 것이다. 물론 이게 파이썬이니 +의 실제 의미도 지저분하다.
이러한 파싱이 일어나는 정확한 메커니즘은 여기 범위를 다소 벗어난다. 많은 언어에서 문법은 모호하고, 우리가 먼저 떠올릴 법한 상향식이 아닌 하향식 파싱 전략은 작동하지 않기도 하며, 파싱이 대신 상향식으로 이뤄져야 할 때가 있다. 이 과정은 복잡하고, 알고리즘적으로 매혹적이며, 종종 좋은 오류 메시지를 내는 데 서툴다. 여기에 우리는 충분히 주목하지 않는 흥미로운 트레이드오프들이 존재한다.
모든 언어 구현이 위 파이프라인을 따르는 것은 아니다. 예를 들어 스캐너 없는 파싱을 실험한 이들도 있다. 개념적으로나 실무적으로나, 그래도 이 구분은 유익하다:
우리는 도구를 만들 때도 복잡도 이론의 이점을 취해야 한다! 또한 이는 관심사의 훌륭한 분리기도 하다. 예를 들어, 같은 스캐너를 재사용하면서 속도나 메모리(혹은 둘 다)와 더 나은 오류 메시지 사이의 트레이드오프를 달리하는 여러 파서를 상상할 수 있다.
따라서 아래에서는 스캐너를 기정 사실로 두겠다. 그러면 전통적 파이프라인에는 사실상 한 단계—파서—만 남는다. 나는 이를 단원제(unicameral) 파이프라인이라 부른다.
많은 나라에는 양원제 입법부가 있다. “bi”는 둘, “camera”는 방을 뜻한다. 즉, 의회에 두 개의 하우스(의회)가 있다. 예컨대 인도에는 로크 사바와 라자 사바가 있고, 영국에는 하원과 상원이 있으며, 미국에는 하원과 상원이 있다. 모두 “하원”과 “상원”이 있다.
여기서의 목적은 계급 관련 연상을 옹호하거나 실제 관행을 파고드는 것이 아니라, 이를 유익하고 대략적인 비유로 쓰려는 것이다. 양원제에 대한 이론은 이렇다. 하원은 좀 더 소란스럽고 떠들썩하다. 터무니없는 법안은 걸러내지만, 완전히 현명하지 못한 법안도 여럿 통과시킨다. 상원은 좀 더 차분하고 심사숙고한다. 입법부에서 상원의원은 임명되거나 더 긴 임기를 갖기 때문에, 모든 포퓰리즘의 바람에 휘둘릴 필요가 없다. 그래서 상원은 유용한 필터 역할을 한다.
즉, 의회는 두 하우스를 가진다. 하원은 잡동사니를 거르되, 때때로 현명하지 못한 법안도 통과시킨다. 상원은 이러한 법안들에 판단을 내리고, 자신들이 현명하다고 여기는 것만 허용한다. 정치학 얘기는 이쯤 하고 컴퓨터 과학으로 돌아가자.
앞서 언급했듯이, 파서만 있으면 단원제 구문이다. 리스프스러운 언어가 제공하는 것은 양원제 구문이다. 하지만 리스프만의 전유물은 아니다. XML과 JSON 같은 표기도 양원제다. 내 요점을 분명히 하기 위해 여기서는 다른 표기들을 사용하겠다.
먼저, 이들 모든 표기는 스캐너를 통과해야 한다. XML에서 <title처럼 닫는 > 없이 쓸 수 없다. 그것은 유효한 여는 태그가 아니다. JSON에서 "hi처럼 닫는 " 없이 쓸 수 없다. 그것은 유효한 문자열이 아니다. 이런 것들은 기본적인 토큰화 실패라 볼 수 있다. 그러니 입력이 스캐너를 통과해 토큰 목록(또는 스트림)을 얻었다고 하자.
토큰을 제대로 썼더라도, XML이나 JSON에서 여전히 할 수 없는 것들이 있다. 예컨대 다음 전체 문서는 XML에서 합법이 아니다:
xml<foo><bar>This is my bar</bar>
왜냐하면 <foo>를 </foo>로 “닫지” 않았기 때문이다. 이 역시 합법이 아니다:
xml<foo><bar>This is my bar</foo></bar>
중첩 구조를 보존하지 않았기 때문이다(즉, foo를 닫기 전에 bar를 먼저 닫아야 한다). JSON도 마찬가지다. 구조를 열었으면 닫아야 하고(또는 그 반대), 중괄호와 대괄호는 연 순서의 역순으로 닫아야 한다.
여기서 주목할 점은, 우리가 데이터가 무엇을 표현하는지 몰라도 이런 판단이 가능하다는 것이다. 의미 있는 이름 대신 foo, bar 같은 이름을 쓴 이유가 있다. 바로 상관이 없기 때문이다. 이 XML과 JSON 데이터는 아주 기초적인 수준에서 잘못되었다. 우리는 이를 정형성(well-formedness)을 만족하지 못한다고 말한다. 이것이 우리의 하원이다.
하지만 더 복잡한 규칙이 있을 수 있다. 예를 들어 bar가 foo 안에 들어가면 안 될 수도 있고, 모든 baz는 하나 이상의 quux가 필요할 수도 있다. XML이나 JSON 포맷을 다뤄본 사람이라면(그게 문서화가 잘 되었든 아니든!) 반드시 지켜야 하는 온갖 규칙들이 있다는 걸 알 것이다. 예컨대 웹 API 문서를 읽을 때, 어떤 종류의 요청에서는 특정 필드를 제공해야 하지만 어떤 키는 제공해서는 안 된다고 적혀 있을 수 있다. 이런 것들을 유효성(validity) 규칙이라 부른다. 이들은 정형성 규칙 위에—사실 정형성 규칙을 생략하고—얹혀 있으며, 그때 검사하지 않았던 것을 집중해서 검사한다. 이것이 상원이다.
이로써 새 파이프라인이 생긴다. 중간에서 정형성을 검사하는 도구가 있고, 그 다음 유효성을 검사하는 도구가 있다. 마지막 단계—유효성을 확인한 다음 “품사” 태그가 붙은 항목을 만들어내는—가 여전히 파서다. 하지만 이제 그 중간에 도구가 하나 더 있다. 이를 가리키는 표준 이름이 있는 것 같지는 않지만 있어야 한다. 리스프 전통을 따라, 나는 이를 리더(reader)라 부른다:
이 상태로는 리더의 출력이 무엇인지 분명하지 않다. 리더가 토큰을 검사만 하고, 이전 파이프라인처럼 같은 토큰 목록(또는 스트림)을 파서로 넘길 수도 있다. 하지만 그것은 손해다! 리더는 매우 유용한 중간 데이터 구조를 만들 수 있다. 리더가 주로 보는 성질은 “것들이 서로 맞물리고 올바르게 중첩되었는가”이다. 다시 말해, 리더는 입력이 트리임을 확인한다. 따라서 파서에 멍청하고 납작한 토큰 목록을 넘기는 대신, 트리를 만든다.
이것은 두 가지 이유로 파서의 일을 크게 단순화한다. 기본적으로는, 이전에 파서가 해야 했던 검사 중 일부를 이미 끝낸다. 더 흥미로운 점은, 토큰은 매우 저수준인 반면 트리는 고수준이라는 것이다. 이미 정형성 있는 트리를 입력으로 받으면, 유효성을 검사하고 추상 구문 트리(AST)를 만들어내는 재귀 함수를 작성하기가 훨씬 쉽다! 이런 이유로 PLAI는 이를 “Primus Inter Parsers”라 부르는데, 대부분에게는 잘 전달되지 않는 농담 같다.
다시 복잡도 이론을 유익한 안내자로 삼으면, 우리가 한 일은 차근차근 복잡도를 끌어올린 것이다:
그리고 우리는 다시금 이런 관심사 분리를 활용해 필요할 때 구현을 바꿔 끼울 수 있다.
이것이 곧 양원제 구문이다. 리더가 “하원”이고, 기초적인 실수를 거르되 여전히 몇몇 유효하지 않은 것들을 통과시킨다. 하지만 그것들을 더 깔끔하고 다루기 쉬운 구조로 싸서 건넨다. 파서는 “상원”으로서 이 깔끔한 구조를 받아 더 깊은 결함을 찾아내고, 더 엄정한 심사를 통과한 것만 허용한다.
양원제 구문의 장점은 많다:
이러한 (정확한!) 구현의 단순성은 중요하다! 언어 구현이 하나뿐일 때는 중요성이 덜할 수 있다. 하지만 컴퓨팅에서 오가는 많은 트래픽은 트리로 표현하는 것이 가장 적절하다. 트리는 문자열보다 훨씬 강력하면서도 쓰고 읽기엔 충분히 단순하다. 따라서 JSON 같은 양원제 구문이 범용적으로 쓰이게 된 것은 놀랍지 않다. 이제 모든 언어는 이를 위한 리더가 필요하고, 이 리더들은 같은 언어를 구현해야 하므로 명세와 구현의 단순성이 미덕이 된다. 이어서 많은 시스템이 JSON 입력을 별도로 검증해야 하는데, 다시 한 번 이런 종류의(트리를 소비하는) 파서를 작성하는 단순성이 큰 이점이다.
이 중간 단계는 도구 측면의 다른 장점도 있다. 구문을 지원해야 하는 것은 언어 구현만이 아니다. 에디터도 필요하다. 정형성 수준에서 괄호 맞추기, 들여쓰기, 색상화 등을 지원하기가 훨씬 쉽다. 트리 들여쓰기도 쉽다! 트리 순회도 쉽다. 모든 언어에 적용 가능한 의미 있는 일반 트리 수준 연산(“여는 곳으로 이동”, “닫는 곳으로 이동”, “한 단계 위로”, “한 단계 아래로” 등)이 존재하고, 같은 정형성 특성을 공유하는 모든 언어(JSON 등)에 대해 적용할 수 있다. 그리고 우리는 에디터가 많으며, 각 에디터는 같은 구문을 지원해야 한다. 양원제 언어는 정형성이라는 중간 개념을 제공함으로써 도구들이 정확하고, 유용하며, 상대적으로 쉽다는 ‘삼박자’를 달성하도록 해 준다.
이 장점들은 하나의 단점과 균형을 이룬다. 어떤 사람들은 이를 그냥 좋아하지 않는다. 항상 프로그램을 더 자유로운 문법이 아니라 트리라는 형태로 써야 한다는 것이 답답하게 느껴진다. 설령 양원제 내부에서도 트레이드오프가 있다. XML은 닫을 때 태그를 반복하도록 요구해서 오류 검출과 리팩터링 시 실수 포착에 도움이 되지만 작성은 번거롭다. 리스프는 태그 반복을 요구하지 않지만 전통적으로 한 종류의 괄호만 사용해 구조 파악이 어렵다. JSON은 구분자를 섞고 닫는 태그를 피함으로써(편집 오류를 방지하는 검사를 일부 제공하면서) 일종의 ‘스위트 스폿’을 찾은 듯하다. 그럼에도 데이터를 쓸 때는 받아들이는 사람들이, 프로그램을 쓸 때는 이를 못마땅해 하므로 리스프스러운 구문에 대한 오래된 반감이 생겨난다.
우리는 리스프로 시작했으니, 다시 돌아가 보자. 리스프란 무엇인가? 리스프는 느낌, 감정, 정서다; 리스프는 바이브다; 리스프는 이슬 맺힌 아침 풀잎, 산들바람에 실려 오는 소나무 향, 배트에 맞는 크리켓공의 소리, 그리곤… 어, 어디까지 했더라. 미안하다.
좋다, 진지하게 가자. 내가 줄곧 “리스프스러운 언어들”이라고 쓰는 점에 주목하라. “스러운”과 “언어들” 둘 다 눈에 띄었을 것이다. 무슨 뜻일까?
나는 특정 언어(예: 커먼 리스프)를 말하는 게 아니다. 커먼 리스프를 포함하되 스킴, 라켓, 클로저 등 많은 언어를 아우르는 한 가족을 말한다. 이 가족은 문화적 전통에 의해 묶여 있지만, 개별 언어는 매우 다를 수 있다. 이들이 공유하는 것은 구문에 대한 철학이다. 그리고 이제 그 철학을 충분히 살펴보았으니, 관대하게 보자면 어떤 양원제 언어든 본질적으로 “리스프스러운” 것이라 할 수 있다.
이 철학의 힘은 언어 플랫폼이 양원제 구문을 한 번 잘 구현해 두면, 많은 언어가 이를 상속받을 수 있다는 데 있다. 앞서 JSON 위에 세워진 (데이터) 언어가 많다고 했다. 각 언어는 유효성 규칙이 제각각이다. 우리는 마찬가지로 어떤 양원제 구문 위에 많은 데이터 및 프로그래밍 언어를 구축할 수 있다. 이들은 가족적 특질을 공유하면서도 세부에서는 꽤 달라질 것이다.
하지만 리스프의 양원제적 특성은 몇 가지 오해를 낳기도 한다:
특정 구현 전략을 강제하지 않는다. 예를 들어 리스프에서는 종종 “코드는 데이터다. 왜냐하면 코드는 s-식으로 표현되고, s-식은 리스프 데이터이기 때문이다.”라는 주장을 한다. 이 용어들에 대한 전체 튜토리얼은 여기서 다루고 싶지 않다. 내가 하고 싶은 말은, 이것이 전혀 성립하지 않는 리스프스러운 언어들도 있다는 것이다.
내가 가장 잘 아는 예는 라켓이다. 왜냐하면 그 일부를 내가 처음 만들었기 때문이다. 라켓에서 코드는 s-식으로 표현되지 않는다. “코드”는 훨씬 더 많은 것을 필요로 하기 때문이다. 예컨대 오류를 보고할 때 사용할 수 있도록 소스 위치를 기록해야 하고; 우리가 무시할 수 있다고 했던 공백들 기억하는가? 오류를 보고하려면 무시할 수 없다! 위생(hygiene) 정보를 담아 우발적 포착을 피해야 하며, 기타 등등이 필요하다. 따라서 라켓에는 syntax object라는 개념(그래, 이것도 데이터다)이 있어 훨씬 더 풍부한 “프로그램 소스” 개념을 포착한다. Syntax는 s-식을 병렬(그리고 확장)하는 별도의 타입이며 s-식과 상호 변환할 수 있지만 s-식이 아니다. 따라서 (예컨대) 리스트에 작동하는 많은 함수들이 있고 s-식에는 리스트가 포함되지만, 그 함수들은 syntax에는 적용되지 않는다.
사람들은 때때로 read 원시 연산이 “파싱한다”고 말한다. 아니다. read는 읽는다. 입력이 정형성 있는지 확인한다는 점에서 “파싱한다”고 할 수 있을지 모르나, 문맥 자유 및 문맥 의존 규칙에 따라 유효성을 판단하고 “품사”를 식별하는 전통적 의미의 파서가 아니므로, 리스프가 파서를 기본 제공한다고 말하는 것은 틀렸다.
이를 구체화해 보자. 다음은 read가 아무 문제 없이 받아들이는 정형성 있는 리스프스러운 항목이다: (lambda 1). 그러나 이는 대부분의 리스프에서 구문 오류다. 이 판단은 리더가 아니라 파서가 내린다. 물론 우리가 새로운 언어를 만들어 그 항목에 어떤 의미를 부여하도록 막을 것은 아무것도 없다. 실제로 나는 이제 만들었다. 그 언어의 파서가 그 조각들을 해석하는 책임을 진다.
매크로는 양원제 구문이 있을 때만 가질 수 있다고 믿는 이들이 있다. 사실이 아니다. 필요한 것은 코드를 표현할 수 있는 방법이다. 매크로는 코드를 코드로 변환하므로. 하지만 더 많은 언어가 매크로의 이점을 인식하면서, 일부는 절차적 매크로(리스프 전통에서 수십 년 전부터 있었던 아이디어)를 도입했다. 양원제 구문은 그저 영광스러운 매크로 시스템으로 가는 관문을 제공할 뿐이다. 우리는 정형성 층이 매크로를 쌓기에 특히 좋은 층이라는 것을 배웠다.
마지막으로 미래지향적 코멘트 몇 가지로 끝내고자 한다.
내가 자주 즐기고(그리고 답답해하는) 것 중 하나는 “프로그래밍 언어를 어떻게 만들죠?”라는 질문을 찾아보고 그들이 받는 조언을 읽는 일이다. 이 대화는 반드시 구문 문제에 발이 묶인다. 하지만 초보자가 구문의 온갖 자질구레한 것들을 만들고 파서를 만드는 법을 배우는 동안, 보통 복잡도·인지·시간 예산을 다 써 버린다. 결과적으로 언어는 끝내 완성되지 않거나 의미론적으로 난장판이 되곤 한다.
나는 다른 경로를 제안하고 싶다. 양원제 구문으로 시작하라. 빠르게 의미론을 생각할 수 있는 지점으로 가라. 당신의 언어는 무엇을 하는가? 프로그램을 어떻게 추론하는가? 어떻게 실행하는가? 어려우면서도 흥미롭고 중요한 부분들을 먼저 하라.
하지만 당신은 이렇게 반박할 것이다. “이제 양원제 구문이 되었잖아! 아무도 여기에다 대고 프로그래밍하지 않을 걸!” 그럴지도 모른다. 하지만 다음 관점을 생각해 보길 바란다:
구문은 하나의 뷰다.
소프트웨어 설계에서 우리가 배운 오래 가는 교훈 중 하나는 모델-뷰 분리다. 즉, 같은 사물을 바라보는 유용한 방법이 종종 여럿 존재하며, 그중 일부는 아직 생각지도 못했을 수 있으므로, 객체를 그것을 보는 방법과 분리하는 것이 현명하다는 것이다(그리고 서로 다른 뷰들을 조율한다). 이 원칙은 프로그램에는 거의 적용되지 않는데, 그래서는 안 된다! 우리는 추상 구문을 모델—“유일한 진짜 프로그램”—로 보고, 모든 구체 구문을 그에 대한 뷰로 보아야 한다.
그러니 당신이 정말 해야 할 일은 추상 구문을 정의하고, 그 위에 가벼운 양원제 구체 구문을 얹는 것이다. 그리고 다른 모든 부분을 만든다. 그런 다음 어떤 뷰를 제공할지 생각하라. 어떤 사람은 양원제 구문으로도 행복할 것이다. 어떤 사람은 더 전통적인 중위 표기를 원할 것이다. 누군가는 중괄호를, 누군가는 유의미한 공백을 원할 수 있다. 어떤 사용자는 블록—궁극의 양원제 구문—을 쓰고 싶어할지도 모른다. 구문을, 힘든 작업이 끝난 뒤에 디자인하고 배포할 수 있는 재미있는 디저트로 보라(그리고 그래야 한다). 이것이 구문이 어렵지 않다는 말은 아니다. 사실 대부분의 사람이 생각하는 것보다 어렵다. 수학과 인간 요인—시각, 운동, 기타 장애를 가진 사람들에 대한 접근성 포함—의 섬세한 상호작용을 요구하기 때문이다. F*dging up a Racket과 훨씬 더 깊이 있는 Beautiful Racket은 이 원칙을 실천적으로, 그리고 아름답게 보여 준다.
이 관점은 또 하나의 유용한 산출물을 준다. 당신의 언어로 프로그램을 생성해야 하는 프로그램들의 아주 좋은 타깃으로 쓸 수 있는 양원제 구문이다. 이는 더 이상 새로운 아이디어도 아니다. 그러니 급진적일 필요가 없다. SMT-LIB와 WebAssembly 텍스트 포맷 같은 형식이 s-식을 택한 데에는 이유가 있다. 우리의 Forge 도구는 Alloy 기반 구문과 s-식 기반 구문을 모두 지원한다. 전자는 (대부분의) 사람들이 쓰기 위한 것이고, 후자는 Forge를 백엔드로 쓰는 도구들의 타깃이다.
요컨대, 이것이 구문 설계의 새로운 비전을 북돋우기를 바란다. 양원제는 우리가 찾아낸 최고의 중간 언어다. 이 점은 이론적 우아함과 실무적 경험 모두에 의해 뒷받침된다. 일부에게 양원제 구문은 사랑스러운 소스 언어이기도 하지만, 핵심 추상 구문에 대한 많은 뷰 중 하나여야 한다. 도구는 더 재사용 가능해지고, 동시에 구문 전쟁은 끝날 수 있다. 우리는 평화와 기쁨과 행복을 찾을 수 있다. 한 쌍의 괄호가 포옹처럼 보이는 것은 우연이 아니다.