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를 가지기 때문에 멤버변수가 됨.
컴파일러가 알아서 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
클래스에 isInstanceOf
와 asInstanceOf
메소드가 있으나 패턴매칭을 권장
타입 소거(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 타입의 e
와 e
표현을 에워싸는(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