2020년의 하스켈에 대한 내 생각

박준규 @curry@hackers.pub
이 글은 저자의 허락을 받아 ChatGPT를 통해 한글로 옮긴 것입니다.
- 저자: Marco Sampellegrini
- 링크: https://marcosampellegrini.com/thoughts-on-haskell-2020
2020년의 하스켈에 대한 내 생각
나는 2019년 10월에 Stick to Simple Haskell이라는 제목으로 발표를 했다. 그 발표를 준비하고 작성하는 데 많은 노력을 들였다. 발표 후 적지 않은 사람들이 나에게 다가와서 정말 좋았다고 말했다. 그들은 흔히 예상되는 사람들, 즉 타입 수준 프로그래밍을 결코 포기하지 않는 사람들이 아니었다. 오히려 나처럼, 그리고 많은 다른 사람들처럼, 단지 일상적인 개발 일이 조금이라도 덜 힘들었으면 좋겠다고 바라는 평범한 소프트웨어 개발자들이었다.
나는 그 발표에서 이야기한 몇 가지 생각들이 더 널리 퍼질 가치가 있다고 생각한다. 아니면 최소한 더 많이 논의될 필요가 있다고 본다. Boring Haskell Manifesto 같은 움직임은 정말 신선한 바람이다. 우리는 그런 것들이 더 많이 필요하다.
이 글은 내가 할 수 있는 작은 기여다.
30살
2020년에 하스켈은 30주년을 맞이한다. 처음부터 하스켈 언어 설계자들은 하스켈이 다음과 같은 목적으로 사용되기를 바랐다.
- 함수형 프로그래밍 교육
- 프로그래밍 언어 연구에서의 혁신과 발전
- 실제 애플리케이션 및 대규모 시스템 구축
이 중 교육은 잠시 제쳐두고 나머지 두 가지에 집중해 보자.
프로그래밍 언어 연구자들은 하스켈을 사랑한다. 왜냐하면 GHC를 확장(Extension)을 통해 쉽게 확장할 수 있기 때문이다. GHC는 새로운 아이디어를 시험해 볼 수 있는 완벽한 놀이터다. 요즘은 파일을 열면 확장 기능 목록이 벽처럼 쌓여 있는 것을 보는 것이 꽤 흔한 일이다.
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
이러한 확장들은 언어와 타입 시스템을 근본적으로 변화시킨다. 그 결과 훨씬 더 복잡하면서도 강력한 무언가를 다루게 된다. 당신은 점점 더 의존 타입(Dependent Types)에 가까워진다. 더 많은 정보를 타입에 담을 수 있게 되므로, 컴파일 타임에 더 많은 보장을 얻는다. 우리는 보통 이러한 것을 “화려한 타입(Fancy types)”이라고 부른다.
그런데도 불구하고, “하스켈이라는 언어 자체”는 1998년 이후로 크게 변화하지 않았다. 아주 대략적이고 부정확하게 표현하자면, 확장 기능을 전혀 활성화하지 않고 GHC를 실행하면 Haskell98이라는 것을 얻게 된다. 이 경우에는 단순한 데이터 타입, 타입 클래스, 게으르고 순수한 함수형 코어를 사용하게 된다.
하스켈로 애플리케이션 만들기
프로그래밍 언어 연구자들은 화려한 타입(Fancy types)에 올인한다. 하지만 나는 PL 연구자가 아니다. 나는 소프트웨어 개발자다. 앞서 말했듯이, 하스켈 설계자들의 목표 중 하나는 산업 현장에서, 실제 애플리케이션을 개발하는 데 하스켈을 사용하게 하는 것이었다. 그럼 화려한 타입으로 무장한 하스켈이 실제 서비스 개발에 적합한가?
종이 위에서는 좋아 보인다
실제 서비스 개발은 논문을 쓰는 것과는 전혀 다르다.
연구 논문은 일단 발표되고 나면 더 이상 유지보수를 할 필요가 없다. 논문에 등장하는 코드 샘플에서 보여주는 기법과 트릭들은 PDF 상으로는 멋지게 보이지만, 실제 코드베이스에 어떤 영향을 미칠지는 전혀 다른 문제다. 연구자들은 실험적인 기능이나 검증되지 않은 확장 기능에 의존할 수도 있다. 느린 컴파일 시간도 별 문제가 되지 않는다.
포용성(Inclusivity)
서비스 개발은 팀워크다.
나는 다양한 배경의 사람들과 함께 일하고 싶다. 하스켈을 다루기 위해 박사학위가 필수 조건이 되는 팀에서 일하고 싶지는 않다. 누구나 접근 가능한 코드베이스를 원한다. 대부분의 비즈니스 로직은 로켓 과학이 아니다. 애플리케이션을 필요 이상으로 복잡하게 만들 이유가 없다.
좀 더 실용적인 관점에서 보면, 하스켈에 투자하는 회사 입장에서 진입 장벽을 낮추면 더 많은 인재 풀이 열린다.
미미한 이득(Marginal Benefits)
화려한 타입은 더 많은 보장을 주고, 더 안전하고 정확한 코드를 작성할 수 있게 해준다. 그렇다면 왜 그런 걸 포기하려는가?
나는 그 이득이 그렇게 극적이지 않다고 생각한다. 일반적인 프로그래밍 언어에서 하스켈로 넘어오는 것 자체가 이미 큰 점프다. 컴파일러가 올바른 코드를 작성하도록 도와주는 것만으로도 충분히 큰 이득이 있다. 물론, 화려한 타입을 사용하면 그보다 조금 더 정확할 수도 있다. x%만큼 더 자신감을 가질 수도 있다. 하지만 그 복잡성이 과연 그만한 가치가 있는가?
현실적인 관점에서 보면, 우리는 타입 시스템만이 전부가 아니라는 사실을 인식해야 한다. 타입은 정말 강력한 도구지만, 우리가 사용할 수 있는 유일한 도구는 아니다. 타입이 있다고 해서 테스트를 안 해도 되는 것은 아니다.
심플 하스켈(Simple Haskell)
나는 1998년 이후에 일어난 모든 일을 무시하자는 것이 아니다.
Haskell98은 좋은 기반이다. 하지만 개선할 수 있다. 핵심 아이디어는 타입 시스템을 있는 그대로 받아들이고, 사용자 편의성(ergonomics)에 집중하는 것이다. 타입 시스템 자체를 복잡하게 만드는 것이 아니라, 언어를 더 다루기 쉽게 만드는 것이다.
이것은 결국 제네릭스(Generics)를 활용하고, 사용하기 편한 확장 기능들을 적절히 활성화하는 것으로 요약할 수 있다.
제네릭스는 Haskell98에는 없다. 제네릭스는 보일러플레이트 코드를 줄이는 데 아주 훌륭하다. 예를 들어, generic-lens를 사용하면 JSON 인스턴스나 렌즈(lens)를 자동으로 파생(derive)할 수 있다.
몇몇 확장 기능은 해가 되지 않고, 오히려 하스켈을 훨씬 더 다루기 편하게 만들어 준다.
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications #-}
애플리케이션 아키텍처
대부분의 애플리케이션은 다음과 같이 작성하면 된다.
app :: Env -> IO ()
여기서 Env
는 의존성 주입 컨테이너(dependency injection container)를 나타낸다.
data Env = Env
{ usersCache :: TVar [User]
, postgresConnection :: PG.Connection
, log :: Severity -> Text -> IO ()
, fetchUser :: UserId -> IO (Maybe User)
, storeFile :: Filename -> ByteString -> IO (Either Text ())
}
내가 어떤 것이 Env
에 포함되어야 하는지 판단하는 기준은 다음과 같다.
- 그 자원이 애플리케이션 전반에서 공유되는 리소스인가?(예: Postgres 연결)
- 다른 구현체를 제공할 필요가 있는가?(예: 테스트 시 모킹(mock) 구현)
만약 타입이 너무 커지면, 다음처럼 더 세분화할 수 있다.
data UserService
= UserService
{ fetchUser :: UserId -> IO (Maybe User)
, updateUser :: User -> IO (Either UserServiceError ())
, deleteUser :: UserId -> IO (Either UserServiceError ())
}
data Env = Env
{ userService :: UserService
, ...
}
애플리케이션 규모가 커지면, ReaderT
디자인 패턴을 사용하여 리팩터링 할 수 있다.
2020년의 하스켈
하스켈은 프로그래밍 언어 연구자들과 소프트웨어 개발자라는 서로 다른 두 집단을 모두 만족시키려 노력했음에도 불구하고 성공을 거두었다. 30년이 지난 지금, 두 집단 모두 여전히 하스켈을 사용하고 있다. 하지만 이들의 요구는 더 이상 달라질 수 없을 만큼 다르다.
소프트웨어 개발자로서 우리는 애플리케이션 작성에 유용한 하스켈 기능이 무엇인지 이해해야 한다. 더 중요한 것은, 해롭거나 비효과적인 기능이 무엇인지도 알아야 한다는 점이다. PL 연구자들과 소프트웨어 개발자들은 같은 도구를 공유하지만, 그렇다고 해서 화려한 타입(Fancy types)을 무턱대고 받아들여야 할 이유는 없다.
하스켈 개발자로서 우리는 모든 문제가 논문 주제가 될 필요는 없다는 것을 깨달아야 한다. 선택은 다음과 같다.
- 타입 레벨에서 아름다운 해결책을 찾기 위해 4시간 동안 퀘스트에 도전할 것인가,
- 아니면 10분만 투자해 지루하지만 확실한 방법으로 해결하고, 어쩌면 테스트도 작성할 것인가?
포용성(Inclusivity)을 위해 약간의 타입 안정성을 희생하는 것도 괜찮다. 테스트를 통해 소프트웨어가 올바르게 동작함을 충분히 확인할 수 있기 때문이다. 이것이 바로 심플 하스켈(Simple Haskell)의 핵심이다. 지루한 하스켈(Boring Haskell)이라는 표현이 오히려 더 적절할 수도 있다.
심플하고 지루한 하스켈을 쓰는 것은 즐거운 일이다. 구체적인 코드가 주는 해방감이 크다. 추상화도 좋지만, 지루한 하스켈이 더 좋다.
초보자가 다음에 당신에게 질문한다면
- Servant를 추천하지 마라. Scotty도 훌륭히 작동한다. 타입 안전 라우팅은 나중에 해도 된다.
- 어떤 이펙트 시스템도 추천하지 마라. 거대한 추상화는 나중에 해도 된다.
- Elm이나 Go를 깎아내리지 마라. 아무 성과도 얻지 못한다.
- Nix를 추천하지 마라. Stack도 충분히 잘 돌아간다. 네가
nix-build
를 원하는 대로 작동시키느라 몇 시간을 썼건 간에. - 그들이 박사학위가 없더라도 계속 대화하라.
2020년에는 아득한 탑(Ivory tower)을 내려와서 지루한 하스켈을 쓰자.
뜻밖의 전개로, 이 글이 나온 지 몇 시간도 지나지 않아 Matt Parsons가 주니어 코드(Junior Code)를 장려하는 아주 좋은 글을 발표했다. 이 글이 마음에 들었다면 꼭 읽어보길 추천한다. 만약 이 글이 마음에 들지 않았다면, 더더욱 필독이다.