프로그래밍 언어 교육에서 동등성 비교를 성급히 도입하지 말아야 하는 이유와, 패턴 매칭과 적절한 인터페이스 설계를 통해 동등성과 불(Boolean)을 피하는 방법을 ML 예제로 설명한다.
10표
내가 CS를 가르치면서 배운 것 중 하나는 특정 주제에 대해 무엇을 말하느냐 못지않게 무엇을 말하지 않느냐가 중요하다는 것이다. 어떤 사고의 흐름이 경험상 학생들을 실수와 오해로만 이끈다는 걸 알게 되면, 그런 방향으로 빠지지 않도록 스스로를 다시 훈련하는 데 많은 주의가 필요하다. 대표적인 예가 바로 골칫거리인 동등성(equality) 문제인데, 이는 학생들에게 끝도 없는 어려움을 만들고 온갖 오해를 불러일으킨다. 내 조언: 동등성은 꺼내지도 마라!
설명해 보자. 먼저 C 같은 기초적인 언어에서는 동등성을 피할 동기도, 피할 가능성도 없다. 모든 데이터가 1차(first-order)이고, 모든 식 타입이 상수 시간에 동등성 검사를 허용한다. 정수의 동등성은 문제가 없지만, 실수의 동등성에는(IEEE 표준에 따르면) 두 가지 서로 다른 의미가 있다는 점이 종종 간과된다. 그중 하나는 반사적(reflexive)이지 않고, 다른 하나는 추이적(transitive)이지 않다. 독은 골라 마시라. 포인터는 서로의 동등성뿐 아니라 “NULL”과도 비교 가능해야 한다. 그것이 무엇을 의미하든 간에. (이것이 토니 호어가 스스로 “십억 달러짜리 실수”라 부른 것으로, 프로그래밍 언어계 의인성 병폐의 대모다.) 다소 난장판이긴 해도, 이런 언어들에서 동등성 자체가 문제라고 생각하는 사람은 거의 없다. 상대적으로 보면 그렇지 않다.
하지만 ML 같은 추상적인 언어에서는 동등성이 보다 근본적인 문제를 낳는다. 모두가 순진하게 생각하는 것과 달리, 모든 타입이 동등성 검사를 허용하는 것은 아니다. 첫 번째 장애물은 결정가능성이다. 예컨대 언어에 함수가 있다면, 그 동등성을 비교할 수 있을 거라 기대할 수 없다. 두 번째는 실용성이다. 언어에 트리나 스트림 같은 집합체(aggregate)가 있다면, 원리적으로 가능하더라도 그들을 동등성으로 비교하고 싶지 않을 것이다. 그리고 무엇보다 중요한 건, 그게 이치에 맞지 않는다는 점이다. 동등성 검사는 추상 타입의 표현 독립성(representation independence)을 침해하므로, 처음부터 그것에 의존할 이유가 없다. 추상적 의미에서 동등한 두 집합은 서로 다른 표현을 가질 수도 있고, 그럴 수도 없다. 추상의 요점은 클라이언트 코드가 그 여부에 절대 의존하지 못하게 하는 데 있다.
따라서 동등성을 꺼냈다면 이미 큰 난장판을 떠안은 셈이다. 그러나 대안이 있다. 동등성에 “아니오”라고 말하라. 더 정확히는, 아예 아무 말도 하지 말고, 꺼내지도 말라. 그리고 이를 피하면 또 다른 이점이 있는데, 바로 불(Boolean)(프로그래밍 언어계 의인성 병폐의 계모)을 통째로 피할 수 있고, 그 편이 더 낫다는 것이다. (그 이야기는 다른 글로 미루겠다. 쓸데없는 논쟁을 피하기 위해 미리 말하자면, 결국 언젠가는 동등성을 도입해야 할 때가 오는데, 그건 가끔 필요하기 때문이다. 다만 훨씬 훨씬 뒤, 학생들이 깔끔하고 좋은 코드를 쓰는 데 진짜 장애물들을 무사히 넘어선 다음이어야 한다. 그들 앞에 통나무까지 굴려 가로막을 필요는 없다. 대신 진짜 중요한 것에 시간을 쓰게 하라.)
동등성의 많은 사용처는 패턴 매칭으로 대체하는 것이 더 낫다. 다음과 같은 귀납적으로 정의된 타입이 있다면
datatype nat = Z | S of nat
정의역이 nat인 모든 함수는 비교로 정의할 것이 아니라 경우분해로 정의해야 한다:
fun plus x y =case x of Z => y | S(x’) => S(plus x’ y)
많은 학생들은 대신 이렇게 쓰고 싶어 한다:
fun plus x y = if x=Z then y else S (plus (pred x) y)
여기서 pred가 미리 정의되어 있다고 치자. 당신이 동등성을 꺼낸 적이 없더라도 말이다! (아이들이 “길거리에서” 뭘 배워 오는지는 놀랍다.) 이런 습관은 반드시 근절해야 한다! 전임자 함수를 하나 더 써야 할 뿐만 아니라, 가련한 학생은 자신의 도구상자에서 가장 귀중한 도구 중 하나인 패턴 매칭의 포괄성 검사(exhaustiveness check)를 스스로 빼앗아 버렸다. (더 미묘하게, 그리고 더 나쁘게도, pred의 정의 자체가 Z인지를 검사해야만 한다. 비록 호출 지점에서는 x가 Z일 리 없다는 걸 “안다” 하더라도 말이다. 불과 명제의 혼동을 논할 때 이 문제로 돌아오겠다.)
그런데 만약 당신이 기계 정수로 프로그래밍하고 있다면 어떨까? 예를 들어 ML에서 타입 int에 대한 팩토리얼 함수를 어떻게 정의할까? 한 가지 기본 접근법은 Standard ML 라이브러리가 제공하는 순서화된 타입(ordered type) 개념을 활용하는 것이다. 요지는 다음과 같다:
fun fact x = case compare (x, 0) of LSS => raise Undefined | EQL => 1 | GTR => x * fact (x-1)
여기서는 여전히 선행자가 필요하지만, 조금만 더 수고하면 그것도 없앨 수 있다:
**datatype TRI = Neg of int | Zero | Pos of int
fun tri x = case compare (x,0) of LSS => Neg (x+1) | EQL => Zero | GTR => Pos (x-1)
fun fact x = case tri x of Neg _ => raise Undefined | Zero => 1 | Pos of x’ => x * fact x’**
이것만 떼어 보면 다소 무거워 보일 수 있지만, 실제로는 처음부터 올바른 인터페이스를 설계하고, 그것을 유리하게 활용하느냐의 문제일 뿐이다.
같은 원칙을 option 타입에 적용해 보면, 왜 “널 포인터 분석(null pointer analysis)”이 그렇게 어리석은 발상인지가 명확해진다. 변수 x가 어떤 타입 t에 대해 t option 타입이라고 하자. 다음처럼 쓰는 대신에
if x=NONE then raise NullPtrException else …
이는 else 분기에서 x가 NONE이 아님(non-NONE)이라는 정보가 전파되지 않는다. 대신 더 명료한 경우분해를 쓰라:
case x of NONE => raise NullPtrException | SOME x’ => …
여기서 두 번째 절의 **x’**는 t option이 아니라 t 타입이며, 따라서 어떤 경우에도 NONE일 수 없다!
이 글은 2011년 3월 15일 화요일 오후 11:07에 게시되었으며, 프로그래밍, 교육 분류에 속합니다. 이 글에 대한 응답은 RSS 2.0 피드를 통해 팔로우할 수 있습니다. 현재 댓글과 핑은 닫혀 있습니다.