Mere 로 Scheme 을

다른 언어의 인터프리터를 쓰는 것은 형 시스템을 시험하는 고전적인 방법이고, Mere 가 지금까지 요구받지 않은 두 가지를 찔렀다 —— 추상 구문이자 실행시 값으로 쓰는 재귀적 다형 variant, 그리고 포획한 환경을 나르는 값이다. 작은 Scheme —— reader, 트리를 걷는 평가기, printer, 약 260 줄 —— 이 둘 다를 부렸다. s-식 형은 코드이자 데이터이자 값이다. 그 케이스 중 하나가 lambda 의 인자 · 본체 · 닫아 넣은 환경을 가지므로 함수는 정의 스코프를 포획한다. 팩토리얼, map, foldr, 더하고 합성하는 closure, quote, 단락 형 —— 모두 올바르고 interp 와 C backend 에서 byte 일치, 언어에 대한 변경은 하나도 없다. 같은 날의 line-diff probe 도 같은 이야기를 했다. 유일한 마찰은 예약 이름이고, 그것이 다음 편이다.

mereinterpretervariantsclosuresdogfood

다른 언어의 작은 인터프리터를 쓰는 것은 자신의 언어가 무엇으로 되어 있는지 알아내는 가장 오래된 방법 중 하나다. 재귀, 패턴 매칭, 환경을 한 번에 부리고, Mere 의 probe 가 지금까지 피해 온 두 가지를 묻는다. 재귀적 다형 variant 는 추상 구문 트리이자 실행시 값으로 봉사할 수 있는가, 그리고 값은 포획한 환경을 날라 closure 가 동작하는가. Scheme 은 자연스러운 소재다 —— 그 코드는 이미 데이터다 —— 그래서 하나 썼다. s-식을 parse 하는 reader, 트리를 걷는 평가기, printer, 약 260 줄.

코드와 데이터와 값의, 하나의 variant

인터프리터 전체가 하나의 재귀 variant 를 축으로 돈다. s-식은 nil, 정수, 불린, 심볼, pair, closure, 또는 primitive —— 그리고 그 하나의 형이 reader 가 낳는 구문이고, 프로그램이 다루는 데이터이며, 평가기가 돌려주는 값이기도 하다, 동시에. 이것은 Lisp 의 정의적 특징이고, 그것을 깔끔히 표현하는 것은 variant 의 진짜 시험이다. 평가기는 “머리가 심볼 if 인 pair” 같은 깊이 중첩된 형태를 매치해 special form 을 인식하고, 패턴 매처는 망라성 검사를 유지한 채 그 중첩 케이스들을 다뤘다. 자기 참조적인 구문=값 형을 표현하는 것은 언어에 아무 부담도 주지 않았고, 어느 ML 에서든 그렇듯 읽혔다.

자신의 스코프를 닫아 넣는 값

더 어려운 물음은 closure 였다. lambda 가 평가되면 그 결과는 정의된 환경을 기억해야 한다. 다른 함수 에서 돌려진 함수가 포획한 변수를 여전히 보도록. Mere 는 지금까지 환경을 값 안에 넣으라고 요구받은 적이 없었다. s-식 variant 의 closure 케이스는 세 가지를 가진다 —— 인자 리스트, 본체, 그리고 포획한 local 환경, 이 자체도 그저 s-식 연관 리스트다 —— 그리고 그것으로 충분했다. 포획한 수를 더하는 lambda 를 돌려주는 함수 make-adder, 둘에서 새 함수를 짜는 compose, 둘 다 lexical 스코프가 요구하는 대로 행동했다. 돌려진 closure 는 태어난 프레임의 올바른 바인딩을 보았다. global 정의는 재귀가 동작하도록 가변 테이블을 지나고 —— 함수는 자신을 부를 무렵엔 스코프에 있다 —— local 바인딩은 lexical 스코프를 위해 포획한 alist 를 따랐다.

측정, 그리고 마찰

모든 테스트 프로그램이 올바른 답을 냈다. interp 에서도 C backend 에서도 동일하게. 10 의 팩토리얼, range 위 제곱의 map, 합의 fold, closure 예제, 코드를 데이터로 지키는 quote, 단락하는 andor. 같은 날의 다른 probe —— 이차원의 최장 공통 부분 수열 테이블 위에 backtrack 으로 edit script 를 복원 하는 행 단위 diff —— 도 같은 이야기를 했다. system 의 diff 도구에 비추어 올바르고, 새 버그 0. 이것이 물건을 세워 잴 때의 언어의 성숙이 보이는 모습이다. Scheme 인터프리터와 diff 도구, 둘 다 비자명하고, 둘 다 컴파일러를 바꾸지 않고 쓰였다. 유일한 마찰은 언어의 버그가 아니라 언어와의 충돌이었다 —— run 이라는 이름의 함수가 두 backend 에서 다르게 행동했다. run 도 내장이기 때문이다. 그 실을 따라 가는 것이 다음 편이고, 버그의 한 사례가 아니라 한 종류를 끝낸다.

← Back to Mere: 언어를 만들다