도메인 특화 언어에 산술 표현식을 다루는 라이브러리가 필요하다고 가정해보다
예시의 첫단계는 입력 데이터를 정의하는 것
문제를 간단히 하기 위해 변수와 숫자, 단항/이항 연산자로 이뤄진 산술식
abstract class Expr
case class Var (name: String) extends Expr
case class Number (name: Double) extends Expr
case class UnOp (operatot: String, arg : Expr) extends Expr
case class BinOp (operatot: String, left: Expr, right: Expr) extends Expr
class 선언 앞에 case가 붙어있으면 케이스 클래스라고 부른다.
scala> val v = Var("x")
v: Var = Var(x)
케이스 클래스의 파라미터 목록에 있는 모든 인자에 암시적으로 val 접두사를 붙인다. 각 파라미터가 클래스의 필드도 된다.
스칼라 컴파일러는 케이스 클래스에 toString, hashCode, equals 메소드의 일반적인 구현을 추가
스칼라 컴파일러는 케이스 클래스에서 일부를 변경한 복사본을 생성하는 copy 메소드를 추가
// 패턴 매치를 사용하는 simplifyTop 함수
def simplifyTop(expr: Expr) : Expr = expr match {
case UnOp("-", UnOp("-", e)) => e // 부호를 두 번 반전
case BinOp("+", e, Number(0)) => e // 0을 더함
case BinOp("*", e, Number(1)) => e // 1을 곱함
}
def simplifyTop(expr: Expr): Expr
scala> simplifyTop(UnOp("-", UnOp("-", Var("x"))))
val res0: Expr = Var(x)
셀렉터 match { 대안들 }
switch (셀렉터) { 대안들 }
(_)
를 활용하면 모든 값 매치 가능case _ => // default case
expr match {
case BinOp(_, _, _) => println(expr + " is a binary operation")
case _ => println("It's something else")
}
(_)
어떤 객체라도 매치 가능x match {
case 5 => "five"
case true => "truth"
case "hello" => "hi!"
case Nil => "the empty list"
case _ => "something else"
}
// 변수 패턴을 사용한 패턴 매치
expr match {
case 0 => "zero"
case somethingElse => "not zero: " + somethingElse
}
(_)
가 아닌 변수에 객체를 바인딩한다.상수 패턴이 기호로 이뤄진 이름일 수도 있다. ex) Nil
scala> import math.{E, Pi}
import math.{E, Pi}
scala> E match {
case Pi => "strange math? Pi = " + Pi
case _ => "OK"
}
res11: String = OK
컴파일러는 Pi가 셀럭터와 매치시킬 변수가 아니라, scala.math 로 부터 import 한 상수인지 알 수 있을 까?
소문자로 시작하는 간단한 이름은 패턴 변수로 취급하고 다른 모든 참조는 상수로 간주한다.
scala> val pi = math.Pi
pi: Double = 3.141592653589793
scala> E match {
case pi => "strange math? Pi = " + pi
}
res12: String = strange math? Pi = 2.718281828459045
※ 소문자 이름을 상수 패턴으로 사용하고 싶으면 두 가지 방법
상수가 어떤 객체의 필드인 경우에는 지정자(qualifier)를 앞에 붙일 수 있다.
역따옴표(`)
를 사용한다.
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 _ =>
}
val x : Any = ...
x match {
case s: String => s.length
case m: Map[_, _] => m.size
case _ => -1
}
Map[_,_]
은 와일드 카드를 활용한 타입 지정 패턴// (@ 기호를 사용한) 변수 바인딩이 있는 패턴
expr match {
case UnOp("abs", e @ UnOp("abs", _)) => e
case _ =>
}
UnOp("abs", _))
구문에서 생성된 객체가 할당def optionsPatternMatching(option: Option[String]): String = {
option match {
case Some(value) => s"I'm not an empty option. Value $value"
case None => "I'm an empty option"
}
}
def regexPatterns(toMatch: String): String = {
val numeric = """([0-9]+)""".r
val alphabetic = """([a-zA-Z]+)""".r
val alphanumeric = """([a-zA-Z0-9]+)""".r
toMatch match {
case numeric(value) => s"I'm a numeric with value $value"
case alphabetic(value) => s"I'm an alphabetic with value $value"
case alphanumeric(value) => s"I'm an alphanumeric with value $value"
case _ => s"I contain other characters than alphanumerics. My value $toMatch"
}
}
문법적인 패턴 매치만으로 case에 대한 구분이 부족할 때, 패턴 가드를 사용
case 문을 좀 더 구체화하고 명확하게 하기 위한 boolean 표현식 이라고 생각
// 패턴 가드가 있는 match 표현식
scala> def simplifyAdd(e: Expr) = e match {
case BinOp("+", x, y) if x == y =>
BinOp("*", x, Number(2))
case _ => e
}
simplifyAdd: (e: Expr)Expr
패턴 매치는 코드에 있는 순서를 따른다. 그래서 case 문에 대한 순서가 굉장히 중요하다.
아래 예시를 보면 알 수 있다.
// case의 순서가 중요함을 보여주는 match 표현식
def simplifyAll(expr: Expr): Expr = expr match {
case UnOp("-", UnOp("-", e)) => simplifyAll (e) // Double negation
case BinOp("+", e, Number(0)) => simplifyAll (e) // Adding zero
case BinOp("*", e, Number(1)) => simplifyAll (e) // Multiplying by one
case UnOp(op, e) => UnOp(op, simplifyAll(e))
case BinOp(op, 1, r) => BinOp(op, simplifyAll(1), simplifyAll(r))
case _ => expr
}
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
}
simplifyAll 함수는 이전에 정의된 simmplifyTop과 달리 산술식의 최상위 위치뿐 아니라 식의 모든 곳에 간소화 규칙을 적용
simplilfyTop 함수에서 2가지의 케이스를 추가하였다.
case UnOp(op, e) => UnOp(op, simplifyAll(e))
모든 단항 연산과 매치
case BinOp(op, 1, r) => BinOp(op, simplifyAll(1), simplifyAll(r))
모든 이항 연산과 매치
마지막으로 모든 경우를 처리하는 case 문이 더 구체적인 규칙 다음에 온다는 점이 중요
만약 순서를 바꾸게 되면 구체적인 매치는 시도 자체를 하지 않을 것
scala> def simplifyBad(expr: Expr): Expr = expr match {
case UnOp(op, e) => UnOp(op, simplifyBad(e))
case UnOp("-", UnOp("-", e)) => e
}
<console>:21: warning: unreachable code
case UnOp("-", UnOp("-", e)) => e
^
simplifyBad: (expr: Expr)Expr
패턴 매치를 작성할 때, 모든 경우를 다 다뤘는 지 확인할 필요가 있다. 그래서 default 케이스를 추가해서 할 수 도 있지만, 이는 합리적인 디폴트 케이스가 있을 때만 가능 -> 만약 디폴트 동작이 없다면 ??
아무곳에서 케이스 클래스를 정의할 수 있기 때문에, 컴파일러에게 놓친 패턴 조합을 찾아달라고 요청할 수 없음...
패턴 매치를 작성할 때 match 식에서 놓친 패턴 조합을 찾을 수 있는 방법은 sealed 키워드를 활용
즉, 패턴 매치에서 셀렉터에 대한 타입을 sealed 클래스로 정의하고 매치를 하는 클래스는 모두 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
// 셀렉터에 @unchecked 어노테이션을 추가
// 컴파일러는 그 match 문의 case 문이 모든 패턴을 다 다루는지 검사하는 일을 생략
def describe(e: Expr): String = (e: @unchecked) match {
case Number(_) => "a number"
case Var(_) => "a variable"
}
scala> val capitals = Map("France"->"Paris", "Japan"->"Tokyo")
capitals: scala.collection.immutable.Map[String,String] = Map(France -> Paris, Japan -> Tokyo)
scala> capitals get "France"
res23: Option[String] = Some(Paris)
scala> capitals get "North Pole"
res24: Option[String] = None
Option[String] 타입의 변수가 null이 될 수도 있는 String 타입의 변수보다 선택적인 String이라는 사실을 더 명확하게 드러내준다.
독립적인 match 표현식뿐 아니라, 스칼라의 여러 곳에서 패턴을 사용할 수 있다.
val이나 var 정의할 때, 단순 식별자 대신 패턴 사용 가능
scala> val myTuple = (123, "abc")
myTuple: (Int, String) = (123,abc)
scala> val (number, string) = myTuple
number: Int = 123
string: String = abc
함수 리터럴이 쓰일 수 있는 곳이라면, 중괄호 사이에 case를 나열한 표현식 작성 가능
본질적으로 case의 나열도 함수의 리터럴 값
val withDefault: Option[Int] => Int = {
case Some(x) => x
case None => 0
}
아래 예시는 아카 엑터(Akka actors) 라이브러리에서 사용하는 receive 메소드
var sum = 0
// case의 나열은 부분 함수 이다.
def receive = {
case Data(byte) =>
sum += byte
case GetChecksum(requester) =>
val checksum = ~(sum & OxFF) + 1
requester ! checksum
}
scala> val second: List[Int] => Int = {
| case x :: y :: _ => y
| }
^
warning: match may not be exhaustive.
It would fail on the following inputs: List(_), Nil
val second: List[Int] => Int = $Lambda$1061/638249818@4f6e663b
scala> second(List(5,6,7))
val res3: Int = 6
scala> second(List())
scala.MatchError: List() (of class scala.collection.immutable.Nil$)
at $anonfun$second$1(<console>:1)
at $anonfun$second$1$adapted(<console>:1)
... 32 elided
처리하지 않는 값을 전달하여 예외가 발생시에 처리 방법
예외를 던져서 호출자에게 전달한다.
스칼라의 Option이나 Either 등을 사용하여 정상적인 값과 그렇지 않은 값을 처리할 수 있다.
부분 함수를 사용하여 처리하지 않는 값에 대하여는 작업을 하지 않도록 할 수 있다.
여기선, 처리하지 않는 값을 다루기 위해서 부분 함수를 포함하는 타입인 PartitionFunction을 이용한다.
* PartitionFunction 클래스는 isDefinedAt 함수를 사용함으로써 파라미터가 case 구문에 속해있는지 먼저 확인을 할 수 있다.
* 아래 예시에서 second는 PartitionFunction 타입의 객체로서 정수형 리스트를 받아서 정수 값을 반환한다.
scala> val second : PartialFunction[List[Int], Int] = {
| case x :: y :: _ => y
| }
val second: PartialFunction[List[Int],Int] = <function1>
scala> second.isDefinedAt(List(5,6,7))
val res5: Boolean = true
scala> second.isDefinedAt(List())
val res6: Boolean = false
scala> second(List(5,6,7))
res0: Int = 6
// 하지만, PartitionFunction에서도 예외가 발생하는 것은 여전하다.
scala> second(List())
scala.MatchError: List() (of class scala.collection.immutable.Nil$)
at scala.PartialFunction$$anon$1.apply(PartialFunction.scala:259)
at scala.PartialFunction$$anon$1.apply(PartialFunction.scala:257)
at $anonfun$1.applyOrElse(<console>:11)
at $anonfun$1.applyOrElse(<console>:11)
at scala.runtime.AbstractPartialFunction.apply(AbstractPartialFunction.scala:38)
... 28 elided
for 표현식 안에 패턴을 사용할 수 있다.
아래 예시는 capitals 맵에서 모든 키/값 쌍을 가져온다.
각 튜플은 country와 city 변수가 있는 패턴과 매치된다.
두 번째 예시는 튜플 패턴과 정확히 매치하지 않을 경우에 해당 값을 버리는 경우에도 사용된다.
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[String]] = List(Some(apple), None, Some(orange))
scala> for (Some(fruit) <- results) println(fruit)
apple
orange