헬: 하스켈 방언 기반의 셸 스크립팅 언어

박준규 @curry@hackers.pub
- 저자: Chris Done
- 원문: https://chrisdone.com/posts/hell/
헬은 제가 개인적인 셸 스크립팅 용도로 만든, 하스켈의 작은 방언 형태의 셸 스크립팅 언어입니다. 2월부터는 Hakyll 대신 헬을 사용해 이 블로그를 생성하고 있습니다.[1]
업데이트: 2024년 10월 3일 기준으로, 저는 업무에서 테라폼과 여러 API와 함께 약 2천 줄 규모의 다양한 대형 스크립트에 헬을 사용하고 있습니다.
#!/usr/bin/env hell
main = do
Text.putStrLn "Please enter your name and hit ENTER:"
name <- Text.getLine
Text.putStrLn "Thanks, your name is: "
Text.putStrLn name
제 2024년 새해 결심은 자동화를 위해 더 많은 셸 스크립트를 작성하는 것입니다. 지금까지는 bash의 단점 때문에 이런 작업을 피해왔습니다. 그리고 그 밖의 문제들도 있습니다.
bash, zsh, fish 등은 다음과 같은 문제를 가지고 있습니다.
- 이해할 수 없는 난해한 문법입니다.
- 인용문 사용(
x=$(ls -1)
등)으로 인해 실수하기 쉽습니다. - 기본적인 작업조차 서브 프로세스에 지나치게 의존합니다.
- 따라서 비교, 산술, 순서 지정 등과 같은 것들은 전혀 원칙적이지 않으며, 함정이 가득합니다.[2][3]
하지만 bash에도 장점이 있습니다. 안정적이고, 단순하며, 모든 환경에서 동일하게 동작합니다. bash 스크립트를 작성하면 수년간 코드를 한 번도 수정하지 않고 계속 실행할 수 있습니다. 지난해 작성한 코드가 내년에도 그대로 동작한다는 점은 대부분의 인기 있는 프로그래밍 언어에서는 해당되지 않습니다. 하스켈을 한번 보세요.
그러므로 제가 실제로 사용하고 싶은 언어를 정의하기 위해, 셸 스크립팅 언어의 구조를 논의해보겠습니다.
- 매우 기본적이어야 합니다.
- 즉시 실행되어야 합니다.(눈에 보이는 컴파일 과정이 없어야 함)
- 모듈 시스템이 없어야 합니다.
- 패키지 시스템이 없어야 합니다.[4]
- 추상화 기능(클래스, 복잡한 데이터 타입, 다형 함수 등)이 없어야 합니다.
- 이전 버전과 호환되지 않게 변경되어서는 안 됩니다.[5]
왜 모듈이나 패키지 시스템이 없어야 할까요? 그런 시스템은 프로그램을 “완성” 상태로 유지하기 어렵게 만듭니다. 항상 다른 통합을 할 수 있고, 추가할 기능이 남아 있기 마련입니다. 저는 헬이 냉정하게 완결된 소프트웨어이길 원합니다. 완성된 소프트웨어에는 아름다움이 있습니다.
위 내용을 바탕으로, 저는 스크립팅 한계를 정의할 수 있습니다. 즉, 모듈 시스템이나 패키지 시스템, 추상화 기능이 필요해지거나, 표준 라이브러리 이상의 기능이 필요해질 때, 그때는 아마 일반 목적의 프로그래밍 언어를 사용하는 것이 적절합니다.
이를 고려하여, 저는 하스켈 방언[6]을 만들기로 선택했습니다. 그 이유는 다음과 같습니다.
- 하스켈을 잘 알고 있습니다.
- 제가 가장 자주 사용하는 언어입니다.
- 비교, 순서 지정 등과 관련해 탄탄한 개념을 가지고 있습니다.
- 동시성을 손쉽게 처리할 수 있는 좋은 런타임을 가지고 있습니다.
- 가비지 컬렉션이 있습니다.
- 바이트와 텍스트를 적절히 구분합니다.
- 정적 Linux x86 바이너리로 컴파일할 수 있습니다.
- 성능이 좋습니다.
- 정적 타입을 지원합니다!
언어를 설계하면서 저는 다음과 같은 결정을 내렸습니다.
- 충실한 하스켈 문법 파서를 사용합니다.
- 이렇게 하는 것이 더 낫습니다. 직관과 코드를 재사용할 수 있습니다.
- 임포트, 모듈, 패키지는 지원하지 않습니다.
- 재귀 정의는 지원하지 않지만,
fix
를 사용하면 가능합니다. - 기본적인 타입 클래스만 지원합니다.(
Eq
,Ord
,Show
,Monad
) 이는 예를 들어List.lookup
이나 일반적인 비교 연산 등에 필요합니다. - 다형 타입은 지원하지 않습니다. 이것은 일종의 추상화이며, 필요하지 않습니다.
- 하스켈에서 이미 사용되는 이름(
List.lookup
,Monad.forM
,Async.race
등)을 그대로 사용합니다. 이를 통해 기존 하스켈에 대한 직관을 재사용할 수 있습니다.
릴리스 페이지에서 정적 링크된 리눅스 바이너리를 다운로드할 수 있습니다. 구현 내부에 대해 알아보려면, 제가 업무에서 헬을 소개할 때 만든 헬 소개 슬라이드를 참고하세요.