Janet 탐구: 실용적인 개발자를 위한 현대적 Lisp

많은 개발자에게 도처에 널려 있는 괄호의 모습은 즉각적인 거부감을 줍니다. 학술적인 Lisp 방언이나 함수형 프로그래밍의 인지된 복잡성 때문에 "Lisp 방식"은 종종 과거의 유물처럼 느껴지곤 합니다. 하지만 Janet은 설득력 있는 반론을 제시합니다. 학술적 순수성이 아닌, 실용적이고 현실적인 유용성을 위해 설계된 현대적인 Lisp 방언입니다.

Janet은 "재미있는 사이드 프로젝트"와 명령줄 도구를 위한 언어로 자리매김하며, 동적 언어의 유연성과 컴파일러의 강력함을 결합합니다. Lisp의 "고대 관습"을 걷어내고 작고 배우기 쉬운 핵심 기능을 제공함으로써, 전통적인 Lisp의 가파른 학습 곡선 없이 메타프로그래밍의 힘을 누리고자 하는 이들에게 가교 역할을 합니다.

현대적 의미론을 갖춘 미니멀리스트 핵심

Janet의 핵심은 일급 함수, 렉시컬 블록 스코핑, 그리고 식별자를 위한 단일 네임스페이스를 특징으로 하는 명령형 언어입니다. 핵심 언어는 놀라울 정도로 가볍며, do, def, var, set, if, while, break, 그리고 fn이라는 단 8개의 주요 명령어로 구성됩니다.

JavaScript 배경을 가진 개발자라면 런타임 의미론이 친숙하게 느껴질 것입니다. 값 타입(value types)을 가지면서도, 언어를 괴롭히는 특유의 "wats"가 없는 JavaScript라고 생각하면 됩니다. 이러한 단순함은 표준 라이브러리에도 반영되어 있어, 단 한 페이지로 볼 수 있을 만큼 간결하며 개발자가 몇 시간 내에 생산성을 높일 수 있도록 돕습니다.

배포 및 임베디드 가능성

Janet의 가장 실용적인 장점 중 하나는 배포 모델입니다. 최종 사용자에게 무거운 런타임이나 복잡한 환경 설정이 필요한 많은 고수준 언어와 달리, Janet은 런타임을 정적으로 링크하는 네이티브 실행 파일로 컴파일할 수 있습니다.

과정은 우아합니다. Janet은 바이트코드로 컴파일되고, 해당 바이트코드를 Janet 런타임을 초기화하는 .c 파일에 작성한 뒤, 시스템의 C 컴파일러를 사용하여 바이너리를 생성합니다. 그 결과 가비지 컬렉터와 바이트코드 컴파일러를 포함하면서도 종종 1MB 미만인 가벼운 실행 파일이 만들어집니다. 이는 휴대 가능한 명령줄 애플리케이션을 만드는 데 이상적인 선택입니다.

독립형 바이너리 외에도, Janet은 임베디드되도록 설계되었습니다. Lua와 유사하게, Janet 런타임은 작은 C 라이브러리입니다. 이를 통해 개발자는 더 큰 C 애플리케이션 내에 스크립팅 인터페이스를 노출하거나, 심지어 웹사이트에 언어를 임베디드하여 프로그래밍 가능한 DSL을 만들 수 있습니다.

강력한 텍스트 처리: Regex 대신 PEG

대부분의 언어가 텍스트 처리를 위해 정규 표현식(Regex)에 의존하는 반면, Janet은 파싱 표현 문법(Parsing Expression Grammars, PEGs)을 활용합니다. PEG는 전통적인 regex에 비해 몇 가지 장점을 제공합니다:

  • 예측 가능성: 일반적으로 더 구조화되어 있고 조합 가능합니다.
  • 다재다능함: 라인 지향적이지 않으므로, 멀티라인 텍스트와 비정규 언어(HTML 또는 JSON 등)를 쉽게 처리합니다.
  • 이진 데이터 지원: PEG는 널 바이트(null bytes)에 걸려 넘어지지 않고 이진 파일 형식을 파싱할 수 있습니다.

하지만 이 선택이 논란이 없는 것은 아닙니다. 일부 개발자는 PEG의 장점이 과장되었다고 주장하며, PEG의 대안(alternatives)이 교환 법칙이 성립하지 않기 때문에 디버깅이 표준 정규 표현식보다 때때로 더 좌절스러울 수 있다고 지기합니다.

메타프로그래밍의 힘

더 깊이 파고들고자 하는 이들을 위해, Janet의 진정한 힘은 매크로에 있습니다. 매크로는 개발자가 다른 코드를 생성하는 코드를 작성할 수 있게 하여, 언어의 구문법과 기능을 효과적으로 확장할 수 있게 합니다.

Janet은 흔히 발생하는 Lisp의 함정인 "위생성(hygiene)" 문제를 간단하고 우아한 해결책을 통해 처리합니다. 즉, 리터럴 함수를 언쿼팅(unquoting)할 수 있도록 허용하는 것입니다. 이를 통해 매크로가 참조 투명성을 유지할 수 있도록 보립니다.

아마도 가장 독특한 feature는 컴파일 타임에서 런타임으로 값을 전달하는 Janet의 능력입니다. 프로그램이 컴파일될 때, Janet은 모든 최상위 명령어를 실행하고 프로그램 상태의 전체 스냅샷을 찍습니다. 이 스냅샷은 공유 참조와 제너레이터 및 클로저의 상태를를 보존합니다. 이는 다음과 같은 강력한 최적화 기능을 가능하게 합니다:

  • 컴파일 타임에 복잡한 데이터(예: 게임을 위한 스플라인 보간법)를 미리 계산합니다.
  • 컴파일 단계에서 파일을 읽어 자산을 직접 바이너리에 임베디드합니다.
  • SQL 스키마를 기반으로 데이터베이스 바인딩을 자동 생성합니다.

실용적인 구문법과 설계

Janet은 접근성을 높이기 위해 Lisp의 "전통"을 의식적으로 피합니다. 고어(archaic)한 용어를 직관적인 용어로 대체합니다: CARfirst, PROGNdo, 그리고 LAMBDAfn이 됩니다. 또한 현대적인 편의 기능을 도입합니다:

  • 컬렉션 다양성: 가변(mutable) 및 불변(immutable) 컬렉션 모두를 지원합니다. 불변 컬렉션은 값 의미론을 사용하고, 가변 컬렉션(prefix @ 사용)은 참조 의미론을 사용합니다.
  • 단축 표기법: | 연산자는 표현식을 함수로 빠르게 승격시키기(lifting)를 가능하게 합니다 (예: |(+ 1 $)).
  • 유연한 문자열: 백틱(backtick)으로 쿼트된 문자열은 번거로운 이스케이프 시퀀스 없이도 어떤 문자도 포함할 수 있습니다.

커뮤니티 관점 및 대안

Janet에 대한 커뮤니티 논의는 생태계 내에서의 위치를 강조합니다. 일부 사용자들은 Fennel을 유사한 대안으로 지적합니다. Lua로 컴파일되는 Lisp인 Fennel은 Lua가 이미 임베디드된 환경에서 훌륭하지만, Janet은 Lua의 견고한 표준 라이브러리와 PEG 파서라는 강점을 가집니다.

다른 이들은 Janet을 Tcl과 비교하며, 둘 다 작고, 임베디드 가능하며, 도메인 특화 언어(DSL)를 만드는 데 탁월하다고 언급합니다. sh 라이브러리의 존재는 Janet의 유용성을 쉘 스크립팅 대안으로서 더욱 공고히 하며, 개발자가 언어 내에서 파이프와 리다이렉션을 직접 표현할 수 있게 합니다:

($ find . -name *.janet | say)

일부 비평가들이 Lisp 스타일의 괄호 구문법이 읽고 편집하기에 "끔찍하다"고 말하지만, 지지자들 사이의 합의는 그 결과로 얻어지는 언어의 힘, 통일성, 그리고 단순함이 그 대가로 치를러야 할 트레이드오프를 충분히 가 가치 있게 만든다는 것입니다.

Sources