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

박준규 @curry@hackers.pub
- 저자: 가브리엘라 곤잘레스(Gabriella Gonzalez)
- 링크: https://www.haskellforall.com/2015/09/how-to-make-your-haskell-code-more.html
- 라이선스: Creative Commons Attribution 4.0 International License
이 글에서는 제가 겪은 경험을 바탕으로 하스켈 코드의 가독성을 높이는 몇 가지 팁을 정리합니다. 각 지침에는 그에 해당하는 이유도 함께 제시됩니다.
이 글이 모든 하스켈 코드를 반드시 이렇게 작성해야 한다는 의미로 받아들이지는 마세요. 여기서 제시하는 지침들은, 생소한 문법 때문에 사람들을 겁주지 않고 언어를 소개하기 위해 예시용으로 작성하는 코드에 적용할 수 있는 가이드라인입니다.
규칙 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을 적극적으로 사용하세요
여러 줄에 걸친 하나의 거대한 표현식을 작성하고 싶은 유혹을 참으세요. 대신, where
나 let
을 사용하여 각각의 하위 표현식을 별도의 줄에 정의하면서 코드를 나누는 것이 좋습니다.
이 규칙은 주로 명령형 프로그래머가 함수형 프로그래밍에 쉽게 적응하도록 돕기 위해 존재합니다. 이러한 프로그래머들은 코드를 읽을 때 문장 경계와 같은 시각적 “구두점”에 익숙합니다. let
과 where
는 실제로는 하위 표현식일 뿐이지만, 시각적으로 큰 프로그램을 더 작은 “문장”으로 나누는 것처럼 보여 줍니다.
규칙 6: 포인트 프리 스타일은 적절히 사용하세요
모든 하스켈 프로그래머는 변수 이름을 모두 없앨 수 있는지 시도하는 시기를 겪습니다. 스포일러: 항상 없앨 수는 있지만, 이렇게 하면 코드가 지나치게 간결해져서 읽기 어려워집니다.
예를 들어, 조금 생각하고 종이와 펜을 사용하지 않고서는 다음 코드가 무슨 의미인지 도저히 알 수 없습니다.
((==) <*>)
하지만 다음과 같은 동등한 표현식은 한눈에 이해할 수 있습니다.
\f x -> x == f x
참고로, 이것은 실제 예제입니다.
포인트 프리 스타일을 어디까지 사용할지는 엄격한 규칙이 있는 것은 아니지만, 고민될 때는 덜 포인트 프리하게 작성하는 편이 안전합니다.
결론
이상입니다! 이 여섯 가지 간단한 규칙만 지켜도 하스켈 코드를 외부인에게 훨씬 읽기 쉽게 만들 수 있습니다.
하스켈은 사실, 지배적인 관용구에 익숙해지면 매우 읽기 쉬운 언어입니다. 그 이유는 다음과 같습니다.
- 순수성
- 명확하게 정의된 함수형 패러다임
- 불필요한 부작용과 상태의 최소화
하지만, 하스켈을 전혀 접해보지 않은 완전한 외부인조차도 코드를 읽기 쉽도록 특별히 신경 써야 합니다. 진입 장벽은 이 언어에 대한 가장 흔한 비판 중 하나이며, 저는 간단하고 깔끔한 코딩 스타일이 그 장벽을 낮출 수 있다고 믿습니다.