왜 Janet인가? (2023)

1 week ago 10
  • Janet은 작은 Lisp 방언이지만 명령형 언어·일급 함수·단일 식별자 네임스페이스·어휘적 블록 스코프를 갖춰 단순하게 시작할 수 있음
  • 핵심 언어는 do, def, var, set, if, while, break, fn 8개 명령만으로 구성되고, 매크로가 더 강력하거나 편리한 제어 흐름 래퍼를 만들어 줌
  • 배포성은 Janet 프로그램을 Janet 런타임과 정적으로 링크한 네이티브 실행 파일로 컴파일해, 사용자에게 Janet이나 의존성 설치를 요구하지 않는 방식으로 확보함
  • 파싱 표현 문법(PEG) 은 정규식보다 단순하고 강력하며 예측 가능하고, sh는 파이프와 리다이렉트를 Janet 안에서 표현하게 해 CLI 작성 범위를 넓힘
  • 컴파일 시점 실행은 톱레벨 명령을 먼저 실행한 뒤 프로그램 상태 스냅샷을 디스크에 저장해, 매크로 없이도 런타임으로 값·공유 참조·제너레이터·클로저 상태를 넘길 수 있게 함

단순한 핵심

  • Janet은 명령형 언어이며 일급 함수, 단일 식별자 네임스페이스, 어휘적 블록 스코프를 가짐
  • 언어 핵심은 do, def, var, set, if, while, break, fn 8개 명령으로 작게 유지됨
  • 매크로는 더 강력하거나 편리한 고수준 제어 흐름 래퍼를 가능하게 함
  • 런타임 의미론은 익숙하고, 표준 라이브러리 전체가 한 페이지에 들어갈 만큼 언어의 나머지 부분도 작음

네이티브 배포와 임베딩

  • Janet 프로그램은 Janet 런타임을 정적으로 링크한 네이티브 실행 파일로 쉽게 컴파일할 수 있음
  • 배포받는 사용자는 Janet, 프로젝트 의존성, 그 밖의 별도 구성요소를 설치할 필요가 없음
  • Janet은 자신을 바이트코드로 컴파일한 뒤, Janet 런타임을 시작하는 .c 파일 안에 그 바이트코드를 기록하고, 시스템 C 컴파일러로 해당 C 파일을 컴파일함
  • 간단한 “hello world” 네이티브 바이너리는 1MB보다 작고, Janet 1.27.0의 aarch64 macOS 기준 크기는 784K였음
  • 이 바이너리에는 전체 Janet 런타임, 가비지 컬렉터, 바이트코드 컴파일러까지 들어가므로 런타임에 Janet 코드를 평가하는 프로그램도 만들 수 있음
  • Janet 런타임은 작은 C 라이브러리라 링크한 뒤 일반 C 함수를 호출해 Janet 값을 조작할 수 있음
  • 웹사이트에도 임베드할 수 있고, Toodle처럼 사용자 정의 프로그래머블 DSL을 가진 정적 사이트를 만들 수 있음

텍스트 파싱과 서브프로세스 DSL

  • Janet의 텍스트 처리는 정규식 대신 파싱 표현 문법을 기반으로 함
  • 파싱 표현 문법은 정규식보다 단순하고 강력하며 예측 가능하고, 줄 단위에 묶이지 않아 여러 줄 텍스트를 파싱할 수 있음
  • HTML, JSON, 그 밖의 비정규 언어를 파싱할 수 있고, 임의의 null 바이트가 있는 바이너리 파일 형식도 다룰 수 있음
  • sh는 파이프와 리다이렉트를 Janet 코드 안에서 직접 표현하게 하는 서드파티 셸 스크립팅 DSL임
($ find . -name *.janet | say)
  • 이 DSL은 Janet을 Perl의 합리적 대안에서 꽤 넓은 범위의 프로그램에 대해 Bash의 합리적 대안으로 끌어올림

컬렉션과 문법 감각

  • Janet 컬렉션 타입은 가변과 불변 형태를 모두 가짐
  • 불변 컬렉션은 값 의미론을 가지므로, 불변 벡터 [1 2]는 메모리 주소가 달라도 (take 2 [1 2 3])와 구분되지 않음
  • 가변 컬렉션은 참조 의미론을 가지므로, 해시 테이블 @{:x 1 :y 2}는 자기 자신과만 같고 같은 키와 값을 가진 다른 해시 테이블은 별도 객체임
  • 문법은 괄호를 널리 쓰지만 리스트에는 [], 테이블에는 {}를 사용해 형태를 나눔
  • 가변 리터럴은 @"mutable string"처럼 항상 @ 접두사를 붙임
  • 익명 함수는 (fn [x] (+ 1 x))로 쓰고, |(+ 1 $)처럼 |로 표현식을 함수로 들어 올리는 축약 표기도 제공함
  • 스플랫 또는 스프레드는 ;를 사용해 (+ ;args)처럼 표현함
  • 백틱 문자열은 원하는 개수의 백틱으로 열고 같은 개수의 백틱으로 닫을 수 있으며, 백틱 문자열 안에서는 \n 같은 이스케이프 시퀀스가 적용되지 않음
  • 나머지 매개변수는 . 대신 &를 써서 (defn foo [first & rest] ...)처럼 작성함
  • Janet은 reader macro를 지원하지 않아 문법 자체가 고정되고, Janet을 읽을 줄 알면 모든 Janet 프로그램을 읽을 수 있음

매크로와 컴파일 시점 상태

  • Janet 매크로는 코드를 작성하는 코드이며, 컴파일 시점에 값과 추상 구문 트리를 조작하는 현재 실행 흐름과 미래에 실행될 애플리케이션 코드 흐름을 동시에 다루게 함
  • Janet 매크로는 위생적(hygienic)이지 않고, 함수용 별도 네임스페이스도 없음
  • 하지만 리터럴 함수를 unquote할 수 있어 완전히 참조 투명한 매크로를 작성할 수 있음
  • Janet 프로그램을 컴파일하면 톱레벨 명령, 일반 문장, 함수 선언 등을 먼저 실행한 뒤 프로그램 상태의 스냅샷을 디스크에 기록함
  • 이 스냅샷은 공유 참조를 보존하므로, 다시 시작한 뒤에도 가변 값을 계속 변경할 수 있음
  • 제너레이터는 다음 재개 시 실행할 명령을 기억하고, 클로저도 닫힌 값을 유지함
  • 매크로는 컴파일 시점 코드 실행의 특수한 형태지만, 매크로 없이도 이 능력을 사용할 수 있음
  • 게임에서는 스플라인을 미리 처리할 수 있고, 컴파일 시점에 파일을 읽어 최종 바이너리에 자산을 넣을 수 있으며, 임의의 부수 효과도 수행할 수 있음
  • Janet for Mortals는 SQL 스키마 파일을 기반으로 데이터베이스 바인딩을 자동 생성하는 예를 보여주며, 이런 작업은 대부분의 언어에서 꽤 어렵다고 평가함

Lisp 전통보다 편안함

  • Janet은 오래된 Lisp 관습을 그대로 따르지 않음
  • CAR는 first, PROGN은 do, LAMBDA는 fn, SETQ는 def로 이름 붙음
  • nil은 빈 리스트가 아니라 독립 타입이고, 불리언은 일급 값임
  • EQ, EQL, EQUAL, EQUALP 계열을 피하고, 연결 리스트도 거의 보이지 않음
Read Entire Article