호모아이코닉(Homoiconic) 파이썬 언어
(substack.com/aljamal)- 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실행까지 포함함
댓글과 토론
Hacker News 의견들
-
간단하고 빠른 Python풍 스크립팅 Lisp에 관심 있다면 Clojure 스타일 언어로 Hy와 Janet을 볼 만함
- Hy(https://hylang.org/): Python 바이트코드로 컴파일되고, 보통 Python보다 느리지만 모든 Python 라이브러리와 호환됨
- Janet(https://janet-lang.org/): Lua 스타일의 매우 가벼운 내장형 가상 머신으로 약 1MB이고, 비슷한 연산에서 Python보다 대략 2배 빠르며 C 상호운용이 아주 쉬움
- 빠른 Python풍 스크립팅이라면, 배터리 포함 Common Lisp인 https://github.com/ciel-lang/ciel도 있음
빠르게 시작하는 바이너리로 제공되고, 일상적인 작업용 라이브러리도 포함
CL↔Python 쪽은 https://github.com/CodyReichert/awesome-cl?tab=readme-ov-fil...도 참고할 만함 - Hy가 Python보다 느리다고? 적어도 실행 시간에는 그러면 안 됨
내가 Hy 유지보수자인데, 의미 있는 성능 차이가 보이면 그건 버그임 -
- Basilisp(https://github.com/basilisp-lang/basilisp): “Python 3.8+를 대상으로 하는 Clojure 호환-ish Lisp 방언”
- 변덕처럼 보일 수 있지만, Lisp는 필수적인 둘러싸는 괄호 때문에 늘 반박을 부르는 언어처럼 느껴졌음
M-표현식은 이 함정을 피하니, Lisp의 동형성(homoiconicity)과 개념적 우아함을 유지하면서도 이런 괄호를 요구하지 않는 실제 언어가 있었는지 궁금함 - https://github.com/yaml/yamlscript
딱 생각하는 그거임. 설명도 아주 직설적임: “Program in YAML”
더 말할 필요가 거의 없음
-
잠깐, 이건 그냥 Python으로 구현한 Lisp 아닌가?
제목이 암시하는 것처럼 동형적인 Python은 아닌 것 같은데, 내가 뭘 놓친 건가?- Greenspun의 열 번째 법칙에 따르면 맞는 말임: https://en.m.wikipedia.org/wiki/Greenspun's_tenth_rule
- 나도 동료들에게 Lisp를 받아들이게 하지 않고도 쓰게 만들 수 있는 동형적 Python인 줄 알고 꽤 설렜는데, 아니었고 그냥 Lisp였음
- 그렇게 보임. 이 동형적 Python을 보고 싶어서 궁금했지만, 가능한지도 잘 모르겠음
그러려면 Python 문법이 튜플, 리스트, 딕셔너리로 구성돼야 하고, 인터프리터가 그것들을 직접 평가해야 할 것임 - 차이가 뭐임?
-
Python으로 간결하게 구현할 수 있는 또 다른 함수형 언어로 Binary Lambda Calculus가 있음
코드의 상당 부분은 BLC의 순수 입출력 모델을 다루며, 변수 조회에는 연관 리스트 대신 de Bruijn 인덱스로 환경 배열을 인덱싱함
같은 페이지에는 9개 다른 언어 구현도 있고, BLC의 자기 인터프리터가 파서와 토크나이저까지 포함해 232비트(29바이트)로 가장 간결함
[1] https://rosettacode.org/wiki/Universal_Lambda_Machine#Python -
MIT의 프로그래밍 기초 과목은 모든 학생에게 Python으로 Lisp 인터프리터를 작성하게 함
예전에 그 수업이 실제로 Lisp로 진행되던 시절의 흔적임
https://py.mit.edu/spring24- 비슷하게, 내가 대학에서 들은 수업도 Lisp로 기초적인 Lisp 인터프리터를 구현하게 했음
-
비슷한 걸 JS 리스트로 해본 적 있음: https://github.com/andrelaszlo/js-lisp
-
현대 언어들이 수십 년 뒤에 Lisp의 놀라운 기능들을 다시 발견하는 게 웃기면서도 아이러니함
며칠 전 내 홈 서버 API를 9시간 호출하던 Python 프로그램이 멈춰서 화가 났음. 비슷한 API 호출들이었고, 미리 정의한 프롬프트 템플릿과 문법 제약으로 LLM을 호출하고 있었음
남은 반복을 따로 돌리기 전에 프로그램 상태를 저장하고 종료하고 싶었지만, 실행 중인 Python 코드를 수정하거나 변수를 들여다보는 방법을 찾지 못해 결국 9시간 작업을 날림
며칠 뒤 https://malisper.me/debugging-lisp-part-1-recompilation/를 봤고, Common Lisp가 이런 기능을 수십 년 전부터 언어에 내장했다는 사실이 놀라웠음. 가장 강력한 매크로 시스템 같은 다른 기능들도 마찬가지임- 현대 언어가 수십 년 뒤 Lisp의 기능을 다시 발견하는 건 놀랍지 않음. Lisp 계열은 가장 유연하고 표현력 있는 언어들이기 때문임
하지만 그게 문제이기도 함. 표현력이 너무 강함
Lisp는 늘 혼합 매체 시각예술을 떠올리게 했는데, 표현의 자유는 좋아 보이지만 결국 더 전통적인 단일 매체 예술보다 결과물이 대체로 떨어지는 경우가 많음. 매체의 제약도 표현력만큼 중요하다는 사실이 드러남 - 어느 정도 상태를 들여다보려면 Py-spy나 Pystack을 쓸 수 있음
또는 https://github.com/malor/cpython-lldb도 있음
더 많은 방법은 https://github.com/albertz/pydbattach/ - 여러 시간 돌아가는 프로세스라면 작업을 디스크에 저장하고, 중단된 지점부터 다시 시작할 준비가 되어 있어야 함
- 예외가 던져지진 않았음?
- 현대 언어가 수십 년 뒤 Lisp의 기능을 다시 발견하는 건 놀랍지 않음. Lisp 계열은 가장 유연하고 표현력 있는 언어들이기 때문임
-
이 구현이 어떻게 동형적 Python임?
-
프로그램이 커지면서 Lisp가 읽기 어려워지는 경향을 어느 정도 제어해 주는 타입 시스템이 있는 Lisp가 있을까?
타입 시스템이 아니더라도 그런 성질을 다스릴 수 있는 다른 요소가 있는지 궁금함
메타프로그래밍은 정말 멋지지만, 때로는 지금까지 본 가장 난해한 추상 Haskell처럼 읽히면서도 길잡이가 될 타입 시그니처조차 없음
타입 시스템과 린터는 코드를 자동으로 길들이는 가장 좋은 도구라고 보지만, Lisp를 선택하는 이유 자체를 제약하지 않으면서 Lisp 프로젝트의 경향을 어떻게 억제할 수 있을지 잘 상상이 안 됨- 타입 시스템이 있는 Lisp라면 Common Lisp가 떠오름
사실상 표준에 가장 가까운 SBCL은 컴파일 시간 타입 검사도 꽤 좋음
예를 들어(declare (type String a b))뒤에(+ a b)를 쓰면,A의 파생 타입이STRING인데NUMBER여야 한다고 경고를 잡아줌
내가 Scheme에서 CL로 옮긴 가장 큰 이유가 이 타입 검사였고, 이후에는 restarts와 continuable asserts 같은 소소하게 좋은 기능들 때문에 계속 머물렀음 - 친근하고 접근하기 쉬운 쪽은 Typed Racket, 강력하지만 복잡한 쪽은 Shen이 있음
- https://en.wikipedia.org/wiki/Dylan_(programming_language)
- 타입 시스템이 있는 Lisp라면 Common Lisp가 떠오름
-
표현식 지향 Python이 있다면 지금의 Python보다 훨씬 나을 것임
이 글은 제목이 암시하는 내용은 아니지만, Lisp 설명으로는 좋음- 표현식 지향 Python은 그냥 Python이 아닐 것임
그래도 괜찮지만, “표현식을 2등 문법으로 두는 것”은 꽤 근본적이고 의도적인 선택임. 괄호 수프를 쓰려는 개발자가 힘들게 만들기 위한 설계이기도 함
그래서lambda:와:=는 사용을 억제하려고 일부러 투박하고, 익명 함수도 작성할 수 없음
Python은 똑똑한 일을 하고 싶다면 호출 가능 객체가 아니라 반복자로 하라고 말하는 언어임
- 표현식 지향 Python은 그냥 Python이 아닐 것임
-
모든 것에 Lisp식 무문법이 있었으면 좋겠음
나한테는 그게 더 좋고, 다행히 CL에서는 그렇게 하기 쉬움