하스켈을 잘 모르는 프로그래머도 이해하기 쉬운 하스켈 코드 작성법

박준규 @curry@hackers.pub

이 글에서는 제가 겪은 경험을 바탕으로 하스켈 코드의 가독성을 높이는 몇 가지 팁을 정리합니다. 각 지침에는 그에 해당하는 이유도 함께 제시됩니다.

이 글이 모든 하스켈 코드를 반드시 이렇게 작성해야 한다는 의미로 받아들이지는 마세요. 여기서 제시하는 지침들은, 생소한 문법 때문에 사람들을 겁주지 않고 언어를 소개하기 위해 예시용으로 작성하는 코드에 적용할 수 있는 가이드라인입니다.

규칙 1: ($)를 사용하지 마세요

이 지침은 아마도 가장 논란이 될 만한 내용이지만, 저는 가독성 향상에 가장 큰 영향을 주는 권장 사항이라고 생각합니다.

이 문제의 전형적인 예시는 다음과 같습니다.

print $ even $ 4 * 2

... 이는 다음 코드와 동일합니다.

print (even (4 * 2))

달러 기호($)의 가장 큰 문제는 대부분의 사람들이 이를 연산자로 인식하지 못한다는 점입니다! 다른 어떤 언어에서도 달러 기호를 연산자로 사용하는 관례가 없기 때문입니다. 실제로 대부분의 개발자는 자바스크립트, 자바, C++, 파이썬처럼 새로운 연산자를 추가할 수 없는 언어로 프로그래밍을 하기 때문에, 달러 기호가 연산자라는 사실을 바로 이해할 것이라고 기대하는 것은 합리적이지 않습니다.

이로 인해 사람들은 달러 기호가 일종의 내장 문법이라고 오해하게 되고, 결과적으로 하스켈의 문법이 불필요하게 복잡하며 가독성보다는 간결함에 최적화되어 있다고 느끼게 됩니다. 이러한 인식은 하스켈 외에서 달러 기호가 가장 많이 사용되는 곳이 펄(Perl)이라는 점 때문에 더욱 강화됩니다. 펄은 흔히 ‘작성만 가능하고 읽기 어렵다(write-only)’고 악명 높은 언어입니다.

설사 그들이 달러 기호가 연산자를 나타낸다는 것을 인식한다고 하더라도, 그 연산자가 무엇을 의미하는지는 쉽게 짐작할 수 없습니다. 통화 기호로 사용되는 기호와 함수 적용 사이에는 명확한 직관적 연결이 없기 때문입니다. 또한 하스켈 언어 외부에서는 이러한 연결을 참고할 만한 전례도 없습니다.

설사 하스켈을 처음 접하는 사람이 운 좋게도 달러 기호가 함수 적용을 나타낸다는 것을 짐작한다고 해도, 이 기호가 왼쪽 결합인지 오른쪽 결합인지 알 수 없기 때문에 여전히 모호합니다. 하스켈을 좀 더 깊이 공부하는 사람조차도 $의 동작 방식을 이해하는 데 어려움을 겪고, 종종 합성 연산자 .와 혼동하곤 합니다. 정성껏 언어를 배우는 사람조차 $를 이해하기 어렵다면, 회의적인 사람들은 도대체 얼마나 이해할 수 있을까요?

이 시점에서 이미 잠재적으로 언어에 관심을 가질 수 있었던 많은 사람들을 잃게 됩니다. 그런데도 달러 기호를 사용해야 할 이유가 무엇일까요? 달러 기호는 표현식을 더 짧게 만들어주지도 않습니다.

규칙 2: 연산자는 적절히 사용하세요

규칙 1은 규칙 2의 특수한 경우라고 볼 수 있습니다.

제가 연산자 사용에 대해 대략적으로 제시하는 지침은, 결합 가능한 연산자는 괜찮고, 그 외의 연산자는 피하라입니다.

괜찮은 연산자는 다음과 같습니다.

  • (.)
  • (+) / (*)
  • (&&) / (||)
  • (++)

피해야 할 연산자는 다음과 같습니다.

  • (<$>) / (<*>) - 앞으로는 liftA{n} 또는 ApplicativeDo를 사용
  • (^.) / (^..) / %~ / .~ - view / toListOf / over / set을 대신 사용

제가 제시한 특정 연산자를 반드시 따라야 하는 것은 아닙니다. 중요한 점은 하스켈을 가르칠 때 연산자를 보다 절제해서 사용하는 것입니다.

연산자와 관련된 문제는 본질적으로 달러 기호 문제와 매우 유사합니다.

  • 일부 사람들에게 연산자로 인식되지 않을 수 있습니다. 특히 다른 언어에 대응되는 연산자가 없다면 더욱 그렇습니다.
  • 연산자의 의미가 즉시 명확하지 않습니다.
  • 우선순위와 결합 방식이 명확하지 않습니다. 특히 하스켈 전용 연산자의 경우 더욱 그렇습니다.

제가 결합 가능한 연산자를 약간 더 선호하는 주된 이유는, 이러한 연산자는 결합 방식이 중요하지 않고, 일반적으로 수학에서 널리 쓰이는 연산자로서 다른 언어에서도 전례가 있기 때문입니다.

규칙 3: do 표기법을 적극적으로 사용하세요

가능하다면 (>>=)fmap보다 do 표기법을 사용하는 것을 권장합니다. 코드가 몇 줄 더 길어지더라도 괜찮습니다. 사람들은 장황하다는 이유로 언어를 거부하지 않습니다.(Java와 Go가 그 증거입니다.) 하지만 생소한 연산자나 함수 때문에 언어를 거부할 수는 있습니다.

즉, 다음과 같이 작성하는 대신에

example = getLine >>= putStrLn . (++ "!")

다음과 같이 작성하는 것이 좋습니다.

example = do
    str <- getLine
    putStrLn (str ++ "!")

정말 한 줄로 작성하고 싶다면, 세미콜론을 사용해 do 표기법을 그대로 활용할 수 있습니다.

example = do str <- getLine; putStrLn (str ++ "!")

do 표기법과 세미콜론은 외부인에게 즉시 인식됩니다. 이는 서브루틴 문법과 유사하기 때문이며, 가장 흔한 경우인 IO에서는 실제로 서브루틴 문법과 동일합니다.

이와 관련된 추가 권장 사항으로, 최근 GHC 메인라인에 통합되어 다음 GHC 버전에서 사용 가능하게 된 ApplicativeDo 확장을 사용하는 것입니다. 저는 ApplicativeDo<$><*> 연산자보다 외부인에게 더 읽기 쉬울 것이라고 생각합니다.

규칙 4: 렌즈를 사용하지 마세요

오해하지 마세요. 저는 렌즈의 가장 큰 지지자 중 한 명이며, 렌즈가 하스켈의 주류 관용구로 확고히 자리 잡아야 한다고 생각합니다. 하지만 초보자에게는 적절하지 않다고 느낍니다.

가장 큰 문제점은 다음과 같습니다.

  • 초보자에게 렌즈의 동작 방식을 설명하기 어렵습니다.
  • Template Haskell이나 반복적인 렌즈 정의가 필요합니다.
  • 함수 접근자와 렌즈 각각에 별도의 이름을 지정해야 하며, 그 중 하나는 보기 좋지 않게 됩니다.
  • lens-family-core의 보다 단형 버전을 사용하더라도 추론된 타입과 오류 메시지가 불명확해집니다.

렌즈는 훌륭하지만, 이를 가르칠 급한 필요는 없습니다. 렌즈를 언급하기도 전에 하스켈 언어에는 이미 배울 만한 독특하고 놀라운 요소가 충분히 많이 있습니다.

규칙 5: where와 let을 적극적으로 사용하세요

여러 줄에 걸친 하나의 거대한 표현식을 작성하고 싶은 유혹을 참으세요. 대신, wherelet을 사용하여 각각의 하위 표현식을 별도의 줄에 정의하면서 코드를 나누는 것이 좋습니다.

이 규칙은 주로 명령형 프로그래머가 함수형 프로그래밍에 쉽게 적응하도록 돕기 위해 존재합니다. 이러한 프로그래머들은 코드를 읽을 때 문장 경계와 같은 시각적 “구두점”에 익숙합니다. letwhere는 실제로는 하위 표현식일 뿐이지만, 시각적으로 큰 프로그램을 더 작은 “문장”으로 나누는 것처럼 보여 줍니다.

규칙 6: 포인트 프리 스타일은 적절히 사용하세요

모든 하스켈 프로그래머는 변수 이름을 모두 없앨 수 있는지 시도하는 시기를 겪습니다. 스포일러: 항상 없앨 수는 있지만, 이렇게 하면 코드가 지나치게 간결해져서 읽기 어려워집니다.

예를 들어, 조금 생각하고 종이와 펜을 사용하지 않고서는 다음 코드가 무슨 의미인지 도저히 알 수 없습니다.

((==) <*>)

하지만 다음과 같은 동등한 표현식은 한눈에 이해할 수 있습니다.

\f x -> x == f x

참고로, 이것은 실제 예제입니다.

포인트 프리 스타일을 어디까지 사용할지는 엄격한 규칙이 있는 것은 아니지만, 고민될 때는 덜 포인트 프리하게 작성하는 편이 안전합니다.

결론

이상입니다! 이 여섯 가지 간단한 규칙만 지켜도 하스켈 코드를 외부인에게 훨씬 읽기 쉽게 만들 수 있습니다.

하스켈은 사실, 지배적인 관용구에 익숙해지면 매우 읽기 쉬운 언어입니다. 그 이유는 다음과 같습니다.

  • 순수성
  • 명확하게 정의된 함수형 패러다임
  • 불필요한 부작용과 상태의 최소화

하지만, 하스켈을 전혀 접해보지 않은 완전한 외부인조차도 코드를 읽기 쉽도록 특별히 신경 써야 합니다. 진입 장벽은 이 언어에 대한 가장 흔한 비판 중 하나이며, 저는 간단하고 깔끔한 코딩 스타일이 그 장벽을 낮출 수 있다고 믿습니다.

11

1 comment

If you have a fediverse account, you can comment on this article from your own instance. Search https://hackers.pub/ap/articles/01996ed6-6f15-7398-96c9-0cfcfe8fc022 on your instance and reply to it.

이 글 저자분의 여러 인터넷 문서들로 공부하는데 많은 도움을 받고 있는데요. (업자가 볼만한 글을 진짜 많이 쏟아내는 감사한 분입니다.) 딱히 반론까지는 아니고, 최근 제가 생각하는 방향과 다른 게 하나 보입니다. 함수형에서 적용applicative은 너무나 중요한 요소입니다. (function을 매핑 정도로 인식했었는데, function 단어 뜻 자체가 무언가에 적용하는 것을 말합니다.) 함수형은(하스켈은) 적용을 반복해서 프로그램을 완성시켜 나갑니다. 코드 자체를, 비하스켈러들에게 홍보하기 위한, 홍보물로 본다면, $를 쓰지말자는 충분히 의미 있는 의견이지만, 실제 "하스켈 이해"를 제대로 하는데는 오히려 돌아가는 길이 될 수도 있지 않을까 조심스럽게 생각해 봅니다. $가 단순 괄호 아끼기가 아닌, 적용이란 중요한 동작 표현을 일관성 있게 하는 것으로 받아들이면, 적용의 추상화를 이용한 패턴들에서 $가 있는 것을 당연하게 받아 들일 수 있습니다.

요즘들어, 제가 여러 함수형 코딩 패턴을 이용하는 건, 뭘 알아서가 아니라, 그저 경험으로 쓰고 있다는 생각이 듭니다. 이런 문제에선 이런 패턴을 쓰더라가 경험으로 익혀지기 전에, 원리를 알고 있어 처음 보는 문제들에도, 패턴을 적소에 적용하고 싶은데, 잘 안됩니다. 물론 제 지능의 문제가 제일 크겠지만, 한 편으론 함수형을 제대로 알려주는 텍스트들을 못 만나서 그렇지 않을까 하는 건방진 생각도 듭니다. 좋은 텍스트가 없다는 뜻은 아닙니다. 제가 비전공자라 디폴트로 봐야하는 바이블들을 놓쳤을 확률이 높습니다.

저는 현재의 적용을 바라보는 눈을 갖는데, 매우 오래 걸렸습니다. 그다지 배경 지식을 요하는 것도 아니라서, 짚어주는 텍스트만 만났어도 입문에 크게 도움이 됐을텐데 하는 아쉬움이 있습니다.

지금의 제 생각으론 펑터적용으로 현실을 모델링할 수 있으면, 찐 함수형 프로그래머 아닐까 싶어요. @curry박준규

1