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 {
scala> simplifyTop(UnOp("-", UnOp("-", 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 {
scala> describe(5) |
< 변수 패턴 >
: 와일드 카드 패턴처럼 어떤 객체와도 매치된다. 다른 점은 변수에 객체를 바인딩해 표현식에 사용할 수 있다.
expr match { |
- 변수와 상수 비교
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 {
expr match { |
< 튜플 패턴 >
expr match { case (a, b, c) => println("matched "+ a + b + c) case _ => } |
< 타입 지정 패턴 >
: 타입 검사나 타입 변환을 간편하게 해준다.
def generalSize(x: Any) = x match {
scala> generalSize("abc") |
: 타입 지정 패턴 매치가 없다면? => 타입 지정 패턴을 통해 isInstanceOf와 asInstanceOf 두 연산을 동시에 하는 효과
if (x.isInstanceOf[String]) { // Any 타입 변수가 String 인스턴스인지 확인 val s = x.asInstanceOf[String] // String 타입으로 변환 s.length } else ... |
< 타입 소거 >
: 스칼라는 실행 시점에 타입 인자에 대한 정보를 유지하지 않는다.
def isIntIntMap(x: Any) = x match {
scala> isIntIntMap(Map(1 -> 1)) |
: 타입 소거로 인해 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 { |
" 패턴 가드 사용 " scala> def simplifyAdd(e: Expr) = e match { |
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 문이 모든 패턴을 다루는지 검사 생략 |
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 함수 타입
scala> withDefault(Some(10)) |
: case 나열은 부분 함수(함수가 처리할 수 있는 파라미터 범위가 부분적, 처리하지 않는 값을 파라미터로 전달 시 예외 발생)
val second: List[Int] => Int = { <console>:17: warning: match is not exhaustive! // 매치가 모든 경우를 포괄 X => 경고
scala> second(List(5, 6, 7))
|
=> 컴파일러에게 부분 함수를 가지고 작업한다는 사실을 알려 에러를 없앤다. => PartialFunction 부분 함수 타입 사용
val second: PartialFunction[List[Int],Int] = {
scala> second.isDefinedAt(List(5,6,7)) // PartialFunction에는 isDefinedAt 메소드가 있다. |
: isDefinedAt 메소드는 부분 함수가 어떤 값에 대해 결과 값을 정의하고 있는 지 알려준다.
: 부분 함수의 전형적인 예가 패턴 매치 함수 리터럴 => 스칼라 컴파일러는 PartialFunction 타입의 패턴 매치 함수 리터럴을 두 번 변환해 부분 함수로 만든다.(리터럴을 실제 함수 구현으로 변환, 해당 함수가 정의 됐는지 여부 검사)
// PartialFunction 타입 함수 리터럴 { case x :: y :: _ => y } 는 아래와 같이 변환 new PartialFunction[List[Int], Int] { |
* 함수 리터럴의 타입이 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, 모든 동작이 의도되서 처리해야 한다.
'스칼라' 카테고리의 다른 글
스칼라 17장 컬렉션(Programming in Scala, 3rd) (0) | 2019.06.06 |
---|---|
스칼라 16장 리스트(Programming in Scala, 3rd) (0) | 2019.06.02 |
스칼라 14장 단언문과 테스트(Programming in Scala, 3rd) (0) | 2019.06.02 |
스칼라 13장 패키지와 임포트(Programming in Scala, 3rd) (0) | 2019.06.01 |
스칼라 12장 트레이트(Programming in Scala, 3rd) (0) | 2019.06.01 |