ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [스칼라] 케이스 클래스와 패턴 매치
    스칼라 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

    댓글

Designed by Tistory.