2018년 8월 19일 일요일

[haskell] 변경 가능성이 다분한 부분을 다루자 -02

출처 : 하스켈로 배우는 함수형 프로그래밍

변경 가능성이 다분한 부분을 하스켈로 다뤄보자.


하스켈에서는 비지터 패턴을 사용하지 않는다.

1. 표현식의 형태를 정의하자.
-- 식
data Expr a = Plus (Expr a) (Expr a)  -- 덧셈의 식
            | Square (Expr a)         -- 제곱의 식
            | Number a                -- 숫자의 식
위 내용으로 식에 대한 정의는 끝났다.

2. 이제 처리방식 기술하자.
-- 식의 평가를 실시하는 함수
evalExpr :: Expr Int -> Int
evalExpr (Plus e1 e2) = evalExpr e1 + evalExpr e2
evalExpr (Square e)   = evalExpr e ^ (2 :: Int)
evalExpr (Number n)   = n

-- 식을 문자열로 하는 함수
showExpr :: Expr Int -> String
showExpr (Plus e1 e2) = showExpr e1 ++ " + " ++ showExpr e2
showExpr (Square e)   = "(" ++ showExpr e ++ ")^2"
showExpr (Number n)   = show n
이것도 이렇게 끝났다. 간단하다.

3. 실행하자
main :: IO ()
main = do
  -- e = 1 + (2 + 3)^2
  let e = Plus (Number 1) (Square (Plus (Number 2) (Number 3)))
  putStrLn (showExpr e)
  print (evalExpr e)

여기서 수정사항이 생긴다면
- 식의 증가 (뺄셈 추가)
a. data Expr 에 뺄셈을 추가 (ex. Sub (Expr a) (Expr a) )
- 식에 대한 처리 종류 증가 (JSON 변환)
a. 단순히 evalExpr이나 showExpr과 같은 함수 증가

이렇게 전편에서 사용한 자바의 비지터 패턴을 이용한 코드와 수정하는 방식도 다르고, 코드의 양도 다르다. 왜 이렇게 다를까?
서적에서는 "환경의차이"라고 한다. 꼭 이렇게 설명을 안 하더라도 한줄한줄 비교를 해보면 다른 점이 있는데 그것들이
1. 타입을 정의하는 방식의 차이
2. 패턴매칭(타입을 확인하는 방식)의 차이

1. 타입을 정의하는 방식의 차이

하스켈에서는 data라는 키워드로 타입을 생성한다. 특이한 점은 수식의 구조를 그대로 반영할 수 있다는 것이다. 대부분 우리가 만들려는 새로운 타입들은 저마다 구조를 갖고 있다. 하스켈에서는 그 구조를 아주 쉽게 표현할 수 있다. 마치 수학의 수식처럼 말이다. 그래서 "식 := 식 '+' 식 | '('식')'^2 | 숫자 " 와 같은 (BNF로 표현되는 덧셈,제곱,숫자 자체 중 무엇이든 될 수 있는) 구조를 아주 쉽게 만들어 내는 것이다.

자바에서는 타입을 정의한다는 것은 class, interface를 생성한다는 것이다. Visitor패턴에서는 이것들을 좀 더 세련되게 만든다.
식을 만들려면 식이라는 타입을 만들어야 겠다. 그래서 Expr 인터페이스를 만들고 이것을 이용해서 Plus, Square, Number를 구현한다. 그리고 단순히 구현하는 것이 아니라 Visitor를 받기 위한 함수를 생성할 것이다! 그것은 accept함수이다. 그리고 이제 방문하는 녀석들을 이용해서 표현식들을 처리할 것이다.

이 시점에서 Haskell에서는 취급하고 싶은 식의 타입을 만드는데 3로 간단하게 정의하였다.
Java에서는 interface 1개, class 3개 총 4개를 정의하고 각 3개의 class에 3가지의 accept를 구현해야 한다.(물론 이건 별거 아니다.)
이는 분명 동일한 데이터구조를 다루고 있는데도 어떤 언어냐에 따라 구현 비용이 다르다는 것을 뜻한다.

물론 자바에서 Visitor 패턴을 버리고 Expr 클래스 하나를 만들어서 그 안에 다~ 넣을 수 있다. 그렇게 하면 Expr 클래스는 비대하게 커질 수 있고, 식을 확장하기 위해서 사용하는 비용은 점점 커질 것이다.

2. 패턴매칭(타입을 확인하는 방식)의 차이

Visitor 패턴에 의한 설계에서는 Expr인터페이스를 갖는 각 클래스가 각각의 accept 메소드 속에서 각 클래스 전용으로 준비한 Visitor 인터페이스 메소드를 호출함으로써 판별한다.

예를들어, Number 클래스는 Visitor 인터페이스의 number 메소드를 호출해 두는 식으로 처리한다. 그리고 showExpr와 evalExpr에서 대응하는 처리 또한 Show와 Eval이라는 Visitor 인터페이스를 갖는 클래스로서 구현하게 된다.

Show,Eval 클래스의 plus,square,number의 구현내용은 Haskell의 showExpr, evalExpr 함수의 구현과 거의 동일하다는 것을 알 수 있다.

반대로, 본질적으로는 Haskell 수준의 기술 방식으로 끝나겠지만, 본질적인 부분 이외의 기술에 그만큼 불필요한 비용이 발생할 것이다. 컴파일러는 "이 중 어느 것인가"를 전혀 모르는 듯한 언어 사양이므로 이렇게 할 수밖에 없다.

**Visitor 패턴을 버린 다른 설계로서 instanceof와 같은 실행 시의 타입 정보를 사용할 수 있는 언어라면, 패턴 매치를 통해 인스턴스가 어떤 타입(클래스)의 것인지를 판별하는 구현도 가능할 것이다. 그러나 이 경우 수식이 확장될 때, 그 표현식에 대응한 처리의 구현이 어디선가 누락되었다고 해도 검출할 수가 없다.
구현의 누락은 인터페이스의 부재로 인한 컴파일 오류로 감지되므로 Visitor패턴 쪽이 instanceof와 같은 방법보다 안전하다. (솔직히 instanceof로 만들어진 코드를 보면서 신용이 갈리가 있나... if문이 남발 된 코드는 항상 믿을 수 없다.)

댓글 없음:

댓글 쓰기