15.1 간단한 예

* 변수, 숫자, 단항/이항 연산자로 이뤄진 산술식을 다루는 라이브러리

  abstract class Expr
  case class Var(name: String) extends Expr
  case class Number(num: Double) extends Expr
  case class UnOp(operator: String, arg: Expr) extends Expr
  case class BinOp(operator: String, left: Expr, right: Expr) extends Expr

< 케이스 클래스 > => DDD의 Value Object를 만들 때 유용(immutable 객체를 만들 때 좋다.)

: case 수식자가 붙은 클래스

: 케이스 클래스는 여러 가지 기능 제공

1. 컴파일러는 클래스 이름과 같은 이름의 팩토리 메소드를 추가한다.

scala> val v = Var("x") 

v: Var = Var(x)

 

scala> val op = BinOp("+", Number(1), v)    // 팩토리 메소드는 중첩해서 객체를 생성할 때 좋다.

op: BinOp = BinOp(+,Number(1.0),Var(x))

 

2. 케이스 클래스 주 생성자 내 파라미터에 있는 모든 인자에 암시적으로 val 접두사를 붙인다. => 각 파라미터가 클래스의 필드가 된다.

 

3. 컴파일러는 케이스 클래스에 toString, hashCode, equals 메소드의 일반적인 구현을 추가한다. ( 모든 인자를 기반으로 구현)

scala> println(op) 

BinOp(+,Number(1.0),Var(x)) 

 

scala> op.right == Var("x") 

res3: Boolean = true

4. 어떤 케이스 클래스의 일부를 변경한 복사본을 생성하는 copy 메소드를 추가한다.

=> 기존 인스턴스에서 하나 이상의 속성을 바꾼 새로운 인스턴스를 생성 가능(디폴트 파라미터와 이름 붙은 파라미터를 활용)

: 이름 붙은 파라미터를 활용해 변경하고 싶은 인자를 명시하여 해당 속성을 바꾼 인스턴스 생성(나머지 인자는 원본 객체 값 활용)

scala> op.copy(operator = "-") 

res4: BinOp = BinOp(-,Number(1.0),Var(x))

5. 패턴 매치를 지원해준다.

 

< 패턴 매치 > => 함수 단순화

 def simplifyTop(expr: Expr): Expr = expr match {
    case UnOp("-", UnOp("-", e))  => e   // Double negation
    case BinOp("+", e, Number(0)) => e   // Adding zero
    case BinOp("*", e, Number(1)) => e   // Multiplying by one
    case _ => expr
  }

 

scala> simplifyTop(UnOp("-", UnOp("-", Var("x"))))
res4: Expr = Var(x)

: expr 셀렉터가 각 case문(대안식)의 => 왼쪽 패턴과 매치되는 지 확인하고 매치되면 => 오른쪽 표현식을 수행한다.

 

- switch와 match의 비교

: 스칼라의 match는 결과 값을 내놓는다.

: 패턴 매치가 성공하면 다음 케이스로 빠지지 않는다.

: 전체 case문에 대해 패턴 매치가 실패하면 MatchError 예외가 발생한다. => 디폴트 케이스를 추가해야 한다.

  expr match {                    // Unit "()" 값 반환
    case BinOp(op, left, right) =>
      println(expr +" is a binary operation")
    case _ =>              // 디폴트 케이스 => MatchError 예외 발생 X
  }

 

15.2 패턴의 종류

< 와일드카드 패턴 >

: 어떤 객체라도 매치

: 주로 디폴트 케이스로 사용

 expr match {
    case BinOp(_, _, _) => println(expr +" is a binary operation")     // BinOp 인자로 무엇이 오든 지 상관 없음
    case _ => println("It's something else")
  }

 

< 상수 패턴 >

: 자신과 똑같은 값과 매치

: 리터럴, val(첫 글자가 대문자, 대문자가 아니면 변수 패턴 적용), 싱글톤 객체에 대한 매치

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

 

scala> describe(5)
res6: java.lang.String = five

scala> describe(true)
res7: java.lang.String = truth

scala> describe("hello")
res8: java.lang.String = hi!

scala> describe(Nil)             // Nil 싱글톤 객체
res9: java.lang.String = the empty list

scala> describe(List(1,2,3))
res10: java.lang.String = something else

 

< 변수 패턴 >

: 와일드 카드 패턴처럼 어떤 객체와도 매치된다. 다른 점은 변수에 객체를 바인딩해 표현식에 사용할 수 있다.

 expr match {
    case 0 => "zero"
    case somethingElse => "not zero: "+ somethingElse
  }

- 변수와 상수 비교 

scala> import math.{E, Pi}
import math.{E, Pi}

scala> E match {
     |   case Pi => "strange math? Pi = "+ Pi
     |   case _ => "OK"
     | }
res11: java.lang.String= OK

: 소문자로 시작하는 패턴은 패턴 변수로 취급하고 나머지 모든 패턴은 상수 패턴으로 간주한다.(단, val)

scala> val pi = math.Pi
pi: Double = 3.141592653589793

scala> E match {
     |   case pi => "strange math? Pi = "+ pi
     | }
res12: java.lang.String= strange math? Pi = 2.718281828459045

: 굳이 소문자를 상수 패턴으로 지정하고 싶다면

=> 상수가 어떤 객체의 필드인 경우 지정자를 앞에 붙인다.(Ex. this.pi or obj.pi)

=> 위 방법을 사용하지 못 하면 역따옴표를 사용해 변수 이름을 감싼다.

scala> E match {
     |   case `pi` => "strange math? Pi = "+ pi
     |   case _ => "OK"
     | }
res14: java.lang.String= OK

 

< 생성자 패턴 >

: 객체가 해당 케이스 클래스 타입인지 검사하고 객체의 생성자가 인자로 전달받은 값들이 패턴 안의 인자와 매치되는 지 검사한다.

: 깊은 매치를 지원 => 패턴 내부에 생성자 패턴이 들어있을 때도 검사 가능

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

 

< 시퀀스 패턴 >

: 배열, 리스트 같은 시퀀스 타입에 대한 패턴 매치 가능

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

 

expr match {
  case List(0, _*) => println("found it")     // 패턴의 마지막 원소를 _*로 표시하면 길이 제한 없음
  case _ =>
}

 

< 튜플 패턴 >

expr match {
  case (a, b, c)  =>  println("matched "+ a + b + c)
  case _ =>
}

 

< 타입 지정 패턴 >

: 타입 검사나 타입 변환을 간편하게 해준다.

def generalSize(x: Any) = x match {
  case s: String => s.length                    // x가 String 타입인지(null은 매치 X)
  case m: Map[_, _] => m.size                 // 키와 값 타입 관계 없이 Map 타입에 패턴 매치('_' 대신 변수 패턴 사용 가능) 
  case _ => -1
}

 

scala> generalSize("abc")
res16: Int = 3

scala> generalSize(Map(1 -> 'a', 2 -> 'b'))
res17: Int = 2

scala> generalSize(math.Pi)
res18: Int = -1

: 타입 지정 패턴 매치가 없다면? => 타입 지정 패턴을 통해 isInstanceOf와 asInstanceOf 두 연산을 동시에 하는 효과

if (x.isInstanceOf[String]) {              // Any 타입 변수가 String 인스턴스인지 확인
  val s = x.asInstanceOf[String]        // String 타입으로 변환
  s.length
} else ...

 

< 타입 소거 >

: 스칼라는 실행 시점에 타입 인자에 대한 정보를 유지하지 않는다.

def isIntIntMap(x: Any) = x match {
   case m: Map[Int, Int] => true
   case _ => false
}

 

scala> isIntIntMap(Map(1 -> 1))
res19: Boolean = true

scala> isIntIntMap(Map("abc" -> "abc"))
res20: Boolean = true

: 타입 소거로 인해 Map의 Key와 Value 타입에 대해 패턴 매치가 이루어지지 않는다.

* 타입 소거의 예외는 배열 => 배열은 원소 타입에 대해 패턴 매치 가능

scala> def isStringArray(x: Any) = x match {
     |   case a: Array[String] => "yes"
     |   case _ => "no"
     | }
isStringArray: (x: Any) java.lang.String

scala> isStringArray(Array("abc"))
res21: java.lang.String = yes

scala> val ai = Array(1, 2, 3)
ai: Array[Int] = Array(1, 2, 3)

scala> isStringArray(ai)
res22: java.lang.String = no

 

< 변수 바인딩 >

: 패턴 내부 변수에 다른 패턴을 추가할 수 있다. 변수 이름 다음에 @기호를 넣고 패틴을 쓴다.

expr match {
  case UnOp("abs", e @ UnOp("abs", _)) => e
  case _ =>
}

: 패턴 매치에 성공하면 e 변수에 UnOp("abs", _))가 들어간다.

 

15.3 패턴 가드

: 어떤 패턴 변수가 한 패턴 안에 오직 한 번만 나와야 한다.(선형 패턴)

: 패턴 가드(패턴 뒤에 if로 시작, 가드가 true가 될 때만 매치에 성공)를 통해 이를 피할 수 있다.

" e + e => e * 2 "

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

" 패턴 가드 사용 "

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

 

15.4 패턴 겹침

: 모든 경우를 처리하는 case 문이 더 구체적인 규칙 다음에 와야 한다. 다음 case 문에서 매치될 것까지 현재 케이스 문에서 매치하면 컴파일 경고가 발생한다.

 scala> def simplifyBad(expr: Expr): Expr = expr match {
             case UnOp(op, e) => UnOp(op, simplifyBad(e))
             case UnOp("-", UnOp("-", e)) => e
         }
:18: error: unreachable code
           case UnOp("-", UnOp("-", e)) => e

 

15.5 봉인된 클래스

: 패턴 매치에 사용되는 클래스들을 하나의 파일 안에 정의하면 컴파일러에서 가능한 패턴 조합을 찾아내 match 식에 놓친 패턴 조합이 있는 지 알려준다. => 다른 파일에서 케이스 클래스의 서브 클래스를 정의 못 하도록 막는다.(봉인된 클래스)

: 봉인된 클래스는 클래스 앞에 sealed 키워드를 넣으면 된다.

: 패턴 매치를 위한 클래스 계층을 작성한다면 그 계층에 속한 클래스를 봉인하는 것을 추천

sealed abstract class Expr
case class Var(name: String) extends Expr
case class Number(num: Double) extends Expr
case class UnOp(operator: String, arg: Expr) extends Expr
case class BinOp(operator: String, left: Expr, right: Expr) extends Expr

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

warning: match is not exhaustive!      // 가능한 패턴을 처리하지 않기 때문에 경고 발생
missing combination           UnOp
missing combination          BinOp

: 위 같은 경고가 필요 없을 때(처리하지 않은 가능한 패턴이 전혀 발생하지 않을 때)

def describe(e: Expr): String = e match {
  case Number(_) => "a number"
  case Var(_) => "a variable"
  case _ => throw new RuntimeException // 결코 실행되지 않을 코드를 추가 => 쓸모 X => 애노테이션 활용
}

def describe(e: Expr): String = 

   (e: @unchecked) match {    // @unchecked : 컴파일러는 그 match 문의 case 문이 모든 패턴을 다루는지 검사 생략
      case Number(_) => "a number"
      case Var(_)    => "a variable"
}

 

15.6 Option 타입

: 선택적인 값을 표현하며, x가 실제 값이라면 Some(x) 형태로 값이 있음을 표현하고 값이 없으면 None 객체가 된다.

Ex. Map의 get 메소드 : 키에 대응하는 값이 있으면 Some 반환, 그 키가 없다면 None 반환

scala> val capitals = Map("France" -> "Paris", "Japan" -> "Tokyo")
capitals: scala.collection.immutable.Map[java.lang.String,
java.lang.String] = Map(France -> Paris, Japan -> Tokyo)

scala> capitals get "France"
res23: Option[java.lang.String] = Some(Paris)

scala> capitals get "North Pole"
res24: Option[java.lang.String] = None

: 옵션 값을 분리해내는 가장 일반적인 방법이 패턴 매치

scala> def show(x: Option[String]) = x match {
         case Some(s) => s
         case None => "?"
       }
show: (x: Option[String])String

scala> show(capitals get "Japan")
res25: String = Tokyo

scala> show(capitals get "France")
res26: String = Paris

scala> show(capitals get "North Pole")
res27: String = ?

: 자바 HashMap의 get은 값이 없으면 null을 반환 => null이 될 수 있는 지 추적, null을 허용했다면 null 여부 검사

: null이 될 수 있는 선택적인 값은 Option 사용 권장 => null이 될 수도 있다고 명확하게 드러내 준다. 

: 어떤 변수가 Option[String] 타입이라면, 그 변수를 String으로 컴파일할 수 없다.

 

15.7 패턴은 어디에나

< 변수 정의에서 패턴 사용 >

: var, val을 정의할 때 식별자 대신 패턴 사용 가능

scala> val myTuple = (123, "abc")
myTuple: (Int, java.lang.String) = (123,abc)

scala> val (number, string) = myTuple
number: Int = 123
string: java.lang.String = abc
scala> val exp = new BinOp("*", Number(5), Number(1))
exp: BinOp = BinOp(*,Number(5.0),Number(1.0))

scala> val BinOp(op, left, right) = exp
op: String = *
left: Expr = Number(5.0)
right: Expr = Number(1.0)

 

< case를 나열해서 부분 함수 만들기 >

: case 문의 나열은 함수 리터럴, 진입점이 여러 개인 일반적인 메소드(여러 종류 인자를 받을 수 있다. ==오버로드 메소드를 여러 개 정의한 효과)

val withDefault: Option[Int] => Int = {    // Option[Int] => Int 함수 타입 
case Some(x) => x
case None => 0
}

 

scala> withDefault(Some(10)) 
res28: Int = 10 

scala> withDefault(None) 
res29: Int = 0

: case 나열은 부분 함수(함수가 처리할 수 있는 파라미터 범위가 부분적, 처리하지 않는 값을 파라미터로 전달 시 예외 발생)

val second: List[Int] => Int = {
case x :: y :: _ => y 
}

<console>:17: warning: match is not exhaustive!     // 매치가 모든 경우를 포괄 X => 경고
missing combination Nil

 

scala> second(List(5, 6, 7)) 
res24: Int = 6 


scala> second(List())            // 처리할 수 없는 파라미터 전달 시 에러
scala.MatchError: List() 
     at $anonfun$1.apply(<console>:17) 
     at $anonfun$1.apply(<console>:17)

=> 컴파일러에게 부분 함수를 가지고 작업한다는 사실을 알려 에러를 없앤다. => PartialFunction 부분 함수 타입 사용

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

 

scala> second.isDefinedAt(List(5,6,7))     // PartialFunction에는 isDefinedAt 메소드가 있다.  
res30: Boolean = true                             

scala> second.isDefinedAt(List()) 
res31: Boolean = false

: isDefinedAt 메소드는 부분 함수가 어떤 값에 대해 결과 값을 정의하고 있는 지 알려준다.

: 부분 함수의 전형적인 예가 패턴 매치 함수 리터럴 => 스칼라 컴파일러는 PartialFunction 타입의 패턴 매치 함수 리터럴을 두 번 변환해 부분 함수로 만든다.(리터럴을 실제 함수 구현으로 변환, 해당 함수가 정의 됐는지 여부 검사)

// PartialFunction 타입 함수 리터럴 { case x :: y :: _ => y } 는 아래와 같이 변환

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 
    } 
}

* 함수 리터럴의 타입이 PartialFunction이 아닌 경우, Function1이거나 타입 표기가 없으면 함수 리터럴은 완전한 함수라고 한다. 

* 부분 함수는 실행 시점에 오류가 발생할 수 있기 때문에 완전한 함수를 사용하는 게 좋다. 부분 함수를 사용해야 한다면 부분 함수가 처리할 수 없는 값을 넘기는 일이 확실히 없도록 하거나, 함수 호출 전 isDefinedAt 메소드로 호출 시 문제가 없는 지 검사하도록 한다.(후자의 예가 akka actors 라이브러리의 receive)

 

 

< for 표현식에서 패턴 사용하기 >

scala> for ((country, city) <- capitals)
         println("The capital of "+ country +" is "+ city)
The capital of France is Paris
The capital of Japan is Tokyo
scala> val results = List(Some("apple"), None, Some("orange"))
results: List[Option[java.lang.String]] = List(Some(apple), 
None, Some(orange))

scala> for (Some(fruit) <- results) println(fruit)
apple
orange

 

* 스칼라는 Exception을 권장 X, 모든 동작이 의도되서 처리해야 한다.

+ Recent posts