요즘의 Haskell 라이브러리 생태계는 매우 풍부하며, 문제 유형별로 특히 추천할 만한 우아하고 종종 간과되는 라이브러리들을 정리한 목록이다.
|
Last updated: April 9, 2025
요즘의 Haskell 라이브러리 생태계는 믿기 어려울 정도로 풍부하고, 좋은 라이브러리들 중 상당수가 간과된다고 생각한다. 아래는 내가 좋아하는 것들의 목록이다. 어떤 것들은 특정 문제 부류(예: 스트리밍, 이펙트 시스템)에 대해 내가 가장 선호하는 해법이기 때문이고, 어떤 것들은 우아한데도 덜 알려져 있기 때문이다.
라이브러리를 작성할 때는 나는 대안 prelude를 절대 사용하지 않는다. 대안 prelude는 거의 정의상 새 기능을 제공하기보다는 재구성하는 것이기 때문에, 추가 의존성을 들일 때의 위험/보상 균형이 맞지 않는다. 라이브러리가 추가하는 모든 의존성은 모든 다운스트림 사용자에게 강제로 전파되며, 그 유지보수자에게 무슨 일이 생기면 잠재적 병목이 된다.
애플리케이션의 경우에도 나는 대체로 base를 선호한다. 대안 prelude 중에서는 relude가 가장 낫다고 생각한다. 이것은 자기 자신이 추가적인 non-boot 의존성을 끌어오지 않으며, 지루한 일들을 조금 더 편하게 만들어주는 것들을 꽤 많이 제공한다. 예를 들면:
Map과 HashMap 같은 흔한 타입 이름들을 import해 줌;viaNonEmpty :: (NonEmpty a -> b) -> [a] -> Maybe b 같은 non-empty 데이터 타입용 헬퍼;(<<$>>) = fmap . fmap; 그리고usingReaderT = flip runReaderTmixins를 만지작거리라는 권고에는 동의하지 않는다. 차라리 default-extensions: NoImplicitPrelude를 설정하고, 각 파일에서 명시적으로 import Relude를 해서 명확함을 유지하는 편이 낫다.
placeholder는 잘 설계된 TODO 라이브러리로, 나중에 돌아와 채워 넣을 수 있도록 프로그래머에게 경고를 발생시키는 “플레이스홀더”를 제공한다. relude가 제공하는 todo와 달리, 유용할 수 있는 pattern synonym도 제공한다.
safe-wild-cards는 Template Haskell을 사용해 RecordWildCards 스타일 패턴 매치의 대안을 제공하는데, 어떤 바인딩이든 사용되지 않으면 경고를 생성한다.
semialign은 align :: Semialign f => f a -> f b -> f (These a b) 함수를 제공하는데, 여기서 These a b는 “a 또는 b 또는 둘 다”를 나타내는 데이터 타입이다. 이는 많은 곳에서 유용하다: 예를 들어 리스트를 zip하되 더 긴 리스트의 꼬리도 유지하기, Map들을 함께 zip하기, 우연히 동시에 발생한 FRP Event를 확인하기, 등.
witherable은 리스트의 mapMaybe를 훨씬 큰 범주의 데이터 구조로 일반화한다.
finitary는 “class Enum을 제대로 한 것”이다. 이는 타입 레벨에서 a의 기수(cardinality)를 드러내는 Finitary a 타입클래스를 제공하고, a와 Finite (Cardinality a) 사이의 전단사(bijection)를 증명하며, 타입 a에 대해 안전한 next/previous/열거 함수를 제공한다.
finitary의 GPL-3.0-or-later 라이선스가 받아들이기 어렵거나, bounds가 최신이 아니거나, 무한히 많은 원소를 가진 데이터 타입을 열거해야 한다면, universe가 가볍고 유용한 대안이 될 수 있다.
hoist-error는 Maybe와 Either 값의 “실패 케이스”를 MonadError e m의 “에러 부분”으로 끌어올리는 작은 콤비네이터 라이브러리다. 이는 보통 함수의 “행복한 경로(happy path)”를 훨씬 명확하게 만들고, “지루한 case 매칭”을 줄이며, >>= either (throwError . MyError) pure 같은 잡음을 감소시킨다.
Either와 동형(isomorphic)이지만 Monad 인스턴스를 포기하고 “에러를 누적하는” Applicative 인스턴스를 제공하는 Validation 타입을 제공하는 라이브러리들이 몇 가지 있다. 나는 문서가 훌륭하고 Semigroup 인스턴스가 좋은 validation-selective를 추천한다. instance (Semigroup e, Semigroup a) => Semigroup (Validation e a)는 (<>)로 실패는 실패끼리, 성공은 성공끼리 결합한다.
에러를 누적하되 Monad 인스턴스도 필요하다면 monad-chronicle을 고려해 보라. 다만 주의할 점이 있다:
applicative로 사용할 때 ChronicleT는 Validation applicative보다 누적하는 에러가 더 적다. 이는 ChronicleT의 Applicative 인스턴스가 Monad 인스턴스와 일치해야 하기 때문이다.
ChronicleT는 순진한 WriterT와 비슷한 지연성(laziness) 관련 우려를 많이 공유하며, 첫 번째 타입 파라미터(“에러” 파라미터) 쪽에 큰 thunk를 쌓아 올릴 수 있다.
formatting은 텍스트 포매팅을 위한 표현력 있는 DSL을 제공한다. Text.Printf와 달리 formatting은 타입 안전하다.
safe-coloured-text는 크로스플랫폼 텍스트 컬러링 라이브러리다.
Haskell 프로그래머들은 정규식을 잘 쓰지 않는 경향이 있는데, 파서 콤비네이터가 대체로 더 읽기 쉽고 실수하기도 더 어렵기 때문이다. kqr의 표현을 빌리면, “다른 언어에서는 간단한 정규식으로 같은 일을 할 수 있다면 전체 파서를 작성하는 건 과하다고 여겨질 것이다. Haskell에서는 파서를 쓰는 게 대수롭지 않다. 우리는 그냥 쓰고, 삶을 계속 살아간다.”
regex-tdfa는 내 기본 선택이다. 순수 Haskell 구현(FFI 없음)이고, 성능 문제를 겪은 적이 없다. regex-base 타입클래스들이 제공하는 유연성은 처음엔 조금 위협적으로 느껴질 수 있지만, 예제를 참고해서 따라 하면 괜찮을 것이다.ref-tf는 가변 레퍼런스를 제공하는 어떤 모나드 위에서도 동작하는 함수를 작성할 수 있게 해 준다. 즉 IO에서도, ST s에서도 동작하는 인터페이스를 제공할 수 있다는 뜻이다.
StateVar는 임의의 I/O를 백엔드로 하는 “레퍼런스 같은” 값들을 위한 타입클래스를 제공한다. 이는 OpenGL 1.1 API처럼 본질적으로 상태적인 것들에 대한 인터페이스를 제공할 때 가끔 매우 유용하다.
algebraic-graphs는 Functional Pearl 위에 구축된, 극도로 우아한 그래프 라이브러리다.
search-algorithms는 BFS, DFS, Dijkstra, A* 같은 고전 알고리즘을 제공하는 즐거운 작은 패키지다. 그래프에 어떤 데이터 구조를 쓰는지는 신경 쓰지 않는데, 다음 상태 관계와 간선 비용 정보를 함수 파라미터로 받기 때문이다.
streaming은 내가 가장 좋아하는 스트리밍 라이브러리다. Monad m 위에서 a의 이펙트풀 스트림을 다음처럼 상상할 수 있을 것이다:data Stream a m r
= Step !a (Stream a m r)
| Effect (m (Stream a m r))
| Return r
streaming은 a를 임의의 functor로 일반화하며, 실제로는 다음 두 가지 주요 타입을 사용한다:
data Of a b = !a :> b deriving Functor
data Stream f m r
= Step !(f (Stream f m r))
| Effect (m (Stream f m r))
| Return r
대부분의 경우 Stream (Of a) m r로 작업하지만, functor 파라미터 덕분에 conduit이나 pipes에서는 작성하기 훨씬 어려워 보이는 것들을 가능하게 해 준다. 소스를 싱크에 직접 연결하는 경우라면, 나는 보통 내가 사용하는 다른 라이브러리들이 내부적으로 사용하는 스트리밍 라이브러리를 그대로 쓰는데, 이는 종종 conduit를 쓰게 된다는 뜻이다.
Copyright © 2026 Jack Kelly