-
[스칼라] 케이스 클래스와 패턴 매치스칼라 2022. 2. 21. 17:40
참고: Programming in scala 4th edition 15장
1. 산술표현식 예
산술표현식을 구현하는 예제를 가지고 케이스 클래스와 패턴 매치를 설명한다.
abstract class Expr case class Var(name: String) extends Expr case class Number(num: Double) extends Expr case class UnaryOperator(operator: String, arg: Expr) extends Expr case class BinaryOperator(operator: String, left: Expr, right: Expr) extends Expr
변수와 숫자 단항/이항 연산자를 정의한 클래스들이다.
케이스클래스
위의 클래스 선언에서 클래스 앞에 case 키워드가 붙어있다. 이런 클래스를 케이스 클래스라고 하는데 몇가지 편리한 기능이 추가된다.
- 클래스 이름과 같은 이름의 팩토리 메서드를 추가한다.
- 클래스 파라미터의 모든 인자에 val 접두사를 붙여 필드로 만든다.
- toString, hashCode, equals의 일반적인 구현을 추가한다.
- 일부 변경한 복사본을 생성하는 copy 메서드를 추가한다.
// 클래스 이름과 같은 팩토리 메서드가 추가됨 val X = Var("X") // 클래스 파라미터의 모든 인자가 필드가 됨 val binaryOperator = BinaryOperator("+", X, Number(1)) print(binaryOperator.operator) print(binaryOperator.left) print(binaryOperator.right) // toString, hashCode, equals의 일반적인 구현을 추가함 print(binaryOperator) print(binaryOperator.hashCode()) // 일부 변경한 복사본을 생성하는 copy 추가 val minus = binaryOperator.copy("-") print(minus)
패턴 매치
def simplifyTop(expr: Expr): Expr = expr match { case UnaryOperator("-", UnaryOperator("-", e)) => e case BinaryOperator("+", e, Number(0)) => e case BinaryOperator("*", e, Number(1)) => e case _ => expr } simplifyTop(UnaryOperator("-", UnaryOperator("-", Var("X")))) // Var("X") simplifyTop(BinaryOperator("*", Var("X"), Number(1))) // Var("X")
스칼라에서 패턴 매치는 (셀렉터) match { 대안 } 의 형식으로 구현된다. 패턴 매치는 표현식이므로 각 대안은 결과값을 리턴해야한다. match식은 순서대로 패턴을 하나씩 검사하고 가장 처음으로 매치된 패턴의 표현식을 실행한다. 사용가능한 패턴은 아래와 같다.
- 상수 패턴 (constant pattern): "+", 1 같은 상수 패턴은 == 연산자를 적용해 매치시킨다.
- 변수 패턴 (variable pattern): 모든 값과 매치되며 => 오른쪽의 표현식에서 그 변수를 사용할 수 있다.
- 와일드카드 패턴(wildcard pattern): 모든 값과 매치되지만 표현식에서 사용할 수 없다. 주로 디폴트 케이스에서 활용된다.
- 생성자 패턴(constructor pattern): UnaryOperator("-", e)와 같은 형태로 UnaryOperator 타입에 첫 인자가 "-"로 주어진 케이스이다. 변수는 e에 맵핑되어 표현식에서 사용할 수 있다.
switch와 match
스칼라의 match는 자바의 switch와 유사한 기능을 하지만 세 가지 차이점이 있다.
- 스칼라 match는 표현식이다.
- 스칼라의 match는 대안이 수행되면 break 같은게 없어도 다음 대안을 실행하지 않는다.
- 매치에 성공하지 못하면 MatchError 예외가 발생한다.
2. 패턴의 종류
와일드 카드 패턴
와일드 카드 패턴은 모든 값과 매치된다. 디폴트 매치를 처리할 때도 활용할 수 있고, 표현식에서 사용하지 않을 인자를 매치시킬 수도 있다.
BinaryOperator("+", Number(1), Number(2)) match { case BinaryOperator(_, _, _) => print("Binary Operator") case _ => print("others") } 출력: Binary Operator
상수 패턴
상수 패턴은 ==을 이용하여 매치된다.
// 상수 패턴 def constantPattern(x: Any) = x match { case 0 => "Zero" case true => "TRUE" case Nil => "Empty List" case _ => "Default" } print(constantPattern(0)) // Zero print(constantPattern(true)) // TRUE print(constantPattern(List())) // Empty List print("Default???") // Default???
변수 패턴
변수 패턴은 와일드 카드 패턴처럼 모든 값과 매치된다. 차이점은 변수로 할당되어 표현식에서 활용할 수 있다는 것이다.
val variable1 = "hi" def variablePattern(x: Any) = x match { case `variable1` => print("I`m hi") case variable1 => print(variable1) } variablePattern("hi") variablePattern("variable1") variablePattern("Variable Pattern")
스칼라 match 내에서 소문자로 시작하는 변수 이름은 기본적으로 표현식에서 활용되는 용도의 변수를 말하고, 상수는 맨 앞자리가 대문자로 시작한다. match 내에서 변수 값을 그대로 활용하고 싶다면 ``를 이용할 수도 있다.
생성자 패턴
BinaryOperator("+", Number(0), e) 와 같은 형태의 패턴이 생성자 패턴이다. 스칼라는 deep match 를 지원하기 때문에 생성자 안에 인자로 다른 생성자가 들어와도 문제 없다.
val binaryMatcher = (expr: Any) => expr match { case BinaryOperator("+", e, Number(0)) => println("a deep match") case _ => } binaryMatcher(BinaryOperator("+", Var("X"), Number(0)))
시퀀스 패턴
배열이나 리스트 같은 시퀀스 타입에 대해서도 매치를 활용할 수 있다.
아래는 길이가 3개인 리스트와 매치시키는 예제이다.
def sequenceMatcher(input: List[Int]) = { input match { case List(_, _, _) => print(s"length is ${input.length}") case _ => } } sequenceMatcher(List(1, 2, 3)) // length is 3 출력됨
_* 연산자를 사용하면 길이에 제약을 두지 않을 수 있다.
def sequenceMatcher(input: List[Int]) = { input match { case List(0, _*) => print(s"length is ${input.length}") case _ => } } sequenceMatcher(List(1, 2, 3)) // 0으로 시작하지 않아 매치되지 않음 sequenceMatcher(List(0, 2, 3, 4)) // length is 4
튜플 또한 위와 유사한 형태로 매치가 된다.
타입 지정 패턴 (typed pattern)
간단한 타입 검사나 타입 변환을 위해 타입 지정 패턴을 활용할 수 있다.
def typeMatcher(input: Any) = input match { case s: String => print("I`m String") case i: Int => print("I`m Integer") case m: Map[_, _] => print("I`m map") case _ => print("Default") } typeMatcher("ssss") typeMatcher(1234) typeMatcher(Var("Default")) typeMatcher(Map("a" -> "b", "b" -> "c"))
타입소거
자바와 마찬가지로 스칼라는 제네릭에서 타입소거를 사용한다. 그래서 실행 시점에 타입 인자에 대한 정보를 유지하지 않는다. 따라서 아래와 같은 매치 코드는 의도대로 동작되지 않는다.
def typeMatcher(input: Any) = input match { case m: Map[Int, Int] => print("I`m map") case _ => print("Default") } typeMatcher(Map(1 -> 1, 2 -> 2)) // I`m map typeMatcher(Map("a" -> 1, "b" -> 2)) // I`m map
키, 밸류가 Int, Int인 형태의 Map만 매치되길 바랬겠지만 타입소거때문에 그렇게 동작하지 않는다.
예외적으로 배열만 타입을 녹여 매칭시킬 수 있다.
변수바인딩
타입 매치에 활용된 식을 변수로 바인딩 시켜두고 표현식에서 활용할 수도 있다.
def variableBinding(input: Any) = input match { case UnaryOperator("abs", e @ UnaryOperator("abs", _)) => e case _ => input } println(variableBinding(UnaryOperator("abs", UnaryOperator("abs", 10))))
패턴 가드
패턴 가드는 패턴 매치에서 대안의 조건문에서 활용된 변수에 특정 조건을 추가해주는 것이다.
def patternGuard(input: Any) = input match { case BinaryOperator("+", x, y) if x == y => BinaryOperator("*", x, Number(2)) case _ => input }
case 이후 if 조건문을 추가하면 해당 조건에 해당하는 case만 대안에서 선택되도록 할 수 있다.
3. 봉인된 클래스(sealed class)
어떤 클래스를 봉인된 클래스로 정의하면 그 클래스의 서브 클래스는 같은 파일에 모두 정의되어야한다. 또한 봉인된 클래스를 상속한 케이스 클래스에 대해서 패턴 매치를 시도하면 놓친 케이스에 대해서 컴파일러가 경고를 발생시킨다.
sealed abstract class Expr case class Var(name: String) extends Expr case class Number(num: Double) extends Expr case class UnaryOperator(operator: String, arg: Expr) extends Expr case class BinaryOperator(operator: String, left: Expr, right: Expr) extends Expr def sealedClassMatch(input: Expr) = input match { case Var(_) => input case Number(_) => input }
4. Option 타입
스칼라는 Option이라는 표준 타입이 있다. Option은 값이 존재하면 Some(값) 값이 존재하지 않으면 None을 반환한다.
스칼라 Map에서 get을 이용해 원소를 꺼낼 경우 Some을 이용하여 값을 반환한다.
val m = Map[Int, String](1 -> "String") m.get(1) def getOption(x: Option[String]) = x match { case Some(x) => x case None => "?" }
Option 타입을 분리해 낼때 패턴매치를 가장 일반적으로 사용한다.
5. 다양한 패턴
변수 정의에서 패턴 사용하기
val exp = BinaryOperator("*", Number(5), Number(1)) val BinaryOperator(op, left, right) = exp print(op, left, right)
case를 나열하여 부분 함수 만들기
case를 나열한 표현식은 부분함수로 구현될 수도 있다.
val someWithDefault: Option[Int] => Int = { case Some(x) => x case None => 0 } print(someWithDefault(Some(10))) // 10 print(someWithDefault(None)) // None
위의 예제에서 someWithDefault는 case의 나열로 이루어진 함수로 Option[Int] 타입을 인자로 받아 Int를 반환한다.
부분함수는 그 함수에서 처리되지 않는 값을 전달해서 호출하면 실행시점에 예외가 발생한다.
val getSecondElement: List[Int] => Int = { case x :: y :: _ => y } print(getSecondElement(List(1, 2, 3))) // 2 print(getSecondElement(List())) // MatchError
컴파일러에게 부분함수로 작업한다는 것을 명시해주면 부분함수로 좀 더 꼼꼼한 작업을 할 수 있다. 컴파일러에게 부분함수를 사용한다고 명시해 주는 방법은 PartialFunction[List[Int], Int]와 같이 타입을 명시해주는 것이다.
val getSecond: PartialFunction[List[Int], Int] = { case x:: y :: _ => y } getSecond.isDefinedAt(List(1,2,3)) getSecond.isDefinedAt(List()) if(getSecond.isDefinedAt(List(1, 2, 3))) print(getSecond.apply(List(1, 2, 3))) if(getSecond.isDefinedAt(List())) print(getSecond.apply(List()))
위와 같은 패턴 매치 함수 리터럴은 부분 함수의 전형적인 예시이다. 스칼라 컴파일러는 이 표현식을 변환해서 부분 함수로 만든다. 어떤 함수 리터럴의 타입이 PartialFunction이면 스칼라는 부분함숫값으로 변환시킨다. 위의 함수 리터럴은 아래 부분 함숫값과 같이 변환된다.
new PartialFunction[List[Int], Int] { override def apply(v1: List[Int]): Int = v1 match { case x:: y :: _ => y } override def isDefinedAt(x: List[Int]) = x match { case x :: y :: _ => true case _ => false } }
for 표현식에서 패턴 사용하기
for 표현식에서도 패턴을 활용할 수 있다.
val capitals = List(("Korea", "Seoul"), ("France", "Paris")) for((country, capital) <- capitals) { println(s"Country: ${country} Capital: ${capital}") } // 출력 결과 Country: Korea Capital: Seoul Country: France Capital: Paris
반복문안에서 생성되는 값이 정확히 매치되지 않아도 사용할 수 있는데, 반복문의 생성값 중 패턴과 일치하지 않는 값들은 버린다.
val capitals = List(("Korea", "Seoul"), ("France", "Paris"), None, ("SomeCountry", "CapitalA", "CapitalB")) for((country, capital) <- capitals) { println(s"Country: ${country} Capital: ${capital}") } // 출력 결과 Country: Korea Capital: Seoul Country: France Capital: Paris
'스칼라' 카테고리의 다른 글
[스칼라] 컬렉션 (0) 2022.02.23 [스칼라] 리스트 (0) 2022.02.22 [스칼라] 트레이트 (0) 2022.02.20 [스칼라] 스칼라 계층구조 (0) 2022.02.13 [스칼라] 상속과 구성 (0) 2022.02.03