Semantic가 왜 Haskell로 작성되었는지, 제어 흐름, 런타임 안정성, 연구·산업적 맥락, 그리고 팀의 경험을 통해 설명합니다.
Semantic는 Haskell로 작성되었고, 코드베이스를 처음 접하는 분들은 보통 두 가지 반응 중 하나를 보입니다: "완전 멋진데!" 또는 "왜 하스켈이죠?" 이 문서는 주로 후자의 질문에 답합니다.
Haskell은 표준화된, 범용의, 컴파일되는, 순수 함수형 프로그래밍 언어로 비엄격 의미론과 강한 정적 타입 시스템을 갖추고 있습니다[1]. 우리는 오래된 명성을 가진 Glasgow Haskell Compiler (GHC)와 빌드 시스템 cabal을 사용하며, 내부적으로 Stack도 사용해 왔습니다. 이 첫 문장만으로도 "순수 함수형", "비엄격 의미론", "강한 정적 타이핑" 같은 개념의 역사와 장점을 조사하는 데 여러 시간을 보낼 수 있을 만큼 밀도가 높습니다. 그 부분은 독자에게 맡기고, 대신 Semantic에서 우리가 크게 의존하는 이 언어의 몇 가지 흥미로운 측면에 집중하겠습니다.
— 더 구체적으로 말하면, 왜 Semantic는 Haskell로 작성되었을까요?
Semantic 프로젝트는 소스 코드를 파싱하고, 분석(평가)하고, 비교하는 일에 관한 것이므로, 우리는 확고히 프로그래밍 언어 이론(PLT)이라는 학술적 영역에 뿌리를 두고 있으며 GitHub의 소스 코드를 분석하는 현실 세계의 문제에 기존 연구를 적용하는 데 상당한 시간을 보냅니다. Haskell은 이 영역에 잘 맞습니다. 이 언어의 기능은 우리가 다루는 자료구조와 알고리즘을 간결하고, 정확하며, 우아하게 표현할 수 있게 해 줍니다. 우리는 각 프로그래밍 언어를 우리가 지원하는 구문 항(term)의 오픈 유니온으로 표현하는 방식으로, 프로그래밍 언어의 구문 항과 diff의 대수적 표현을 구성할 수 있었습니다.
Haskell에는 Semantic처럼 야심찬 프로젝트를 가능하게 하는 많은 측면이 있습니다. 강한 타이핑, 지연(느긋한) 평가, 순수성 등은 그중 일부에 불과합니다. 그러나 Haskell이 필수적인 또 다른 이유가 있습니다. 바로 풍부하고 사용자 정의 가능한 제어 흐름을 지원한다는 점입니다.
Go나 Java 같은 언어에서는 제어 흐름의 세부 사항이 언어 안에 내장되어 있습니다. 모든 ALGOL의 후손(파생 언어)처럼 프로그램 실행은 main()에서 시작해서, 함수와 메서드 호출을 거쳐 위에서 아래로 진행되다가 프로그램이 무한 루프에 들어가거나 종료됩니다.
Haskell은 다릅니다. Haskell에서는 제어 흐름이 언어에 의해 지시되지 않고, 사용되는 자료구조에 의해 결정됩니다. 같은 문법이 비결정적 및 백트래킹 계산, 동시성 및 병렬성, 그리고 전통적인 명령형 블록에도 사용됩니다. 내장 언어 의미론이 아니라 사용자 정의 해석 함수가 코드가 실행되는 방식을 결정합니다. 이는 추상화와 다형성 지원이 제한적인 Go 같은 언어에서는 구현이 거의 불가능하고, Java에서는 유지보수 악몽이 될 것입니다. 우리 코드 2만 줄 전체를 함수가 아닌 자료구조로 다시 작성해야 하기 때문이죠. 이는 다른 언어에서는 현실적인 작업이 아닙니다. 심지어 OCaml과 Swift 같은 함수형 언어조차도 이 수준의 추상화를 제공하진 못합니다.
그 예로 재개 가능한 예외(resumable exceptions)라는 개념이 있습니다. Semantic의 해석 패스 동안 잘못된 코드(미정의 변수, 타입 오류, 무한 재귀 등)는 패스의 호출 컨텍스트에 따라 인식되고 처리됩니다. Semantic의 본질이 인터프리터이기 때문에, 이 기능은 빠른 개발에 본질적으로 중요합니다. 우리는 해석 및 분석 패스를 구성할 때, 호출 지점을 특수화하는 것만으로 GHC가 신뢰할 수 없고 잠재적으로 잘못된 코드를 실행하는 동안 오류를 어떻게 처리해야 하는지 지정합니다. 이를 Java로 이식하려면 try/catch/finally 메커니즘을 엄청나게 남용해야 할 것입니다. Java에는 제어 흐름의 정책과 메커니즘을 분리할 방법이 없기 때문입니다. 그리고 Go에는 예외가 없으므로, 이런 기능은 아예 불가능합니다.
강한 타이핑의 미덕에 대해서는 많은 논의가 있어 왔습니다. 그 모든 장점을 완전히 탐구하는 일은 이 글의 범위를 넘어가지만, Semantic는 원칙적으로 런타임 크래시를 거의 겪지 않는다는 점은 언급할 가치가 있습니다. 널 포인터 예외, 메서드 누락 예외, 잘못된 캐스팅 등은 Haskell이 그러한 버그를 포함한 프로그램을 만들기 매우 어렵게 만들기 때문에 사실상 발생하지 않습니다. 물론 어떤 수준의 타입 안전성도 모든 프로그램의 정당성을 보장하진 못하지만, Semantic Code 팀이 대부분의 시간을 프로덕션 크래시를 디버깅하는 대신 기능 개발에 쓰고 있다는 사실은 놀랍습니다—그리고 이는 상당 부분 우리가 선택한 언어 덕분이라고 할 수 있습니다.
Semantic는 독특한 프로젝트이며 종종 현대 컴퓨터 과학 연구의 최전선에 서 있습니다. 따라서 우리가 쌓을 역사가 없습니다. 외부 영감의 원천과 추상화의 도약은 연구 및 학계 커뮤니티에서 옵니다. 한 가지만 예를 들자면, 우리는 아마도 Oleg Kiselyov의 확장 가능한 이펙트(extensible effects) 작업을 가장 크게 활용하고 있으며, 업계에서 Wu 등(Wu et al)의 고차 이펙트(higher-order effects) 개념을 처음 사용한 팀일 것입니다.
이 중대한 연구는 Java나 C++이 아니라 Haskell에서 이루어지고 있습니다. Haskell의 간결함, 강력함, 올바름에 대한 집중은 연구자들이 고급 연구를 기존 언어에 끼워 맞추는 고된 작업이 아니라 문제의 본질에 집중할 수 있게 해 줍니다. Haskell로 작성하면 읽고, 포팅하고, 버그를 고치는 순환에 갇히는 대신 다른 사람들의 작업 위에 구축할 수 있습니다.
GHC는 25년이 넘는 역사를 가진 견고한 기술입니다. 언어로서의 Haskell과 그 주요 구현체인 GHC 모두 언어의 미래를 이끄는 활동적이고 기능적인 위원회를 보유하고 있습니다. Haskell은 Facebook 등을 비롯해 업계에서 대규모로 사용되고 있습니다.
Haskell은 매일 작업하기에 즐거운 언어입니다. 생산적이면서도 시야를 넓혀 줍니다. 하지만 모든 것과 마찬가지로 약점도 있으니, 그에 대해서도 조금 이야기해 보겠습니다.
StrictData 언어 확장을 사용합니다.이 시점에서 우리는 이 프로젝트의 많은 목표(추상 해석, 그래프 분석, 이펙트 분석, 코드 작성, AST 매칭 등)를 가능하게 하는 Haskell의 언어 기능에 꽤 단단히 의존하고 있습니다. 다른 프로그래밍 언어로 Semantic를 구현할 수 있을까요? 물론입니다. 이 프로젝트의 시맨틱 diff 부분의 초기 프로토타입은 Swift로 작성되었지만, 곧 감당하기 어려워졌고 초기의 거친 Haskell 프로토타입조차도 훨씬 더 성능이 좋았습니다. Haskell을 채택한 이후로 우리는 GitHub의 나머지 인프라에 연동하는 데 아무런 문제도 없었습니다. 커맨드라인 도구, 웹 서버(HTTP/JSON)로 실행했고, 이제는 Twirp RPC 서버로도 실행합니다. 우리는 GitHub에서 Kubernetes와 Moda, 그리고 이제는 gRPC Twirp의 초기 도입자였으며, 종종 다른 팀들보다 훨씬 앞서 이러한 새로운 인프라 구성요소 위에서 애플리케이션을 제공했습니다. 자체 빌드 시스템을 관리했고, Docker 같은 새로운 기술을 빠르게 채택했으며, Enterprise 제품에도 배포하는 등 프로젝트의 짧은 생애 동안 정말 많은 일을 해냈습니다. 우리는 아직까지 언어 선택에 제약받은 적이 없습니다. 오히려 강한 정적 타입 시스템의 모든 이점을 유지하면서도, 서너 개(그리고 계속 늘어나는) 프로그래밍 언어의 구문과 평가 의미론을 추상화하고 표현하는 Semantic의 능력에 매일 놀라고 있습니다. 더 "인기 있는" 언어를 선택했다면, 우리는 아마 수십만 줄의 코드에 발이 묶여 기술 부채, 애플리케이션 성능, 더 많은 언어를 추가하는 부담에 대해 불평하고 있었을 것입니다. 현재 우리는 2만 줄의 Haskell 코드와 놀라운 프로그램 분석 능력을 손에 쥐고 있으며, 더 많은 언어를 추가하거나 GitHub의 변화하는 요구를 지원하는 일도 크게 두렵지 않습니다.