Programming in Scala 15장

Scala는 돌아서면 잊어버려 15장부터는 노트합니다. 편의상 음슴체로 갑니다.

15 Case Classes and Pattern Matching

15.1 A simple example

산술 연산을 위한 case 클래스 정의는 다음과 같음.

1
2
3
4
5
abstract class Expr
case class Var(name: String) extends Expr
case class Number(name: Double) extends Expr
case class UnOp(operator: String, arg: Expr) extends Expr
case class BinOp(operator: String, left: Expr, right: Expr) extends Expr

Case classes

클래스명과 동일한 factory 메소드가 추가됨

1
2
val v = Var("x")
val op = BinOp("+", Number(1), v) 

클래스 매개변수 리스트의 모든 인자는 (암묵적으로) val prefix를 가지기 때문에 멤버변수가 됨.

1
2
v.name
op.left

컴파일러가 알아서 toString, hashCod, equals 를 구현함 (참고, Scala 에서는 == 가 equals 를 대신함)

1
2
println(op)
op.right == Var("x")

다른 복사본을 만들어주는 copy 메소드가 추가됨

1
op.copy(operator = "-") // BinOp(-, Number(1.0), Var(x))

Pattern Matching

패턴매칭을 이용한 몇 가지 산술식의 단순화 (예. 이중부정, 0 더하기, 1 곱하기)

1
2
3
4
5
6
def simplifyTop(expr: Expr) : Expr = expr match {
  case UnOp("-", UnOp("-", e)) =>  e  
  case BinOp("+", e, Number(0)) => e   
  case BinOp("*", e, Number(1)) => e
  case _ => expr
}

switch문과 유사한 match표현식

1
2
selector match { alternatives }
  switch (selector) { alternatives }
  • match 는 결과를 수반하는 표현식
  • alternative 표현식, 즉 패턴은 다음 case로 떨어지지(fall-though) 않음
  • 하나라도 매칭되지 않으면 MatchError 예외 발생.

기본 공백 case의 패턴 매칭 (Unit값을 반환, MatchError를 피하기 위한 꼼수)

1
2
3
4
5
expr match {
  case BinOp(op, left, right) =>
      println(expr + " is a binary operation")
  case _ =>
}

15.2 Kinds of patterns

Wildcard Patterns

이항연산의 요소와 무관한 와일드카드 패턴

1
2
3
4
expr match {
  case BinOp(_, _, _) => println(expr + " is a binary operation")
  case _ => println("It's something else")
}

Constant patterns

literal을 상수값으로 사용하는 패턴

1
2
3
4
5
6
7
def describe(x: Any) = x match {
  case 5 => "five"
  case true => "truth"
  case "hello" => "hi!"
  case Nil => "the empty list"
  case _ => "something else"
}

Variable patterns

임의의 변수를 이용하는 패턴 (와일드카드 패턴과 달리 Object에만 한정)

1
2
3
4
expr match {
  case 0 => "zero"
  case somethingElse => "not zero: " + somethingElse
}

변수와 상수의 구분: 소문자로 시작할 경우에만 패턴 변수로, 그렇지 않으면 상수로 취함

1
2
3
4
5
6
7
8
E match {
  case Pi => "strange math? Pi = " + Pi
  case _ => "OK"
} // OK에 매칭
  
E match {
  case pi => "strange math? Pi = " + Pi
} // pi에 매칭

pi는 변수이므로 모든 input과 매칭. 수식어를 붙여주거나 back-tick 문법을 사용

1
2
3
4
E match {
  case `pi` => "strange math? Pi = " + pi
  case _ => "OK"
} // OK에 매칭

Constructor patterns

생성자의 이름과 extra 패턴으로 생성자 매개변수를 확인. (deep match: 상위 레벨의 객체도 확인하고 이후 패턴에 대해 그 객체의 내용물도 확인하는 패턴)

1
2
3
4
expr match {
  case BinOp("+", e, Number(0)) => println("a deep match")
  case _ =>
}

Sequence patterns

고정 길이 매칭

1
2
3
4
expr match {
  case List(0, _, _) => println("found it")
  case _ =>
}

임의 길이 매칭

1
2
3
4
expr match {
  case List(0, _*) => println("found it")
  case _ =>
}

Tuple patterns

1
2
3
4
5
def tupleDemo(expr: Any) =
  expr match {
      case (a, b, c) => println("matched " + a + b + c)
      case _=>
  }

Typed patterns

타입 테스트/캐스팅을 위한 매칭

1
2
3
4
5
def generalSize(x: Any) = x match {
  case s: String => s.length      // generalSize("abc")
  case m: Map[_, _] => m.size     // generalSize(Map( 1 -> '2', 2 -> 'b'))
  case _ => -1                 // generalSize(math.Pi)
}

타입 테스트/캐스팅용 Any 클래스에 isInstanceOfasInstanceOf 메소드가 있으나 패턴매칭을 권장

타입 소거(type erasure) – Scala도 런타임시 타입 정보를 모름 (단, Array는 예외)

Variable binding

변수가 바인딩된 패턴 – 매칭되는 패턴이 변수로 반환됨

1
2
3
4
expr match {
  case UnOp("abs", e @ UnOp("abs", _)) => e // 변수 이름, @, 패턴
  case _ =>
}

15.3 Pattern guards

Scala에서는 패턴이 선형적(linear), 패턴의 변수는 패턴 내에 한번만 쓰임.

1
2
3
4
def simplifyAdd(e: Expr) = e match {
  case BinOp("+", x, x) => BinOp("*", x, Number(2))    // error: x is already defined as value x
  case _ => e
}

패턴 가드 (임의의 boolean 표현식), 가드 표현식이 참일 경우만 매칭됨

1
2
3
4
5
def simplifyAdd(e: Expr) = e match {
  case BinOp("+", x, y) if x == y =>
      BinOp("*", x, Number(2))
  case _ => e
}

15.4 Pattern overlaps

구체적인 패턴을 먼저 기술하고 일반적인 패턴을 기술하는 식으로 오버래핑

1
2
3
4
5
6
7
8
9
10
def simplifyTop(expr: Expr) : Expr = expr match {
  case UnOp("-", UnOp("-", e)) =>  e  
  case BinOp("+", e, Number(0)) => e   
  case BinOp("*", e, Number(1)) => e
  case UnOp(op, e) =>
      UnOp(op, simplifyAll(e))
  case BinOp(op, l, r) =>
      BinOp(op, simplifyAll(l), simplifyAll(r))
  case _ => expr
}

그렇지 않으면 컴파일 에러

1
2
3
4
def simplifyBad(expr: Expr): Expr = expr match {
  case UnOp(op, e) => UnOp(op, simplifyBad(e))
  case UnOp("-", UnOp("-", e)) => e           // error: unreachable code
}

15.5 Sealed classes

sealed 키워드로 부모 클래스를 봉인하면 미리 정의된 하위 case 클래스의 계층 구조를 부모 클래스의 기본 패턴 조합으로 설정.

봉인 – 런타임시 생성되는 하위 case 클래스의 추가를 막음 (단, 같은 파일에 있는 것들은 예외)

1
2
3
4
5
sealed abstract class Expr
  case class Var(name: String) extends Expr
  case class Number(name: Double) extends Expr
  case class UnOp(operator: String, arg: Expr) extends Expr
  case class BinOp(operator: String, left: Expr, right: Expr) extends Expr

기본 패턴 조합이 아닌 경우, 컴파일 경고가 발생 (match is not exhaustive!)

1
2
3
4
def describe(e: Expr): String = e match {
  case Number(_) => "a number"
  case Var(_) => "a variable"
}

컴파일 경고를 피할려면, RuntimeException을 던지거나 @unchecked 주석을 활용

15.6 The Option type

Option 타입 – Some(x) 형태이거나 혹은 None 값의 형태. Collection에 의해 생성

1
2
3
val capitals = Map("France" -> "Paris", "Japan" -> "Tokyo")
  capitals get "France"        // Some(Paris)
  capitals get "North Pole"    // None

일반적으로 패턴 매칭에서 선택적인 값을 가져올 때 사용

1
2
3
4
5
6
7
def show(x: Option[String]) = x match {
  case Some(s) => s
  case None => "?"
}

show(capitals get "Japan")            // Tokyo
show(capitals get "North Pole"       // ?

Java의 경우 HashMap은 값을 찾지 못하면 null을 반환하는데 오류에 취약함. Java에서는 null 확인을 잊어버려 NullPointerException이 매우 빈번하게 발생함.

Scala에서는 이런 게 안 통함, 왜냐하면 해쉬맵은 value 타입만 저장할 수 있으며 null은 value 타입으로 허용되지 않음.

15.7 Patterns everywhere

Patterns in variable definitions

단일 할당으로 다중 변수 정의

1
2
val myTuple = (123, "abc")       // myTuple: (Int, java.lang.String) = (123, abc)
val (number, string) = myTuple    // number: Int = 123, string: java.langString =  abc

case 클래스 작업에 유용 – 정확한 case 클래스를 알고 있을 경우, 패턴으로 해체할 수 있음.

1
2
val exp = new BinOp("*", Number(5), Number(1))
val BinOp(op, left, right) = exp    // op: String =  *, left: Expr = Number(5.0), right: Expr = (Number1.0)

Case sequences as partial functions

case 시퀀스는 함수 리터럴임 – 함수 리터럴을 쓰는 곳엔 case 시퀀스를 쓸 수 있음.

아래 함수는 두 가지 case로 구성.

1
2
3
4
5
6
7
val withDefault: Option[Int] => Int = {
  case Some(x) => x
  case None => 0
}

  withDefault(Some(10))   // Int = 10
  withDefault(None)      // Int = 0

전형적인 Actor 코드에도 유용

1
2
3
4
5
6
7
8
9
10
11
react {
  case (name: String, actor: Actor) => {
      actor ! getip(name)
      act()
  }
      
  case msg => {
      println("Unhandled message: " + msg)
      act()
  }
}

case 시퀀스는 partial 함수를 제공 – 아래 예제는 정수형 리스트의 두 번째 요소를 반환하는 partial 함수.

1
2
3
4
5
6
val second: List[Int] => Int = {
  case x :: y :: _ => y            // 컴파일시 Nil 조합이 없다는 warning
}
  
second(List(5, 6, 7))       // Int = 6
second(List())               // MatchError

PartialFunction 타입으로 다시 작성하면

1
2
3
val second: PartialFunction[List[Int],Int] = {
  case x :: y :: _ => y
}

Partial 함수는 여러 개의 entry point를 제공. 이를 위한 isDefinedAt 메소드가 있음 – 함수가 특정한 값(entry point)에 정의되어 있는지를 테스트함.

1
2
second.isDefinedAt(List(5, 6, 7)    // true
second.isDefinedAt(List())         // false

함수 리터럴 { case x :: y :: _=> y } 는 컴파일러에 의해 다음의 partial 함수로 변환됨.

1
2
3
4
5
6
7
8
9
10
new PartialFunction[List[Int], Int] {
  def apply(xs: List[Int]) = xs match {
      case x :: y :: _ => y
  }
      
  def isDefinedAt(xs: List[Int]) = xs match {
      case x :: y :: _ => true
      case _ => false
  }
}

가능한 완전한(complete) 함수로 작업. 왜냐하면 partial 함수를 사용하면 컴파일가 처리 못하는 런타임 에러를 허용하게 됨. 그러나 다룰 수 없는 값의 입력을 원치 않을 때 partial 함수를 사용하거나 혹은 partial 함수를 요구하는 프레임워크에서 그 함수를 호출하기 전에 isDefinedAt 메소드로 확인할 수 있음.

Patterns in for expressions

tuple 패턴으로 쓰인 for 표현식

1
for((country, city) <- capitals)

패턴과 매칭되는 리스트 요소 뽑아내기

1
2
val results = List(Some("apple"), None, Some("orange"))
for(Some(fruit) <- results) println(fruit) // apple orange

15.8 A Larger Example

x/(x+1)의 표현식을 다음과 같이 형태로 출력하는 ExprFormatter라는 클래스를 만들어 봄.

  x
-----
x + 1

수평 레이아웃의 경우, 아래와 같은 구조의 표현은 ( x + y ) * z + 1을 출력해야 함.

1
2
3
4
5
BinOp("+",
BinOp("*",
BinOp("+", Var("x"), Var("y")),
  Var("z")),
  Number(1))

레이아웃을 쉽게 읽으려면 중복 괄호는 제거해야함. 괄호의 어디에다 두어야할지 알기 위해선 우선순위를 알아야함. 우선순위는 map 리터럴로 다음과 같이 표현할 수 있음.

1
2
3
4
Map(
  "|" -> 0, "||" -> 0,
  "&" -> 1, "&&" -> 1, 
)

그러나 위 방법은 해당 부분에 대한 우선순위의 사전 계산이 많이 포함되므로, 그냥 상승하는 우선순위(increasing precedence)에 대한 연산자 그룹을 정의하고 이로부터 각 연산자의 우선순위를 계산하면 됨.

1
2
3
4
5
6
7
8
9
10
11
12
// 상승하는 우선순위의 그룹에 연산자를 저장함
  
private val opGroups =
  Array(
      Set("|", "||"),
      Set("&", "&&"),
      Set("^"),
      Set("==", "!="),
      Set("<", "<=", ">", ">="),
      Set("+", "-")
      Set("*", "%")
  )

precedence 변수는 연산자와 그들의 우선순위에 대한 맵으로 0부터 시작하는 정수값인데 두 개의 생성자로부터 생성됨.

1
2
3
4
5
6
7
8
private val precedence = {
  val assocs =
      for {
          i <- 0 until opGroups.length
          op <- opGroups(i)
      } yield op -> i
      Map() ++ assocs
  }

첫 번째 생성자는 onGroups 배열의 모든 인덱스 i를 만들고, 두 번째 생성자는 opGroups(i)의 모든 연산자 op를 만듦. for문의 각 연산자가 연산자 op와 그 인덱스 i의 조합을 만들기 때문에 배열에 있는 연산자들의 상대적 위치에 의해 그들의 우선순위가 결정됨.

단항 연산자의 우선순위는 모든 이항 연산자보다 높으므로 opGroup 배열의 길이로 설정함. (곱하기, 나누기보다 하나 높은 값임)

1
private val unaryPrecedence = opGroups.length

분수 연산자의 우선순위는 수직 레이아웃에 사용되므로 약간 다르게 다룸. 특별한 놈이니 -1을 할당함.

1
private val fractionPrecedence = -1

이제 format 메소드를 작성하겠음. format 메소드는 Expr 타입의 ee 표현을 에워싸는(enclosing) 연산자의 우선순위의 값 enclPrec 두 개의 인자가 있음. 이 메소드는 2차원 문자열 배열의 레이아웃 요소(elem)를 반환함.

stripDot은 보조 메소드임, private format 메소드는 표현를 만들기 위한 대부분의 작업을 함. 같은 이름의 마지막 메소드 format은 라이브러리 내 단독 public 메소드로써 처리할 표현을 넘겨받음.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
private def format(e: Expr, enclPrec: Int): Element =
  e match {
      case Var(name) =>
          elem(name)

      case Number(num) =>
          def stripDot(s: String) =
              if (s endsWith ".0")    s.substring(0, s.length - 2)
                  else s
              elem(stripDot(num.toString))
              
      case UnOp(op, arg) =>
          elem(op)    beside format (arg, unaryPrecedence)

      case BinOp("/", left, right) =>
          val top = format(left, fractionPrecedence)
          val bot = format(right, fractionPrecedence)
          val line = elem('-', top.width max bot.width, 1)
          val frac = top above line above bot
          if (enclPrec != fractionPrecedence) frac
              else elem(" ") beside frac beside elem(" ")
              
      case BinOp(op, left, right) = >
          val opPrec = precendecn(op)
          val l = format(left, opPrec)
          val r = format(right, opPrec + 1)
          val oper = l beside elem(" " + op + " ") beside r
          if (enclPrec <= opPrec) oper
              else elem("(") beside oper beside elem(")")
  }
          
  def format(e: Expr): Element = format(e, 0)
}

이하는 생략.

15.9 Conclusion

(생략)

The Mores

Copyright © 2014 - Patrick Yoon. Powered by Octopress