타입의 타입: Haskell의 카인드 시스템

Types of types: Haskell's Kind system

thunk.blog ▲ 99 댓글 73 Thom2503

요약

Haskell에서 타입을 분류하는 카인드를 알면 더 안전하고 표현력 있는 코드를 짤 수 있습니다.

블로그 'Thunks & Thoughts'를 쓰는 개발자 올라올루와 M의 글입니다. Haskell로 레디스 서버를 따라 만들며 설정 코드를 고친 경험을 예로 들어, 값에서 타입, 타입에서 카인드로 이어지는 분류 체계를 설명합니다.

왜 중요한가

  • 함수를 인자로 받는 고차 함수처럼, 타입 생성자를 인자로 받는 타입도 만들 수 있음을 보여 줍니다.
  • 추상적인 카인드 개념이 실제 코드에서 중복을 없애고 실수를 컴파일 단계에서 잡아 준 사례를 제시합니다.

핵심 내용

  • 값이 타입으로 묶이듯 타입은 카인드로 묶이며, Int의 카인드는 Type이고 Maybe의 카인드는 Type -> Type입니다.
  • 고차 카인드 타입(타입 생성자를 인자로 받는 타입)은 (Type -> Type) -> Type 같은 카인드를 가집니다.
  • 설정 레코드의 필드를 f로 감싸 두고, f에 Identity를 넣으면 완성된 설정, Last를 넣으면 일부만 채운 설정으로 씁니다.
  • Last를 합치는 규칙 덕분에 명령줄 옵션, 설정 파일, 기본값 순으로 우선순위가 저절로 정해집니다.
  • DataKinds 확장으로 ConfigField라는 카인드를 직접 만들어 설정 키 이름과 그 값의 타입을 한데 묶었습니다.

HN 반응

  • Haskell의 발상이 주류 언어로 퍼지길 바라는 목소리가 컸지만, 실무에 쓰기엔 생태계가 부족하다는 반론도 나왔습니다.
  • 자바 제네릭은 ArrayList 같은 타입 생성자를 인자로 받지 못해 카인드가 낯설다는 설명과 함께, 모나드가 어려운 이유도 여기 있다는 의견이 나왔습니다.

댓글

30개 표시 · 전체 73개
  1. adamddev1 HN

    하스켈의 아이디어가 주류 언어로 스며들수록 세상은 더 행복해질 거라고 봅니다. 이런 높은 수준의 추상화가 온갖 것을 하나로 엮는 데 이렇게까지 유용하고 강력해질 수 있다니, 정말 아름답습니다.

  2. thomasfromcdnjs HN

    저는 바이브 코딩으로 제 언어를 만들고 있는데, 컴파일러는 하스켈로 짜여 있어요. 에이전트들이 하스켈에서 영감을 얻으니 아주 잘 돌아가고, 저도 이런저런 언어 기능을 막 추가하는 재미에 빠져 있어요. 지금은 prophecy ghost variables를 구현하는 중이에요. 공개 프로젝트는 아니지만, 일단 http://lsalpha.com 에서 대강 살펴볼 수는 있어요.

  3. bigmadshoe HN

    정말 멋지네요! 주로 ML 분야를 노리신다면, 홈페이지에 피드 포워드 네트워크나 트랜스포머 블록, 손실 함수 같은 ML 특화 예제 구현을 분명하게 올려 두면 좋겠어요.

    추가: https://lsalpha.com/systems 에 있는 GitHub 링크도 제겐 404가 떠요. 그래서 실제 구현이 어떤 모습인지 보지 못한 채 사이트를 나왔어요.

  4. thomasfromcdnjs HN

    아주 좋은 제안이에요. 말씀이 맞으니 그대로 할게요!

  5. entropy47 HN

    안타깝게도 제가 겪은 대부분의 바이브 코딩 프로젝트가 딱 이렇습니다.

  6. fourthark HN

    아마 저장소가 비공개인 거겠죠...

  7. pjmlp HN

    저도 동의하지만, 에이전트가 나온 뒤로는 프로그래밍 언어를 갖고 노는 재미도 끝났다고 생각합니다. 에이전트가 원하는 동작을 스스로 하도록 몰아가려면 형식 체계(formalism)로 옮겨 가야 하니까요.

    물론 요즘 언어 런타임을 만드는 사람들이 에이전트 런타임을 만드는 쪽으로 넘어가듯이, 내부를 들여다볼 수 있는 사람들은 계속 있을 겁니다.

  8. crote HN

    하스켈은 훌륭한 연구용 언어입니다. 다만 실제 프로덕션 프로그래밍에 쓰려고만 하지 마세요. ;-)

    탁월한 아이디어와 악마같이 영리한 추상화가 많지만, 하스켈에서는 그런 것들이 미완성에 쓰기 불편한 채로 남는 경우가 많아요. 다른 언어로 옮겨진 뒤에야 비로소 무르익는 경우가 대부분이고요.

  9. gwerbin HN

    Idris 2의 가장 좋은 점 하나는 의존 타입이나 수량(quantities), 증명 탐색과는 아무 상관이 없습니다. 바로 표준 라이브러리인데, 하스켈 표준 라이브러리를 깔끔하게 정리한 것에 가깝습니다. 이름 충돌을 피하도록 제대로 된 오버로딩을 지원하고, 같은 타입에 대한 여러 인터페이스 구현 중에서 골라 쓸 수도 있습니다.

    간단한 예제는 https://github.com/stefan-hoeck/idris2-tutorial 에서 볼 수 있습니다.

  10. tome HN

    Haskell is a great research language - just don't try to use it for actual production programming. ;-) (하스켈은 훌륭한 연구용 언어입니다. 다만 실제 프로덕션 프로그래밍에 쓰려고만 하지 마세요. ;-)

    이런, 그 조언이 10년하고도 수백만 달러어치 가치를 만들어 낸 뒤에야 도착했네요. 저한텐 너무 늦었어요 :(

  11. crote HN

    더 프로그래머 친화적인 언어를 썼다면 수십억 달러어치 가치를 만들어 냈을 수도 있겠네요!

    농담은 접어 두고, 하스켈로 상업 프로젝트를 하는 게 불가능하진 않지만 제 생각엔 스스로 일을 괜히 어렵게 만드는 건 맞습니다. 특히 AI 이전 시대에는 더 그랬고요. 일주일에 쓸 수 있는 시간은 정해져 있는데, 그 시간을 비즈니스 로직 짜는 데 쓰고 싶으세요, 아니면 생태계의 빈틈을 메우려고 라이브러리 만드는 데 쓰고 싶으세요?

    루비를 예로 들어 볼게요. 대부분의 기준으로 썩 훌륭한 언어는 아니고, 언어 자체가 최선의 선택인 애플리케이션은 아주 드물 거라고 봅니다. 하지만 Ruby on Rails를 얹으면 얘기가 달라집니다. 갑자기 독자적인 철학이 있고 필요한 건 다 들어 있는 웹 앱 스택이 생기고, 시간의 99%를 중요한 비즈니스 로직 작성에만 쓸 수 있어요. 아직 덜 여문 Servant의 타입 DSL이 기본적인 CRUD 하나 쓰게 해 주도록 며칠씩 씨름할 필요 없이 MVP에만 집중하면 됩니다.

    이메일을 보내고 싶다고요? 그쪽 REST API를 연동하는 하스켈 코드를 직접 짜든가, 아니면 그쪽 공식 C# SDK를 쓰든가 해야죠. 의존성이 열두 개면 이걸 열두 번 반복하는 겁니다.

    게다가 이건 개발자 중 일부가 알고 보니 생각했던 것만큼 20배속 록스타가 아니고, 그들이 만든 영리한 고수준 추상화 중 일부가 심각하게 망가져서 득보다 해가 더 큰 상황이 벌어지기 전의 얘기예요...

    저한테 하스켈은 '놓친 물고기'예요. 핵심 언어는 정말 좋아하고 정말 모든 데 쓰고 싶은데, 생태계와 문화 때문에 그게 불가능해요.

  12. arunix HN

    그런 예를 좀 들어 주실 수 있나요?

  13. i2talics HN

    저는 모나드가 하스켈 생태계 밖의 사람들에게 설명하기 악명 높게 어려운 이유 중 하나가, 코드에서 직접 다룰 수 있는 카인드(kind) 시스템을 가진 언어가 거의 없기 때문이라고 오래전부터 의심해 왔습니다. C++처럼 카인드 시스템이 너무 난해해서 사실상 구경도 못 하는 언어도 있고요. "type->type" 카인드를 가진 것들에 대해서도 추상화할 수 있다는 점부터 설명하면, 모나드를 직접 설명하지 않고도 모나드 이해의 상당 부분에 다다를 수 있습니다.

  14. senshan HN

    문제는 모나드가 아닙니다. "세계"를 표현해야 한다는 건 어떤 프로그래머든 이해해요. 그 표현을 모듈식으로 유지해야 하다 보니 모나드 트랜스포머가 나오는데, 저 같은 보통 사람들은 바로 여기서 손을 놓고 C++로 돌아갑니다.

    https://en.wikipedia.org/wiki/Monad_transformer

  15. DonaldPShimoda HN

    모나드는 "세계"라는 개념과 본질적으로 아무 관련이 없습니다. 그건 하스켈의 IO 모나드(그리고 어쩌면 그 파생물)에만 해당하는 얘기입니다. IO 모나드가 추상화하는 대상이 프로그램 외부의 효과이기 때문이죠.

    모나드는 모나드 법칙을 따르는 모든 추상화 구조입니다. 쓸모없는 정의처럼 보일 수 있지만, 제 요지는 모나드라는 개념에 쓸 만한 직관을 얻기가 필연적으로 어렵다는 겁니다. 정보를 잃지 않고는 이보다 더 단순하게 줄일 수가 없거든요.

    지금까지 제가 정리한 가장 직관적인 설명은, 모나드가 순서가 있는 계산들의 구조에 대한 추상화라는 것입니다. 함수형 프로그래머가 아닌 사람에게 가장 와닿는 예는 아마 예외(와 그 핸들러), 그리고 null 허용 타입일 겁니다. 이런 것을 일급으로 다루는 언어에서는 보통 "옵셔널 타입"이라고 부르고, 자바는 그렇지 않습니다.

  16. Garlef HN

    흥미롭네요. 저는 수학 쪽 출신인데, 제게 모나드는 "타입에 의존하는 것들"이라는 개념을 담아낸 것입니다. 어떤 타입 X에 대해서든 X의 리스트, 레코드, 트리 같은 것을 생각할 수 있잖아요. 운이 좋으면 이 장치로 두 가지를 할 수 있습니다. 1. X의 인스턴스 x를 모두 X로 이루어진 기본적인 리스트/레코드/트리 등으로 만들 수 있습니다. 2. X의 리스트의 리스트를 X의 평범한 리스트로 평탄화할 수 있습니다.

    무엇을 찾아야 하는지 알고 나면 이런 구조가 아주 자주 보입니다.

    그리고 정말 운이 좋으면 이 둘이 서로 깔끔하게 맞아떨어집니다.

    예를 들어 L(L(L(X)))에서 L(X)로 가는 길은 두 가지입니다. 안쪽을 먼저 평탄화하거나, 바깥쪽을 먼저 평탄화하는 거죠.

    그리고 모나드라면 어느 쪽으로 가든 결과가 같습니다.

    계산과 관련된 부분은 사실 그 결과로 따라오는 것입니다. X->L(Y) 타입의 함수와 Y->L(Z) 타입의 함수가 있으면, 이 둘을 합성해서 X->L(Z) 타입의 함수를 만들 수 있으니까요.

  17. pjmlp HN

    C++로 돌아가는 게 답인지는 잘 모르겠습니다. C++20 이후로 하스켈 비슷한 코드를 꽤 많이 쓸 수 있고, 새 표준에서는 더 좋아졌으니까요.

  18. taolson HN

    제네릭 Monad 타입클래스 위에서 동작하는 함수에는 그 말이 맞을 수 있습니다. 하지만 특정 타입, 예를 들어 Maybe에 대한 구체적인 Functor / Applicative / Monad 인스턴스를 구현하는 데는 타입클래스(고차 카인드 타입)가 필요하지 않아요.

        fmap f Nothing = Nothing
        fmap f (Just x) = Just (f x)
    
    pure x = Just x
    
    Nothing >>= f = Nothing
        Just x >>= f = f x
    
  19. mrkeen HN

    "제네릭"과 "특정 타입에 대해서"는 서로 상쇄됩니다.

    IntList를 쓰려고 List<A>가 필요한 건 아니고, 특정 정수 하나를 가지려고 + 연산이 여러 정수에 대해 동작할 필요도 없잖아요.

  20. taolson HN

    맞습니다. 하지만 매개변수 다형성(List<A>)과 타입클래스 같은 애드혹 다형성 사이에는 큰 차이가 있어요. 담을 수 있는 값 타입마다 새 리스트 타입을 따로 짜야 한다면 아주 고통스럽겠죠. 하지만 모나드는 서로 다른 구현이 훨씬 적고(주로 Maybe / Either / List / State / IO 정도), 각각의 동작이 너무 달라서 코드에서 어떤 모나드를 쓰는지 정확히 알아야 하는 경우가 거의 대부분이에요. 그래서 아무 모나드에나 쓸 수 있는 "제네릭" 코드는 그만큼 쓸모가 덜합니다.

  21. fn-mote HN

    모나드를 이해 못 하겠다는 사람을 보면, 저는 일단 그 무지를 훈장처럼 달고 다닌다고 짐작합니다.

    아니라면 조금만 외우고 그 신비감을 없애 버리세요. 그렇게 동떨어진 내용은 아닙니다.

  22. lolakutty HN

    자바에서는 ArrayList<A>라고 쓸 수 있는 것 같아요. 이 타입이 사실 "type -> type" 카인드일 텐데요. 하지만 자바에서는 A<B> 같은 타입을 쓸 수 없는 것 같고, 펑터와 모나드에는 그게 필요하죠..

  23. mrkeen HN

    조금만 다듬자면,

    ArrayList의 카인드는 type->type이고,

    *ArrayList<A>*의 카인드는 type입니다.

    A가 화살표 왼쪽의 type 자리에 들어가고, 오른쪽만 남는 겁니다.

  24. vatsachak HN

    아키텍처를 설계하는 엔지니어가 된 뒤로 알게 된 게 하나 있습니다.

    뭘 하든 결국 세미그룹이나 모나드 같은 대수적 구조, 그리고 렌즈나 트래버설 같은 범주론적 구조가 계속 튀어나온다는 겁니다.

  25. skybrian HN

    시스템 설계자라면 알아 두면 도움이 될 만한 종류의 것 같지만, 시스템을 쓰는 일반 사용자는 알 필요가 없어야 하겠죠.

  26. ivanjermakov HN

    제 경험상 이런 범주들은 불변 데이터를 다룰 때 튀어나오는데, 불변 데이터는 많은 분야에서 흔하죠.

  27. childintime HN

    이 글이 실제로 하려는 말이 뭔가요? 글쓴이도 그걸 모르는 것 같은데요.

    추상화 나라 이야기로 읽힙니다. 한번 들어가면 다시는 못 빠져나오고, 거기서 보내는 하루하루마다 세금을 내야 하며, 있는 줄도 몰랐던 문제의 해법이랍시고 팔리는 곳이죠.

    어떤 문제든 C로 표현할 수 있는데, 글쓴이가 한 일을 C로 하면 어떤 모습일까요? KISS입니다. 추상화 나라는 이런 저속한 것들로부터 사람을 확실히 지켜 주나 봅니다.

  28. hutao HN

    요지는 "카인드"를 쓰면, 그 자체가 제네릭 타입을 타입 매개변수로 받는 제네릭 타입을 작성할 수 있다는 겁니다.

    구체적인 예로 JSON 객체용 데이터 타입을 정의한다고 해 봅시다. JSON 배열을 표현할 타입(자바라면 ArrayList)과 JSON 객체를 표현할 타입(HashMap 등)을 골라야 할 겁니다. 그런데 사용하는 쪽에서 HashMap 대신 TreeMap을 쓰고 싶다면 어떨까요?

    "카인드"가 있으면 JSON 데이터 타입을 JSON<ArrayType, MapType> 같은 추상적인 형태로 정의하고, 사용하는 쪽이 JSON<ArrayList, HashMap>이나 JSON<ArrayList, TreeMap>으로 인스턴스화하게 할 수 있습니다. 안타깝게도 자바 같은 언어에서는 이게 불가능합니다. ArrayList, HashMap, TreeMap은 그 자체가 제네릭 타입인데, 타입 매개변수가 될 수 있는 건 제네릭이 아닌 타입(Integer, String, HashMap<String, Integer> 등)뿐이기 때문입니다. 하지만 스칼라처럼 고차 카인드 타입(higher-kinded types)이 있는 언어에서는 가능해집니다.

    이런 고차 카인드 언어에서 Integer나 HashMap<String, Integer> 같은 타입의 "카인드"는 *이고, ArrayList 같은 제네릭 타입의 카인드는 * -> *입니다(제네릭이 아닌 타입 매개변수 하나를 받는 제네릭 타입이라는 뜻). HashMap과 TreeMap의 "카인드"는 둘 다 * -> * -> *입니다(제네릭이 아닌 타입 매개변수 둘을 받는 제네릭 타입이라는 뜻).

    C++ 템플릿도 고차 카인드 타입과 비슷한 추상화를 허용하며, 라이브러리 nlohmann::json( https://json.nlohmann.me/api/basic_json/ )은 제가 설명한 JSON 예제와 아주 비슷한 일을 실제로 합니다. 기본값으로 ArrayType에는 std::vector를, MapType에는 std::map을 쓰되, 사용하는 쪽이 내부 컨테이너를 직접 고를 수도 있게 해 놨죠. 다만 C++ 템플릿은 타입 검사를 하려면 완전히 전개되어야 해서, 컴파일 오류 메시지가 길고 읽기 어려워질 수 있습니다. "카인드" 시스템은 템플릿에 제대로 된 타입 시스템을 입혀서 이 문제를 기본적으로 해결하고, 더 이해하기 쉬운 오류 메시지를 가능하게 합니다.

  29. rramadass HN

    훌륭한 설명입니다!

    함께 보면 좋은 글: Concepts for higher-kinded types in C++20(C++20의 고차 카인드 타입을 위한 컨셉) - https://mmore500.com/cse-491/blog/2020/02/24/concepts-monads.html

  30. mrkeen HN

    As any problem can be expressed in C, how would what the author did look like in C? KISS (어떤 문제든 C로 표현할 수 있는데, 글쓴이가 한 일을 C로 하면 어떤 모습일까요? KISS입니다)

    이게 C인데요.

      void * bsearch (const void *key, const void *array, size_t count, size_t size, comparison_fn_t compare)
    

    "돌려받는 값이 찾고 있는 값과 같은 타입이다"를 이걸로 어떻게 표현하죠?

Hacker News에서 보기 ↗