- Lisp의 호모아이코닉성은 코드와 데이터를 같은 형태로 다루는 성질이며, 고전적인 “Lisp in Lisp”를 Python으로 옮기면 이 아이디어가 더 익숙한 문법 안에서 드러남
- 원래 Lisp는 코드 표현인 M-expression과 데이터 표현인 S-expression을 함께 두었고, “Lisp in Lisp”는 M-expression으로 S-expression Lisp를 구현함
- Python 버전은 S-expression을 Python 리스트로 표현하고, M-expression은 함수 호출과 조건문으로 옮겨 별도 파서 없이 인터프리터를 구성함
- 첫 인터프리터는
atom,eq,car,cdr,cons,append같은 리스트 원시 연산 위에 서며,lambda지원을 위해assoc,pairlis, 환경 리스트가 더해짐 eval은 식과 환경을 함께 받아 변수 바인딩을 처리하고,pairlis와assoc를 통해 동적 스코프 방식으로 인자와 값을 연결함
Lisp가 보여준 코드와 데이터의 통합
- Lisp는 1960년대 초 John McCarthy의 Lisp paper와 Lisp 1.5 manual을 통해 수십 년 뒤에도 유효한 여러 아이디어를 남김
- 그중 핵심은 호모아이코닉성임
- 일반적인 언어에서는 코드가 데이터에 작용하는 연산의 나열로 이해됨
- Lisp는 코드와 데이터를 같은 형태로 다뤄 연산자와 피연산자의 경계를 흐림
- Alan Kay는 Lisp 1.5 manual 13쪽 하단의 “Lisp in Lisp” 코드를 “Maxwell's Equations of Software”라고 불렀음
- 몇 줄의 코드가 전체 프로그래밍 세계를 담고 있다는 점에서 큰 깨달음이었다는 취지의 인용이 포함됨
“Lisp in Lisp”를 Python으로 옮기는 방식
- 목표는 고전적인 “Lisp in Lisp” 코드를 Python으로 다시 쓰면서 원래 코드의 정신을 최대한 유지하는 데 있음
- Lisp는 두 가지 문법적 표현을 가짐
- M-expression: 코드 표현, meta의 약자
- S-expression: 데이터 표현, symbolic의 약자
- 두 표현은 의미적으로 동등함
- 기존 “Lisp in Lisp” 코드는 M-expression으로 작성되어 S-expression Lisp를 구현함
- Python 구현에서는 Lisp S-expression을 Python 리스트로 표현함
- Lisp는 “List Processing”의 약자이고, 리스트라는 하나의 데이터 구조를 중심으로 작동함
- Python 리스트는 Lisp S-expression을 에뮬레이션하기에 적합한 표현으로 사용됨
- M-expression은 Python의 함수 호출과 조건문 같은 코드 구조로 번역됨
- 이 매핑 덕분에 문자열 조작이나 별도 파서 구현 없이 인터프리터를 만들 수 있음
리스트 원시 연산으로 만든 첫 인터프리터
- Lisp 구현에는 언어 바깥에서 제공되는 몇 가지 기본 함수가 필요함
- Python 구현에 사용된 리스트 원시 연산은 다음과 같음
atom(x):x가 리스트인지 확인eq(x,y):x와y가 같은지 확인car(x): 리스트의 첫 요소cdr(x): 리스트의 나머지cons(x,y): 원자를 리스트에 붙임append(x,y): 두 리스트를 이어 붙임
- 몇 가지 재귀 원시 연산을 제외하고, Llama3-70b를 Groq에서 활용해 “Lisp in Lisp” 코드의 부분집합을 실행하는 인터프리터를 빠르게 만들 수 있었음
- 예제에서는 Python 리스트가 S-expression처럼 동작함
- 전체 코드는 github gists에 제공됨
lambda와 재귀를 위한 확장
- 첫 구현에는 중요한 기능인
lambda가 빠져 있음lambda는 Lisp에서 익명 함수를 정의하고 호출하는 주된 방식임- Lisp에서
lambda가 없으면 재귀를 구현할 수 없음 - 재귀가 없으면 계산 가능한 모든 것을 계산할 수 있는 최소 기준인 튜링 완전성에 도달하지 못함
lambda를 넣기 위해assoc(x,y)와pairlis(x,y)가 추가됨assoc(x,y)는 리스트로 구현한 키/값 조회이며, 연관 리스트를 사용함pairlis(x,y)는 Python의zip(x,y)처럼 두 리스트를 묶음
- 원래 Lisp는 단순한 선형 스캔도 재귀로 처리해야 했음
- 원래 Lisp에는 루프가 없었기 때문임
- Python 번역에서는
assoc와pairlis를 리스트 컴프리헨션으로 더 간결하게 표현할 수 있음
COND처리에서는 원래 Lisp의evcon을 루프로 번역하고,LAMBDA처리에서도evlis에 같은 방식을 적용함
환경 리스트와 동적 스코프
- 원래 Lisp의
eval함수는 두 인자를 받음- 첫 번째 인자는 평가할 S-expression
- 두 번째 인자는 키/값 목록으로 된 환경 리스트임
- 환경은
LAMBDA처리에서 변수 바인딩을 유지함- 함수에
x변수가 있고 데이터를 대입하면,pairlis가x심볼과 데이터를 묶음 - 묶인 값은 환경 리스트에 저장되거나 추가됨
x가 필요할 때는assoc가 환경에서 찾아 식에 다시 대입함
- 함수에
- 이 바인딩 방식은 동적 스코프로 불림
- 최종 구현은 원래 “Lisp in Lisp”를 Python으로 옮긴 형태이며, 마지막 예제에서
lambda실행까지 포함함