scala 패턴매칭 관련해서 이전 문서 보다가 스터디하면서 정리했던 내용이 있길래 블로그로 무브
패턴 매칭
단순 매치
val bools = Seq(true, false)
for (bool <- bools) {
bool match {
case true => println("Got heads")
case false => println("Got tails")
}
}
컴파일러는 시퀀스의 타입으로부터 그 원소에 true와 false라는 두가지 경우가 있음을 안다.
따라서 매치가 완전하지 않을 경우 경고를 표시한다.
또한 일치하는 case 절이 없는 경우 MatchError가 발생한다.
매치 내의 값, 변수, 타입
특정 값이나 특정 타입의 모든 값과 매치시킬 수 있다.
{code:scala}
for {
x <- Seq(1, 2, 2.7, "one", "two", 'four) // <1>
} {
val str = x match { // <2>
case 1 => "int 1" // <3>
case i: Int => "other int: "+i // <4>
case d: Double => "a double: "+x // <5>
case "one" => "string one" // <6>
case s: String => "other string: "+s // <7>
case unexpected => "unexpected value: " + unexpected // <8>
}
println(str) // <9>
}
{code}
결과
int 1
other int: 2
a double: 2.7
string one
string: two
unexpected value: 'four
- 매치는 앞에서부터 차례대로 처리된다.
- 구체적인 절이 앞으로 나와야 한다.
- 덜 구체적인 절은 뒤로 가야 한다.
- 기본절은 가장 마지막에 와야한다.
{code:scala}
def checkY(y: Int) = {
for {
x <- Seq(99, 100, 101)
} {
val str = x match {
case y => "found y!" // `y`(역 작은 따옴표)
case i: Int => "int: "+i
}
println(str)
}
}
checkY(100)
{code}
결과
int:99
found y!
int: 101
--------'y'일 경우--------
found y!
found y!
found y!
- case 절에서 소문자로 시작하는 이름은 뽑아낸 값을 저장할 새로운 변수 이름으로 간주한다
- 정의한 값을 참조하고 싶을 경우 역작은따옴표로 둘러싸야 한다.
- 대문자로 시작하는 이름은 타입 이름으로 간주된다.
- '\|'를 사용해서 *case \: Int \| \:Double => "Int or Double?"* 과 같이도 사용가능하다.
시퀀스에 일치시키기
Seq 는 정해진 순서대로 원소를 순회할 수 있는 List나 Vector 등의 모든 구체적인 컬렉션 타입의 부모 타입이다.
(Seq는 시퀀스('sequence 순서가 정해진 열')이라는 뜻에서 온 말이다)
{code:scala}
val nonEmptySeq = Seq(1, 2, 3, 4, 5) // <1>
val emptySeq = Seq.empty[Int]
val nonEmptyList = List(1, 2, 3, 4, 5) // <2>
val emptyList = Nil
val nonEmptyVector = Vector(1, 2, 3, 4, 5) // <3>
val emptyVector = Vector.empty[Int]
val nonEmptyMap = Map("one" -> 1, "two" -> 2, "three" -> 3) // <4>
val emptyMap = Map.empty[String,Int]
def seqToString[T](seq: Seq[T]): String = seq match { // <5>
case head +: tail => s"$head +: " + seqToString(tail) // <6>
case Nil => "Nil" // <7>
}
for (seq <- Seq( // <8>
nonEmptySeq, emptySeq, nonEmptyList, emptyList,
nonEmptyVector, emptyVector, nonEmptyMap.toSeq, emptyMap.toSeq)) {
println(seqToString(seq))
}
- Nil : 비어있는 List를 표현하는 객체, 이 객체는 모든 빈 시퀀스와 일치한다 (List가 아닌 컬렉션에도 Nil을 사용할 수 있다)
- tail : Seq에서 머리를 제외한 나머지 부분
결과
1 +: 2 +: 3 +: 4 +: 5 +: Nil
Nil
1 +: 2 +: 3 +: 4 +: 5 +: Nil
Nil
1 +: 2 +: 3 +: 4 +: 5 +: Nil
Nil
(one,1) +: (two,2) +: (three,3) +: Nil
Nil
- Map은 순회 시 특별한 순서를 보장하지 않기 때문에 Seq의 서브타입이 아님 (따라서 Map.toSeq를 호출해 키-값 튜플의 시퀀스 만들어 주어야함)
- 연산자 +: 는 시퀀스의 '콘즈' 연산자다. 메서드 이름이 콜론(:)으로 끝나면 오른쪽으로 결합되어 Seq의 꼬리에 대한 호출이 됨을 기억 (List :: 연산자와 비슷)
h3. 4.4 튜플에 일치시키기
{code:scala}
val langs = Seq(
("Scala", "Martin", "Odersky"),
("Clojure", "Rich", "Hickey"),
("Lisp", "John", "McCarthy"))
for (tuple <- langs) {
tuple match {
case ("Scala", _, _) => println("Found Scala") // <1>
case (lang, first, last) => // <2>
println(s"Found other language: $lang ($first, $last)")
}
}
{code}
결과
Found Scala
Found other language: Clojure (Rich, Hickey)
Found other language: Lisp (John, McCarthy)
케이스 클래스에 일치시키기
{code:scala}
case class Address(street: String, city: String, country: String)
case class Person(name: String, age: Int, address: Address)
val alice = Person("Alice", 25, Address("1 Scala Lane", "Chicago", "USA"))
val bob = Person("Bob", 29, Address("2 Java Ave.", "Miami", "USA"))
val charlie = Person("Charlie", 32, Address("3 Python Ct.", "Boston", "USA"))
for (person <- Seq(alice, bob, charlie)) {
person match {
case Person("Alice", 25, Address(_, "Chicago", _)) => println("Hi Alice!")
case Person("Bob", 29, Address("2 Java Ave.", "Miami", "USA")) =>
println("Hi Bob!")
case Person(name, age, _) =>
println(s"Who are you, $age year-old person named $name?")
}
}
{code}
결과
Hi Alice!
Hi Bob!
Who are you, 32 year-old person named Charlie?
-zipWithIndex : 튜플을 번호와 함께 출력하고 싶을 경우 사용
{code:scala}
val itemsCosts = Seq(("Pencil", 0.52), ("Paper", 1.35), ("Notebook", 2.43))
val itemsCostsIndices = itemsCosts.zipWithIndex
for (itemCostIndex <- itemsCostsIndices) {
itemCostIndex match {
case ((item, cost), index) => println(s"$index: $item costs $cost each")
}
}
{code}
결과
itemsCostsIndices : Seq[((String, Double), Int)] = List(((Pencil,0.52),0), ((Paper,1.35),1), ((Notebook,2.43),2))
0: Pencil costs 0.52 each
1: Paper costs 1.35 each
2: Notebook costs 2.43 each
apply, unapply 메서드
- 케이스 클래스의 특징 중 하나는 컴파일러가 모든 케이스 클래스에 대해 각 클래스와 이름이 같은 싱글턴 객체인 *동반 객체*를 자동으로 만들어 낸다.
(동반 객체는 직접 정의도 가능하다.)
* Companion Class (동반 클래스) : (객체 관점) 클래스의 이름이 싱글톤 객체의 이름과 같다.
* Companion Object (동반 객체) : (클래스 관점) 싱글톤 객체의 이름이 클래스의 이름과 같다.
{code:scala}
case class Point(x: Double = 0.0, y: Double = 0.0)
> 만들어지는 동반 객체
object Point { def apply(x: Double = 0.0, y: Double = 0.0) = new Point(x,y)}
> 동반 객체에는 자동으로 apply라는 메서드가 추가되고 동반 클래스의 생성자와 같은 인자를 받는다.
> 객체 뒤에 인자 목록을 덧붙이면 스칼라는 그 객체의 apply를 호출하기 위해 찾는다.
val p1 = Point.apply(1.0, 2.0)val p2 = Point(1.0, 2.0)둘은 동등한 식이라고 볼 수 있다.
Point.apply 메서드는 사실상 Point를 만드는 팩토리로 단지 동작은 new 키워드 없이 생성자를 호출하는 것과 같다.
{code}
unapply
{code:scala}
object patternMatching2 {
object Test {
def unapply(a: Int): Boolean = {
if (a < 10) { true }
else { false }
}
}
object Test2 {
def unapply(a: Int): Option[(Int, String)] = {
if (a > 30) { Some(a/10, "from Test2") }
else { None }
}
}
class Example {
def method(target: Int) {
target match {
case Test() => println("matched to Test")
case Test2(n @ Test(), m) => println("result: " + n + " " + m)
case 120 => println("match to 120")
case 11 => println("noneMatching")
}
}
}
val t = new Example //> t : patternMatching2.Example = patternMatching2$Example@593634ad
t.method(11) //> matched to Test
t.method(40) //> result: 4 from Test2
t.method(120) //> match to 120
}
{code}
- n, m은 Test2로부터 리턴받을 아큐먼트
- 돌려받는 아규먼트에 다른 extractor를 @로 연결하면 해당 값에 대한 패턴매칭을 추가로 적용할 수 있다.
케이스 절의 변수 바인딩에 대해 더 살펴보기
객체 자체에도 변수를 대입하고 싶은 경우
p @ ...문법은 전체 Person 인스턴스를 p에 대입하며, a @ ...부분도 비슷하게 Address 객체를 대입한다.
{code:scala}
case class Address(street: String, city: String, country: String)
case class Person(name: String, age: Int, address: Address)
val alice = Person("Alice", 25, Address("1 Scala Lane", "Chicago", "USA"))
val bob = Person("Bob", 29, Address("2 Java Ave.", "Miami", "USA"))
val charlie = Person("Charlie", 32, Address("3 Python Ct.", "Boston", "USA"))
for (person <- Seq(alice, bob, charlie)) {
person match {
case p @ Person("Alice", 25, address) => println(s"Hi Alice! $p")
case p @ Person("Bob", 29, a @ Address(street, city, country)) =>
println(s"Hi ${p.name}! age ${p.age}, in ${a.city}")
case p @ Person(name, age, _) =>
println(s"Who are you, $age year-old person named $name? $p")
}
}
{code}
결과
Hi Alice! Person(Alice,25,Address(1 Scala Lane,Chicago,USA))
Hi Bob! age 29, in Miami
Who are you, 32 year-old person named Charlie? Person(Charlie,32,Address(3 Python Ct.,Boston,USA))
패턴 매칭의 다른 사용법
if 식에서도 패턴 매칭을 사용할 수 있다.
{code:scala}
val p = Person("Alice", 25, Address("1 Scala Lane", "Chicago", "USA"))
if ( alice == Person("Alice", 25, Address("1 Scala Lane", "Chicago", "USA")))
"yes"
else "no"
결과
res0: String = yes
위치지정자 _는 여기서 사용할 수 없다.
== 검사에 사용할 $eq$eq라는 내부 함수가 있다.
JVM 명세에서는 식별자에 알파벳, 숫자, _, $만 사용할 수 있기 때문에, 스칼라는 그 외의 문자를 JVM이 받아들일 수 있는 문자열로 조작한다.
그래서 =은 $eq가 된다.
마치며
- 정보를 정제된 방식으로 추출해야 하는 경우 패턴 매칭을 고려하라.
- for 내장과 함께 패턴 매칭은 자주 사용하는 스칼라 코드를 간결하면서 강력하게 만들어준다.
- 스칼라 프로그래머가 1/10 정도의 줄 수로 자바로 만든 프로그램과 비슷한 기능을 작성하는 경우도 드물지 않다.
- 패턴 매칭은 여러 함수형 언어의 보증서로 데이터 구조로부터 데이터를 뽑아내는 유연하고 간결한 기법이다.