 |  |
|
난이도 : 초급 Ted Neward, 사장, Neward & Associates
옮긴이: 김도형 dwkorea@kr.ibm.com
2008 년 10 월 28 일 분야 특화 언어(domain-specific language, 약자로 DSL)는 많은 관심을 끄는 주제가 되었습니다. 사람들이 함수 언어에 대해 이야기하는 상당 부분은 분야 특화 언어를 만드는 데 적용할 수 있느냐 하는 것입니다. 바쁜 프로그래머를 위한 스칼라 입문의 여덟 번째인 이번 글을 시작하면서 우선 "외부(external)" DSL을 구축하는 데 있어 함수 언어가 얼마나 효과적인지를 잘 보여주는 예제로 간단한 계산기 DSL을 만들어 봅니다. 이를 위해 스칼라의 새로운 특성인 "케이스 클래스(case classes)"를 살펴 본 후 함수 언어의 오랜 특성인 "패턴 일치(pattern matching)"를 다시 살펴 봅니다.
지난 글이 나간 후 독자 의견을 통해 지금까지 이 연재에서 소개된 예제가 너무 단순하다는 불평과 조언을 들었다. 새로운 언어를 배우는 초기에는 단순한 예제를 사용하는 편이 낫지만, 더 심화된 내용이나 언어의 효능, 장점을 설명할 때는 독자 입장에서 좀 더 "실제에 가까운 예제"를 원하는 게 당연하다. 따라서 이번에는 두 부분으로 나눠 분야 특화 언어(DSL)를 구축하는 예제를 다뤄보겠다. 이번에 만들어 볼 분야 특화 언어는 작은 계산기 언어다.
 |
이 연재에 대해
Ted Neward는 이 연재를 통해 독자들에게 스칼라 프로그래밍 언어를 심층적으로 소개한다. 이 새 developerWorks 연재를 통해 스칼라에 대한 최근 스칼라를 둘러싼 떠들썩한 호평의 실체를 살펴보고, 스칼라의 언어적 특성의 일부가 실제 어떻게 사용되는지도 배울 것이다. 비교가 필요할 때는 항상 스칼라 코드와 자바 코드를 나란히 보일 예정이다. 하지만 곧 알겠지만 스칼라에 있는 많은 요소는 자바 언어와 직접적인 관계가 없는 것들이고, 이런 부분이 스칼라를 매력적으로 만든다. 그렇다면 자바 코드로도 할 수 있다면 왜 힘들여 스칼라를 배울까?
|
|
분야 특화 언어(Domain-specific languages)
여러분이 프로젝트 관리자가 위에 올려 놓은 돌덩이 아래서 기어 나올 능력(혹은 시간)이 없을 경우를 감안해 간단히 설명하면, 분야 특화 언어는 애플리케이션의 능력을 정확히 제 위치에 (다시 한번) 가져다 놓으려는 시도일 뿐이다. 바로 사용자의 손 말이다.
사용자가 배워 직접 사용할 수 있는 새로운 텍스트 형태의 언어를 정의함으로써, 실질적으로 프로그래머가 UI 관련 요청을 받아 개선하는 과정을 끝없이 반복할 필요가 없어지고, 사용자가 직접 애플리케이션에 새로운 동작을 추가할 수 있는 스크립트와 기타 도구를 만들도록 할 수 있다. 이런 예를 드는 게 다소 모험일 수 있겠지만(혹은 악의에 찬 이메일을 받을 공산이 크지만) 크게 성공적인 DSL의 공인된 예는 스프레드시트(spreadsheet)에서 다양한 계산과 셀(cell) 내용을 표현하기 위해 사용하는 마이크로소프트 오피스 엑셀(Microsoft® Office Excel) "언어"다. 혹자는 심지어 좀 더 넓게 봐서 SQL 자체가 관계형 데이터베이스를 다루기 위한 일종의 DSL이라고 주장할지도 모르겠다(프로그래머가 오라클 데이터베이스에서 전통적인 read()/write() 호출을 이용해 데이터를 꺼내야 한다고 생각해 보라. 으~).
지금부터 만들어 볼 DSL은 수식을 받아 계산하도록 설계된 간단한 계산기 언어다. 목적은 본질적으로는 여기서 작성할 코드가 계산해서 결과를 내어 놓을 상대적으로 단순한 산술식을 사용자가 입력할 수 있도록 작은 언어를 만드는 것이다. 예제를 간단하게 하기 위해 이 언어에서는 좀 더 완전한 계산기에서 통상 지원하는 많은 특성을 제외했다. 하지만 예제를 그저 교육 목적에 한정하고 싶지는 않다. 즉, 언어가 충분히 확장성이 있어서 독자가 더 많은 기능을 제공하는 언어를 만들 때 완전히 새로 만들 필요 없이 여기서 만든 것을 새로 만들 언어의 핵심으로 사용할 수 있어야 한다. 이는 그 언어가 쉽게 확장할 수 있고 사용하는 데 큰 문제가 없으면서도 가능한 캡슐화를 유지해야 한다는 것을 의미한다.
 |
DSL에 대해 더 알아보자
DSL은 큰 주제다. 심지어 이 글의 한 문단짜리 소개로는 설명을 시작하기에도 모자랄 정도로 내용이 많고 광범위하다. DSL에 대한 더 많은 정보를 원하면 마틴 파울러(Martin Fowler)가 "집필 중인 책"을 살펴보길 권한다(역주: 원문에는 글 마지막을 참고하라고 되어 있으나 누락되어 있어서 관련 링크를 찾아 넣었다). 특히 "내부(internal)"와 "외부(external)" DSL 구분에 대한 논의를 살펴 보라. 스칼라는 유연한 문법과 함수적인 특성 덕에 내부/외부 DSL 모두를 만들기 위한 효과적인 언어로 사용할 수 있다.
|
|
다시 말해 목표는 (결과적으로) 클라이언트가 다음과 같은 식으로 코드를 작성할 수 있게 하는 것이다.
Listing 1. 계산기 DSL: 목표
// 만들어진 계산기를 사용하는 자바 코드
String s = "((5 * 10) + 7)";
double result = com.tedneward.calcdsl.Calculator.evaluate(s);
System.out.println("결과는 " + result); // 57이어야 한다.
|
이번에 예제를 완성하지는 않을 것이다. 이 글에서 일부를 다룬 후 나머지는 다음 편에서 완성하겠다.
구현과 설계 관점에서는 문자열 기반 파서(parser)를 만드는 것으로 시작해 "각 문자를 하나씩 읽어내면서 계산"하는 파서를 구현하는 식으로 만들어 나가고 싶을지도 모르겠다. 하지만 이런 방식은 더 단순한 언어를 만들 때는 몰라도 복잡해지면 전혀 통하지 않는다. 이 언어의 목표 중 하나가 쉬운 확장이라는 점을 생각하면 구현 전에 잠시 그 언어의 설계에 대해 생각해 봐야 한다.
기본적인 컴파일러 이론을 잘 아는 사람이라면 언어 처리기(language processor, 해석기(interpreter)와 컴파일러 모두를 포함)의 기본 동작은 최소 다음 두 가지 기본 단계로 구성됨을 알 것이다.
- 들어오는 텍스트를 받아 추상 문법 트리(Abstract Syntax Tree, AST)를 만들어 내는 파서
- (컴파일러의 경우) AST를 받아서 원하는 바이트코드를 생성하는 코드 생성기나 (해석기의 경우) AST를 받아서 그 내용을 실행하는 평가기(evaluator)
AST가 있을 경우 결과 트리에 어떤 최적화를 할 수 있다는 것을 알면 이런 구분의 이유가 명료해진다. 우리가 만들 계산기의 경우, 수식들을 살펴 곱셈식의 피연산자 중 하나가 "0"인 경우처럼(다른 피연산자가 뭐든 무조건 "0"이다) 수식들을 통째로 날려 버릴 수 있는 곳을 찾을 수도 있다.
가장 먼저 해야 하는 것은 계산기 언어를 위한 AST를 정의하는 것이다. 다행히 스칼라에는 "케이스 클래스"라는 게 있다. "케이스 클래스"는 데이터가 많고 캡슐화를 거의 안 하는 클래스로 AST를 만드는 데 적합한 몇몇 유용한 특성을 가지고 있다.
케이스 클래스
AST 정의에 대해 더 들어가기 전에 케이스 클래스가 뭔지 간단히 살펴 보자. 케이스 클래스는 스칼라 프로그래머가 어떤 가정된 기본 설정을 바탕으로 클래스를 생성할 수 있는 편의 기능이다. 예를 들어 다음 코드를 작성한다고 하자.
Listing 2. 사람에 대한 정보를 담는 예제
case class Person(first:String, last:String, age:Int)
{
}
|
스칼라 컴파일러는 예상대로 생성자를 생성하는 외에 여러 가지를 해 준다. 즉, 상식 선에서 동작하는 equals(), toString(), hashCode() 메서드 구현을 생성해 준다. 사실 이런 종류의 케이스 클래스는 아주 일반적이라서 (즉, 추가로 멤버가 없는 클래스) 케이스 클래스 선언 다음의 "{}"는 생략할 수 있다.
Listing 3. 세상에서 가장 짧은 클래스 선언
case class Person(first:String, last:String, age:Int)
|
이 코드를 컴파일한 결과는 javap로 쉽게 확인할 수 있다.
Listing 4. Person 클래스를 javap로 살펴본 결과
C:\Projects\Exploration\Scala>javap Person
Compiled from "case.scala"
public class Person extends java.lang.Object implements scala.ScalaObject,scala.
Product,java.io.Serializable{
public Person(java.lang.String, java.lang.String, int);
public java.lang.Object productElement(int);
public int productArity();
public java.lang.String productPrefix();
public boolean equals(java.lang.Object);
public java.lang.String toString();
public int hashCode();
public int $tag();
public int age();
public java.lang.String last();
public java.lang.String first();
}
|
여기서 보는 바와 같이 케이스 클래스를 컴파일하면 일반적인 클래스를 컴파일할 때는 일어나지 않는 많은 일이 일어난다. 이는 케이스 클래스가 (이전 "컬렉션 타입" 편에서 간략히 살펴 본) 스칼라의 패턴 일치와 함께 사용하도록 설계되었기 때문이다.
케이스 클래스를 사용하는 것은 기존 클래스와 다소 다른데 이는 일반적으로 케이스 클래스 객체를 통상적인 "new" 문법으로 생성하지 않기 때문이다. 실제 클래스 이름과 같은 이름의 팩토리 메서드(factory method)를 통해 생성되는 것이 보통이다.
Listing 5. New 없이 생성하는 예
object App
{
def main(args : Array[String]) : Unit =
{
val ted = Person("Ted", "Neward", 37)
}
}
|
케이스 클래스는 그 자체로는 그다지 흥미롭거나 달라 보이지 않을지도 모른다. 하지만 중요한 차이는 케이스 클래스를 사용할 때 나타난다. 케이스 클래스를 위해 생성된 코드는 참조값이 같냐보다 비트 단위로 같냐를 따진다. 따라서 다음 코드에는 자바 프로그래머를 놀라게 할 흥미로운 부분이 있다.
Listing 6. Person 객체 간 비교
object App
{
def main(args : Array[String]) : Unit =
{
val ted = Person("Ted", "Neward", 37)
val ted2 = Person("Ted", "Neward", 37)
val amanda = Person("Amanda", "Laucher", 27)
System.out.println("ted == amanda: " +
(if (ted == amanda) "Yes" else "No"))
System.out.println("ted == ted: " +
(if (ted == ted) "Yes" else "No"))
System.out.println("ted == ted2: " +
(if (ted == ted2) "Yes" else "No"))
}
}
/*
C:\Projects\Exploration\Scala>scala App
ted == amanda: No
ted == ted: Yes
ted == ted2: Yes
*/
|
케이스 클래스의 진짜 가치를 느낄 수 있는 것은 패턴 일치에 사용할 때다. 지금까지 이 연재를 읽었다면 자바의 "switch/case"와 비슷하지만 아주 더 심오한 효과와 기능을 가진 것임을 기억할 것이다(이 연재의 세 번째 글로 스칼라의 다양한 제어문에 대해 다룬 글). 패턴 일치는 주어진 구문의 값이 case 값에 일치하는지 살펴볼 수 있을 뿐 아니라 부분적인 와일드카드(wildcard)에 대해서도 맞춰볼 수 있다(부분적인 "default"와 비슷한 것으로 생각할 수 있다). case들은 일치하는지 시험하기 위한 조건(guard)을 가질 수 있고, 해당 일치 조건에서 값을 뽑아 지역 변수에 연계할 수 있다. 심지어 해당 일치 조건에서 타입이 일치하는지도 확인할 수 있다.
Listing 7에서 보듯이 패턴 일치는 케이스 클래스와 함께 사용하면 완전히 새로운 경지의 효과를 발휘하기 시작한다.
Listing 7. 케이스 클래스와 함께 사용한 패틴 일치 예제
case class Person(first:String, last:String, age:Int);
object App
{
def main(args : Array[String]) : Unit =
{
val ted = Person("Ted", "Neward", 37)
val amanda = Person("Amanda", "Laucher", 27)
System.out.println(process(ted))
System.out.println(process(amanda))
}
def process(p : Person) =
{
p + "을(를) 처리해 다음 사실을 밝혀 냈다. =>" +
(p match
{
case Person(_, _, a) if a > 30 =>
" 그들은 분명히 나이가 많다."
case Person(_, "Neward", _) =>
" 그들은 좋은 유전자를 타고 났다...."
case Person(first, last, ageInYears) if ageInYears > 17 =>
first + " " + last + " is " + ageInYears + " years old."
case _ =>
" 이 사람으로 뭘 할지 모르겠다."
})
}
}
/*
C:\Projects\Exploration\Scala>scala App
Processing Person(Ted,Neward,37)을(를) 처리해 다음 사실을 밝혀 냈다. => 그들은 분명히 나이가 많다.
Processing Person(Amanda,Laucher,27)을(를) 처리해 다음 사실을 밝혀 냈다. => Amanda Laucher은(는) 27살이다.
*/
|
Listing 7에서는 동시에 많은 일이 일어난다. 어떤 일이 일어나는지 천천히 살펴본 후 계산기 예제로 돌아가 어떻게 적용할지를 살펴 보자.
첫째, 전체 match 식은 괄호로 싸여 있다. 이는 패턴 일치 문법에서 반드시 필요한 건 아니지만 여기서는 패턴 일치식의 결과(함수 언어에서는 모든 것이 수식이란 점을 기억하자)를 그 앞 선두 문자열에 연결하기 때문에 사용했다.
둘째, 첫 case 식에는 와일드카드가 두 개 있는데('_' 문자가 와일드카드다) 일치한 Person 내 해당 필드 두 개에 어떤 값이든 일치된다는 뜻이다. 하지만 동시에 지역 변수 a에 p.age가 연결된다. 이 case는 해당 조건식(guard expression, 뒤 따라오는 if 식)이 만족해야 만족한다. Listing 7에서 첫 번째 Person만 만족하고 다음 Person은 만족하지 못한다. 두 번째 case 식은 Person의 firstName을 위한 와일드카드를 사용한다. 하지만 lastName 부분은 문자열 상수인 "Neward"에 일치하는지를 보고 age 부분에는 와일드카드를 적용한다.
첫 번째 Person이 이미 이전 case에 일치했고 두 번째 Person은 성(last name)이 Neward가 아니기 때문에 이 case는 둘 다에 일치하지 않는다. (하지만 Person("Michael", "Neward", 15)는 첫 번째 case의 조건식을 만족시키지 못하고 두 번째로 넘어오기 때문에 두 번째를 만족한다.)
세 번째 case는 간혹 추출(extraction)이라고 부르는 패턴 일치의 일반적인 사용 예를 보인 것이다. 여기서 일치해 볼 객체 p 내의 값들은 case 블록 내에서 사용할 수 있도록 지역 변수(first, last, ageInYears)로 추출된다. 마지막 case 식은 다른 case 식이 모두 만족하지 않았을 때 실행되는 일반적인 경우를 위한 기본값이다.
짧기는 하지만 케이스 클래스와 패턴 일치에 대한 개요를 명심하고 계산기 AST를 만드는 문제로 돌아가 보자.
계산기 AST
먼저 계산기의 AST는 수식은 보통 부분식들로 구성되기 때문에 아마도 공통 베이스 타입을 가져야 할 것이다. 이를 확인할 수 있는 가장 쉬운 방법은 예제로 "5 + (2 * 10)"을 살펴 보는 것이다. 여기서 부분식인 "(2 * 10)"은 "+" 연산자의 오른쪽 피연산자다.
사실 이 식에서는 세 가지 AST 타입이 필요하다.
- 기본 수식(base expression)
- 상수값을 담을 Number 타입
- 연산자와 두 피연산자를 담을 BinaryOperator 타입
조금 생각해 보면 수학에서는 양수를 음수로 바꾸는 부정 연산자(negation operator, 마이너스 부호)가 있다는 사실을 떠올릴 수 있을 것이다. 따라서 다음과 같은 기본적인 AST를 만들 수 있다.
Listing 8. 계산기 AST(src/calc.scala)
package com.tedneward.calcdsl
{
private[calcdsl] abstract class Expr
private[calcdsl] case class Number(value : Double) extends Expr
private[calcdsl] case class UnaryOp(operator : String, arg : Expr) extends Expr
private[calcdsl] case class BinaryOp(operator : String, left : Expr, right : Expr)
extends Expr
}
|
이 모든 것을 하나의 패키지(com.tedneward.calcdsl)에 넣기 위해 패키지 선언을 사용한 것과, 각 클래스의 선두에 해당 클래스를 이 패키지나 서브패키지의 다른 멤버에서 접근할 수 있다는 것을 나타내기 위해 접근 제한 지시어(access modifier)를 사용한 것을 유심히 살펴보기 바란다. 이렇게 한 이유는 이 코드를 시험하는 일련의 JUnit 테스트를 만들기 위한 것이다. 계산기의 실제 클라이언트 프로그램은 AST를 볼 필요가 없다. 따라서 단위 테스트는 com.tedneward.calcdsl의 서브패키지가 되도록 작성한다.
Listing 9. 계산기 테스트(testsrc/calctest.scala)
package com.tedneward.calcdsl.test
{
class CalcTest
{
import org.junit._, Assert._
@Test def ASTTest =
{
val n1 = Number(5)
assertEquals(5, n1.value)
}
@Test def equalityTest =
{
val binop = BinaryOp("+", Number(5), Number(10))
assertEquals(Number(5), binop.left)
assertEquals(Number(10), binop.right)
assertEquals("+", binop.operator)
}
}
}
|
지금까지는 잘해 왔다. 이제 AST가 생겼다.
여기에 대해 잠시 생각해 보자. 스칼라 코드 네 줄로 얼마나 복잡하게 중첩된 수식이든(분명 단순한 수식이지만 그래도 유용하다) 표현할 수 있는 타입 계층을 만들었다. 이 결과가 객체 프로그래밍을 더 쉽고 더 표현력 있게 하려는 스칼라의 방향성을 보여주는 것임에 비해 함수 언어적이지는 않다(함수 언어적인 부분은 뒤에 나오니 걱정 말라).
다음으로 AST를 받아 결과를 계산할 평가 함수(evaluation function)가 필요하다. 이 함수는 패턴 일치를 사용하면 매우 간단히 작성할 수 있다.
Listing 10. 계산기(src/calc.scala)
package com.tedneward.calcdsl
{
// ...
object Calc
{
def evaluate(e : Expr) : Double =
{
e match {
case Number(x) => x
case UnaryOp("-", x) => -(evaluate(x))
case BinaryOp("+", x1, x2) => (evaluate(x1) + evaluate(x2))
case BinaryOp("-", x1, x2) => (evaluate(x1) - evaluate(x2))
case BinaryOp("*", x1, x2) => (evaluate(x1) * evaluate(x2))
case BinaryOp("/", x1, x2) => (evaluate(x1) / evaluate(x2))
}
}
}
}
|
여기서 evaluate()가 Double을 돌려주는데, 이는 패턴 일치 내 각 case를 계산하면 Double 값이 되어야 한다는 뜻이다. 이는 어렵지 않다. 우선 Number 객체들은 단순히 가진 값을 돌려준다. 하지만 다른 경우, 즉 두 가지 연산자의 경우에는 주어진 피연산자들에 지정된 연산(부정, 덧셈, 뺄샘 등)을 행하기 전에 피연산자들을 먼저 계산해야 한다. 함수 언어에서 일반적인 재귀 호출을 사용해 단순히 해당 연산을 하기 전에 각 피연산자에 대해 evaluate()를 호출해 준다.
이렇게 각종 연산자 자체가 아닌 "밖"에서 계산을 수행하는 방식은 대부분의 골수 객체 지향 프로그래머에게는 근본적으로 틀렸다고 생각될 것이다. 분명히 캡슐화(encapsulation)와 다형성(polymorphism)의 법칙을 심각하게 위배했다. 솔직히 말해 심지어 논쟁할 가치도 없다. 최소 전통적인 관점에서는 그 중에서도 분명히 캡슐화를 위배했다.
여기서 생각해야 하는 더 큰 질문은 정확하게 우리가 해당 코드를 무엇으로부터 캡슐화하려는가 하는 것이다. 이 AST가 해당 패키지 밖에서는 접근할 수 없다는 점과 클라이언트는 (결과적으로) 단순히 계산하고자 하는 수식을 담은 문자열을 전달할 것이라는 점을 상기하자. AST 케이스 클래스를 직접 다루는 것은 단위 테스트뿐이다.
하지만 이는 모든 캡슐화가 필요 없거나 한물 갔다는 말이 아니다. 사실 정확하게 반대다. 즉, 우리가 객체 세계에서 친숙해진 설계 방식 외에도 다른 잘 맞는 설계 방식이 있다는 점을 말하려는 것이다. 스칼라가 객체와 함수의 융합이라는 것을 잊지 말자. Expr과 서브클래스에 추가 동작(예를 들어 toString 출력을 보기 좋게 다듬는 것 같은)을 넣을 필요가 있다면, Expr에 메서드를 추가하는 것은 어렵지 않다. 함수와 객체 지향 세계의 조합은 이점이다. 함수 언어 프로그래머든 객체 지향 프로그래머든 상대의 설계 방식을 무시하거나 두 방식을 조합해 흥미로운 결과를 만들어낼 수 있다는 사실을 간과해서는 안 된다.
설계 관점에서 예제 내 몇몇 다른 선택은 문제가 있을 수도 있다. 예를 들어 연산자를 표현하기 위해 문자열을 사용했기 때문에 단순한 오타로도 오류가 생길 수 있는 여지를 열어 놓은 셈이다. 상용 코드에서는 문자열 대신 열거형(enumeration)을 사용할 수 있다(아마 사용해야 할 것이다). 하지만 문자열로 남겨두면 추후 연산자로 (abs, sin, cos, tan 같은) 복잡한 함수나 심지어 사용자 정의 함수를 추가할 "여지"를 남겨둘 수 있다. 이는 열거형을 사용해서는 쉽지 않을 것이다.
모든 설계와 구현 사항의 결정에는 옳은 한 가지 길이란 없다. 그저 결과가 있을 뿐이다. Caveat emptor.(역주: Let the buyer beware라는 뜻의 라틴어다. 구매자 위험 부담 원칙을 말하는 말로 구매자가 스스로 주의해 잘 따져야 하고 일단 사고가 나면 책임은 자신에게 있다는 뜻이다. 여기서는 모든 설계와 구현 사항을 잘 따져 스스로 알아서 결정하라는 의미로 썼다.)
하지만 여기에 적용할 수 있는 한 가지 흥미로운 구현 수단이 있다. 어떤 수식은 단순하게 만들 수 있고 따라서 (잠재적으로) 식의 평가를 최적화할 수 있다(그리고 이는 AST의 유용성을 보여주는 한 예다).
- "0"을 더하는 것은 0이 아닌 피연산자로 단순화될 수 있다.
- "1"을 곱하는 것은 0이 아닌 피연산자로 단순화될 수 있다.
- "0"을 곱하는 것은 0으로 단순화될 수 있다.
등이다. 그래서 이런 단순화를 수행하기 위해 simplify()라는 평가 전 과정을 추가하겠다.
Listing 11. 계산기(src/calc.scala)
def simplify(e : Expr) : Expr =
{
e match {
// 부정 두 번은 원래 값과 같다.
case UnaryOp("-", UnaryOp("-", x)) => x
// + 하나는 원래 값과 같다.
case UnaryOp("+", x) => x
// x를 1로 곱하면 원래 값과 같다.
case BinaryOp("*", x, Number(1)) => x
// 1을 x로 곱하면 x다.
case BinaryOp("*", Number(1), x) => x
// x를 0으로 곱하면 0이다.
case BinaryOp("*", x, Number(0)) => Number(0)
// 0을 x로 곱하면 0이다.
case BinaryOp("*", Number(0), x) => Number(0)
// x를 1로 나누면 원래 값과 같다.
case BinaryOp("/", x, Number(1)) => x
// x에 0을 더하면 원래 값과 같다.
case BinaryOp("+", x, Number(0)) => x
// 0에 x를 더하면 x다.
case BinaryOp("+", Number(0), x) => x
// 다른 경우는 (아직은) 단순화할 수 없다.
case _ => e
}
}
|
여기서 거듭 패턴 일치의 상수 일치와 변수 연결 때문에 이런 식을 쉽게 작성할 수 있다는 점을 유의하자. evaluate()에는 계산 전에 먼저 단순화를 하도록 호출하는 부분을 추가하기만 하면 된다.
Listing 12. 계산기(src/calc.scala)
def evaluate(e : Expr) : Double =
{
simplify(e) match {
case Number(x) => x
case UnaryOp("-", x) => -(evaluate(x))
case BinaryOp("+", x1, x2) => (evaluate(x1) + evaluate(x2))
case BinaryOp("-", x1, x2) => (evaluate(x1) - evaluate(x2))
case BinaryOp("*", x1, x2) => (evaluate(x1) * evaluate(x2))
case BinaryOp("/", x1, x2) => (evaluate(x1) / evaluate(x2))
}
}
|
더한 단순화도 가능하다. 이 방식이 어떻게 트리의 최하단에서만 단순화하는지 알겠는가? BinaryOp("*", Number(0), Number(5))와 Number(5)를 피연산자로 가진 BinaryOp이 있다면, 먼저 중첩된 BinaryOp은 Number(0)으로 단순화할 수 있다. 하지만 이제 바깥 BinaryOp의 피연산자 중 하나도 0이므로 마찬가지로 단순화할 수 있다.
필자로서 충동적으로 이 부분은 독자의 몫으로 남기겠다. 사실을 말하면 좀 즐겨보자. 독자가 구현한 내용을 보내주면 다음 글의 코드 묶음과 본문에 (감사의 표시와 함께) 포함시키겠다. (이를 시험하는 단위 테스트가 몇 개 있고 당장에는 실패하도록 되어 있다. 받아 들인다면 여러분의 임무는 해당 테스트와 어떤 단계든 중첩된 BinaryOp와 UnaryOp을 가진 테스트를 통과하게 하는 것이다.)
결론
분명히 예제를 마친 건 아니다. 파싱하는 부분을 구현해야 하지만, 계산기 AST는 아주 잘 만들어졌다. 큰 수술 없이 연산자를 추가할 수 있다. 또 AST를 따라 가는 데는 많은 코드가 필요하지 않고(Gang of Four의 방문자(visitor) 패턴과 유사하다), 이미 계산하기 위한 코드가 동작한다(클라이언트가 AST를 만들어 주기만 하면 된다).
더 중요한 것은 케이스 클래스를 패턴 일치와 사용해 간단히 AST를 생성하고 평가할 수 있다는 것을 알았다. 이는 스칼라 코드에서 빈번한 설계 방식이며(사실 대부분의 함수 언어에서도 그렇다), 이 환경에서 한동안 개발을 하려고 한다면 익숙해질 필요가 있다.
참고자료 교육
- "바쁜 자바 프로그래머를 위한 스칼라 입문: 컬렉션 타입"(Ted Neward, 한국 developerWorks, 2008년 9월): 패턴 일치에 대해 다룬다.
- "바쁜 자바 프로그래머를 위한 스칼라 입문: 클래스 동작"(Ted Neward, 한국 developerWorks, 2008년 4월): 스칼라의 다양한 제어문에 대해 다룬다.
- "바쁜 자바 프로그래머를 위한 스칼라 입문"(Ted Neward, 한국 developerWorks): 이 연재를 모두 읽어 보자.
- "자바를 이용한 함수 프로그래밍"(Abhijit Belapurkar, 한국 developerWorks, 2007년 4월): 자바 개발자 관점에서 함수 프로그래밍의 이점과 용례에 대해 배운다.
- "Scala by Example"(Martin Odersky, 2007년 12월): 짧은 코드 중심의 스칼라 입문서(PDF 형식)
- Programming in Scala(Martin Odersky, Lex Spoon, Bill Venners; Artima, 2007년 12월): 최초의 책 한 권 분량의 스칼라 입문서
- 한국 developerWorks 자바 기술 존: 자바 프로그래밍의 모든 측면에 대한 수백 개 기사가 제공된다.
제품 및 기술 얻기
- Scala 내려받기: 이 연재와 함께 스칼라를 배워보자.
- SUnit: 표준 스칼라 배포판의 일부로 scala.testing 패키지 내에 있다.
토론
필자소개  | 
|  | Ted Neward는 Neward & Associates의 사장으로 자바, .NET, XML 서비스와 다른 플랫폼에 대한 컨설팅, 조언, 교육, 강연을 한다. 워싱턴 주 시애틀 근처에 산다. |
기사에 대한 평가
 |
| 이 문서 북마킹 하기
|
|  |