16.1 리스트 리터럴

val fruit = List("apples", "oranges", "pears")
val nums = List(1, 2, 3, 4)
val diag3 =
  List(
      List(1, 0, 0),
      List(0, 1, 0),
      List(0, 0, 1)
)
val empty = List()

: 리스트는 배열과 비슷하지만 변경 불가능하고 구조가 재귀적이다. 배열은 평면적이다. (22장)

 

16.2 리스트 타입

: 리스트는 동종 원소로 이뤄진다.(= 원소의 타입이 같다)

: 리스트 타입은 공변적이다.(S가 T의 서브타입이면 List[S]도 List[T]의 서브타입이다)

       Ex. List[String]은 List[Object]의 서브타입

     => 빈 리스트 타입 List[Nothing]은 모든 T 타입의 List의 서브타입

     => val xs: List[String] = List() 가 가능

 

16.3 리스트 생성

: 모든 리스트는 Nil과 ::(콘즈)로 리스트를 만든다. List(...)은 Nil과 ::을 확장해주는 래퍼에 불과

      Ex. List( 1, 2, 3 )은 1 :: ( 2 :: ( 3 :: Nil ))) 로 생성된다.

: 콜론으로 끝나기 때문에 :: 연산자는 오른쪽 결합 법칙

 

16.4 리스트 기본 연산

- head : 어떤 리스트의 첫 번째 원소 반환

- tail : 어떤 리스트의 첫 번째 원소를 제외한 나머지 원소로 이뤄진 리스트 반환

- isEmpty : 리스트가 비어 있다면 true 반환

: head와 tail은 비어 있지 않은 리스트에만 유효 => 빈 리스트에 head와 tail을 호출하면 예외 발생

" 삽입 정렬 예제 "

def isort(xs: List[Int]): List[Int] =
    if (xs.isEmpty) Nil
    else insert(xs.head, isort(xs.tail))

 

def insert(x: Int, xs: List[Int]): List[Int] =
    if (xs.isEmpty || x <= xs.head) x :: xs
   else xs.head :: insert(x, xs.tail)

 

16.5 리스트 패턴

scala> val List(a, b, c) = fruit
a: String = apples
b: String = oranges
c: String = pears

 

scala> val a :: b :: rest = fruit        // 리스트 원소의 개수를 미리 알 수 없다면
a: String = apples
b: String = oranges
rest: List[String] = List(pears)

: 리스트 패턴은 head, tail, isEmpty의 대안

" 삽입 정렬 예제 "

def isort(xs: List[Int]): List[Int] = xs match {
    case List()   =>   List()
    case x :: xs1   =>   insert(x, isort(xs1))
}

def insert(x: Int, xs: List[Int]): List[Int] = xs match {
    case List()  =>  List(x)
    case y :: ys  =>  if (x <= y) x :: xs
                                    else y :: insert(x, ys)
}

 

16.6 List 클래스의 1차 메소드

- 1차 메소드 : 어떤 메소드가 함수를 인자로 받지 않는 것

< 두 리스트 연결하기  - ":::" >

: ":::"는 두 인자를 리스트로 받는 리스트 연결 연산, "::"와 마찬가지로 오른쪽 결합 법칙

scala> List(1, 2) ::: List(3, 4, 5)
res0: List[Int] = List(1, 2, 3, 4, 5)

Ex. 분할 정복 원칙을 통해 ":::" 구현 예제

def append[T](xs: List[T], ys: List[T]): List[T] =
    xs match {
        case List() => ys
        case x :: xs1 => x :: append(xs1, ys)
}

 


< 리스트 길이 구하기 : length >

: length는 리스트 끝을 찾기 위해 전체 리스트를 순회 => 리스트 원소 개수만큼 시간 소요 => xs.isEmpty 를 xs.length==0으로 바꾸는 것은 권장 X

 

< 리스트 양 끝에 접근하기 : init, last >

- init : 마지막 원소를 제외한 모든 원소를 포함한 리스트 반환

- last : 마지막 원소 반환

: init, last 는 빈 리스트에 호출하면 예외

: head와 tail은 상수 시간 복잡도이지만 init와 last는 전체 리스트를 순회해야해 리스트 길이만큼 시간이 걸린다.

 

< 리스트 뒤집기 : reverse >

scala> val abcde = List('a', 'b', 'c', 'd', 'e')
abcde: List[Char] = List(a, b, c, d, e)

 

scala> abcde.reverse
res6: List[Char] = List(e, d, c, b, a)

- reverse는 다음 법칙을 만족

1. xs == xs.reverse.reverse

2. reverse는 init을 tail로 바꾸고 last를 head로 바꾼다.

     => xs.reverse.init == xs.tail.reverse

     => xs.reverse.tail == xs.init.reverse

     => xs.reverse.head == xs.last

     => xs.reverse.last == xs.head

- reverse는 ":::"을 사용해 구현할 수 있다.

def rev[T](xs: List[T]): List[T] = xs match {
      case List() => xs
      case x :: xs1 => rev(xs1) ::: List(x)
}

: rev를 n번 재귀 호출, xs ::: ys 는 xs의 크기만큼 시간이 걸린다. => rev 복잡도 : n + (n-1) +... 1 = (n+1)*n/2 => 변경 가능한 연결리스트를 뒤집을 때 선형 복잡도가 드는 것과 비교하면 실망스러운 결과

 

< 접두사와 접미사 : drop, take, splitAt >

- xs take n 은 xs 리스트 처음부터 n까지 원소 반환

- xs drop n 은 첫 번째에서 n번째까지 원소를 제외한 모든 원소 반환

- xs splitAt n 은 주어진 인덱스 위치에서 리스트를 분할해서 두 리스트가 들어있는 순서쌍 반환 == (xs take n , xs drop n)

scala> val abcde = List('a', 'b', 'c', 'd', 'e')
abcde: List[Char] = List(a, b, c, d, e)

 

scala> abcde splitAt 2
res10: (List[Char], List[Char]) = (List(a, b),List(c, d, e))

 

< 원소 선택하기 : apply, indices >

- apply : 인덱스의 원소 선택

scala> abcde apply 2

res11: Char = c

 

scala> abcde(2)
res12: Char = c

: 스칼라에서 apply 연산을 거의 사용하지 않는다. xs(n)에서 인덱스 n의 값에 비례해 시간이 걸리기 때문

: xs apply n == (xs drop n).head

- indices : 리스트에서 유효한 모든 인덱스의 리스트를 반환

scala> abcde.indices
res13: scala.collection.immutable.Range= Range(0, 1, 2, 3, 4)

 

< 리스트의 리스트를 한 리스트로 반듯하게 만들기 : flatten >

scala> List(List(1, 2), List(3), List(), List(4, 5)).flatten
res14: List[Int] = List(1, 2, 3, 4, 5)

 

scala> val fruit = List("apples", "oranges", "pears")

scala> fruit.map(_.toCharArray).flatten
res15: List[Char] = List(a, p, p, l, e, s, o, r, a, n, g, e, s, p, e, a, r, s)

: flatten은 리스트의 원소가 모두 리스트인 경우에만 적용 가능

 

< 두 리스트를 순서쌍으로 묶기 : zip, unzip >

scala> val zipped = abcde zip List(1, 2, 3) 
zipped: List[(Char, Int)] = List((a,1), (b,2), (c,3))       // 길이가 긴 쪽에 남는 원소를 버린다

 

scala> zipped.unzip
res19: (List[Char], List[Int]) = (List(a, b, c),List(1, 2, 3))

 

scala> abcde.zipWithIndex                    // 각 원소를 인덱스와 함께 순서쌍으로 묶을 때
res18: List[(Char, Int)] = List((a,0), (b,1), (c,2), (d,3), (e,4))

 

< 리스트 출력하기 : toString, mkString >

- xs mkString ( pre, sep ,post ) => pre + xs(0) + sep + ... + sep + xs + post

: mkString 메소드는 인자 일부가 없는 오버로드한 메소드 2개 존재

      => xs mkString sep == xs mkString ("", sep, "")

      => xs.mkString == xs mkString ""

- addString : 생성한 문자열 결과를 반환하지 않고 StringBuilder 객체에 추가

scala> val buf = new StringBuilder
buf: StringBuilder =
scala> abcde addString (buf, "(", ";", ")")
res25: StringBuilder = (a;b;c;d;e)

 

< 리스트 변환하기 : iterator, toArray, copyToArray >

- xs copyToArray (arr, start) : 리스트 원소를 arr의 start 부터 연속적으로 복사

scala> val arr2 = new Array[Int](10)
arr2: Array[Int] = Array(0, 0, 0, 0, 0, 0, 0, 0, 0, 0)


scala> List(1, 2, 3) copyToArray (arr2, 3)
scala> arr2
res28: Array[Int] = Array(0, 0, 0, 1, 2, 3, 0, 0, 0, 0)

- iterator

scala> val it = abcde.iterator
it: Iterator[Char] = non-empty iterator

scala> it.next
res29: Char = a

 

scala> it.next
res30: Char = b

 

Ex. 병합 정렬

def msort[T](less: (T, T) => Boolean)
    (xs: List[T]): List[T] = {

  def merge(xs: List[T], ys: List[T]): List[T] =
    (xs, ys) match {
      case (Nil, _) => ys
      case (_, Nil) => xs
      case (x :: xs1, y :: ys1) =>
        if (less(x, y)) x :: merge(xs1, ys)
        else y :: merge(xs, ys1)
    }

  val n = xs.length / 2
  if (n == 0) xs
  else {
    val (ys, zs) = xs splitAt n
    merge(msort(less)(ys), msort(less)(zs))
  }
}

 

scala> msort((x: Int, y: Int) => x < y)(List(5, 7, 1, 3))
res31: List[Int] = List(1, 3, 5, 7)

 

scala> val intSort = msort((x: Int, y: Int) => x < y) _    // 커링을 사용하면 특정 비교 함수로 특화 가능
intSort: (List[Int]) => List[Int] = <function>

 

scala> val mixedInts = List(4, 1, 9, 0, 5, 8, 3, 6, 2, 7)
mixedInts: List[Int] = List(4, 1, 9, 0, 5, 8, 3, 6, 2, 7)

scala> intSort(mixedInts)
res0: List[Int] = List(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)

 

 

16.7 List 클래스의 고차 메소드

* 고차 메소드 : 인자로 다른 함수를 받는 함수

 

< 리스트 매핑 : map, flatmap, foreach >

- xs map f : List[T] 타입인 xs 와 T => U 타입인 f 함수를 받는다. xs의 모든 원소에 함수 f를 적용해서 나온 결과 값 리턴

scala> List(1, 2, 3) map (_ + 1)
res32: List[Int] = List(2, 3, 4)


scala> val words = List("the", "quick", "brown", "fox")
words: List[String] = List(the, quick, brown, fox)


scala> words map (_.length)
res33: List[Int] = List(3, 5, 5, 3)


scala> words map (_.toList.reverse.mkString)
res34: List[String] = List(eht, kciuq, nworb, xof)

- flatMap : map과 유사하지만 함수 f를 적용해서 나온 모든 리스트를 연결한 단일 리스트를 반환

scala> words map (_.toList)
res35: List[List[Char]] = List(List(t, h, e), List(q, u, i,c, k), List(b, r, o, w, n), List(f, o, x))

 

scala> words flatMap (_.toList)
res36: List[Char] = List(t, h, e, q, u, i, c, k, b, r, o, w, n, f, o, x)

- foreach는 오른쪽 피연산자로 프로시저(결과 값이 Unit인 함수)를 받는다.

scala> var sum = 0
sum: Int = 0

scala> List(1, 2, 3, 4, 5) foreach (sum += _)
scala> sum
res39: Int = 15

 

< 리스트 걸러내기 : filter, partition, find, takeWhile, dropWhile, span >

- xs filter p : List[T] 타입 xs와 T => Boolean 타입의 술어 함수 p를 받는다. xs의 원소 중 p(x)가 true인 원소 리스트 리턴

scala> List(1, 2, 3, 4, 5) filter (_ % 2 == 0)
res40: List[Int] = List(2, 4)


scala> words filter (_.length == 3)
res41: List[String] = List(the, fox)

- partition : filter와 비슷하지만 술어 함수 p가 true인 원소를 포함한 리스트와 false인 원소를 포함한 리스트의 순서쌍 반환

              xs partion p == (xs filter p, xs filter (!p(_)))

scala> List(1, 2, 3, 4, 5) partition (_ % 2 == 0)
res42: (List[Int], List[Int]) = (List(2, 4),List(1, 3, 5))

- find : filter와 비슷하지만 술어 함수 p를 만족하는 첫 번째 원소 반환(true인 원소 x가 존재하면 Some(x), 없으면 None으로 반환)

scala> List(1, 2, 3, 4, 5) find (_ % 2 == 0)
res43: Option[Int] = Some(2)


scala> List(1, 2, 3, 4, 5) find (_ <= 0)
res44: Option[Int] = None

- xs takeWhile p : 술어 p에 만족하는 가장 긴 접두사 반환

- xs dropWhile p : 술어 p에 만족하는 가장 긴 접두사 제거

scala> List(1, 2, 3, -4, 5) takeWhile (_ > 0)
res45: List[Int] = List(1, 2, 3)

 

scala> List(1, 2, 3, -4, 5) dropWhile (_ > 0)
res45: List[Int] = List(-4,5)

- xs span p == (xs takeWhile p , xs dropWhile p)

scala> List(1, 2, 3, -4, 5) span (_ > 0)
res47: (List[Int], List[Int]) = (List(1, 2, 3),List(-4, 5))

 

< 리스트 전체에 대한 술어 : forall, exists >

- xs forall p : 리스트의 모든 원소가 p 술어 함수를 만족할 때 true 반환

- xs exists p : 리스트의 원소 중에 p 술어 함수를 하나라도 만족할 때 true 반환

 scala> val diag3 =    List(      

          List(1, 0, 0),   

          List(0, 1, 0),   

          List(0, 0, 1)    

     ) 

 

scala> def hasZeroRow(m: List[List[Int]]) =           

       m exists (row => row forall (_ == 0))
hasZeroRow: (m: List[List[Int]])Boolean    

 

scala> hasZeroRow(diag3)  

res48: Boolean = false

 

< 리스트 폴드: "/:", ":\" >

- 왼쪽 폴드 연산 : " ( z /: xs ) (op) " , z는 시작 값, xs는 폴드할 대상 리스트, op는 이항 연산자 의미하고 폴드한 결과는 z를 시작으로 xs의 모든 원소에 대해 op 연산자를 연속으로 적용한 것 

     Ex. ( z /: List(a,b,c) ) (op) 는 op(op(op(z,a),b),c) 와 같다.

scala> def sum(xs: List[Int]): Int = (0 /: xs) (_ + _)    // sum(List(a,b,c)) 는 0+a+b+c와 같다.
sum: (xs: List[Int])Int


scala> def product(xs: List[Int]): Int = (1 /: xs) (_ * _)   // product(List(a,b,c)) 는 1*a*b*c와 같다.
product: (xs: List[Int])Int

 

- 오른쪽 폴드 연산  => (List(a,b,c) :\ z) (op) 는 op(a, op(b, op(c, z))) 와 같다.

- 결합 법칙이 성립하는 연산에 대해 오른쪽 폴드와 왼쪽 폴드는 결과가 동일하지만 효율이 다를 수 있다.

def flattenLeft[T](xss: List[List[T]]) =
    (List[T]() /: xss) (_ ::: _)           // 스칼라 타입 추론 제약 때문에 리스트 타입을 제대로 추론 못 해 타입 표기          

def flattenRight[T](xss: List[List[T]]) =
    (xss : List[T]()) (_ ::: _)

: "xs ::: ys" 연산자는 첫 번째 인자 xs에 비례하는 시간이 걸린다. => flattenLeft(xss)가 첫 번째 원소인 리스트 xss.head를 n-1번 복사 => flattenLeft보다 flattenRight가 효율적

 

- 오른쪽/왼쪽 폴드 연산은 foldLeft, foldRight 메소드로 대신 사용 가능

 

Ex. 폴드를 사용해 리스트 뒤집기

def reverseLeft[T](xs: List[T]) =                // 위에서 보았던 ":::"로 구현한 reverse 비교해 선형 시간 복잡도
   (List[T]() /: xs) {(ys, y) => y :: ys}

 

< 리스트 정렬 : sortWith >

- xs sortWith before : xs를 두 원소를 비교할 수 있는 함수 before을 사용해 정렬한다. x before y는 정렬 결과에서 x가 y 앞에 있어야 한다면 true를 반환해야 한다.

scala> List(1, -3, 4, 2, 6) sortWith (_ < _)  

res51: List[Int] = List(-3, 1, 2, 4, 6)    

 

16.8 List 객체의 메소드

- List.apply : List(1,2,3) == List.apply(1,2,3)

- List.range(from, until) : from부터 until-1까지 모든 정수가 들어간 리스트를 만든다.

- List.range(from, until, seq) : from에서 시작해 seq만큼 간격이 떨어져 있는 수의 리스트를 만든다.

scala> List.range(1, 9, 2)  

res55: List[Int] = List(1, 3, 5, 7)    

 

scala> List.range(9, 1, -3)  

res56: List[Int] = List(9, 6, 3)

- List.fill : 같은 원소를 0번 이상 반복한 리스트를 만든다. 첫 번째 인자는 생성할 리스트의 차원, 두 번째 인자는 리스트의 원소

scala> List.fill(5)('a')  

res57: List[Char] = List(a, a, a, a, a)    

 

scala> List.fill(3)("hello")  

res58: List[String] = List(hello, hello, hello)]

 

scala> List.fill(2, 3)('b')                 // 인자를 2개보다 많이 전달하면 다차원 리스트를 생성

res59: List[List[Char]] = List(List(b, b, b), List(b, b, b))

- List.tabulate : 제공된 함수로 계산한 원소의 리스트를 생성한다. List.fill과 비슷하고 두 번째 인자에 원소 대신 함수를 가진다.

scala> val squares = List.tabulate(5)(n => n * n)  

squares: List[Int] = List(0, 1, 4, 9, 16)  

 

scala> val multiplication = List.tabulate(5,5)(_ * _)  

multiplication: List[List[Int]] = List(List(0, 0, 0, 0, 0), 

       List(0, 1, 2, 3, 4), List(0, 2, 4, 6, 8),    

       List(0, 3, 6, 9, 12), List(0, 4, 8, 12, 16))

- List.concat : 여러 리스트를 연결한다.

 scala> List.concat(List('a', 'b'), List('c'))  

res60: List[Char] = List(a, b, c) 

 

16.9 여러 리스트를 함께 처리하기

- zipped 메소드는 여러 리스트에 공통 연산을 수행한다.

scala> (List(10, 20), List(3, 4, 5)).zipped.map(_ * _)  

res63: List[Int] = List(30, 80)

 

scala> (List("abc", "de"), List(3, 2)).zipped.forall(_.length == _)  

res64: Boolean = true  

 

scala> (List("abc", "de"), List(3, 2)).zipped.exists(_.length != _)  

res65: Boolean = false

 

16.10 스칼라의 타입 추론 알고리즘(흐름 기반 타입 추론) 이해

scala> msort((x: Char, y: Char) => x > y)(abcde)            // 함수 파라미터에 이름과 타입 지정
res66: List[Char] = List(e, d, c, b, a)

scala> abcde sortWith (_ > _)                 
res67: List[Char] = List(e, d, c, b, a)

scala> msort(_ > _)(abcde)                                 // 함수 파라미터에 이름과 타입 지정 X => 타입 추론 실패
:12: error: missing parameter type for expanded 
function ((x$1, x$2) => x$1.$greater(x$2))
msort(_ > _)(abcde)
               ^ 

: abcde.sortWith(_>_)에서 abcde 타입은 List[Char] => sortWith가 "(Char,Char) => Boolean" 타입의 인자를 받는 메소드 => 함수 파라미터 타입 지정 X

: msort(_>_)(abcde)는 msort 자체에서 "_>_"의 타입을 알 방법 X => "( T, T ) => Boolean" 타입 추론 실패

             * 두 번째 인자인 abcde를 통해서 알 수도 있지만 두 번째 이후의 파라미터 목록은 타입 추론에 사용 X

" 추론 실패 문제 해결 "

scala> msort[Char](_ > _)(abcde)            // msort에 명확하게 타입 파라미터를 전달
res68: List[Char] = List(e, d, c, b, a)

 

def msortSwapped[T](xs: List[T])(less:
    (T, T) => Boolean): List[T] = {

  // same implementation as msort,
  // but with arguments swapped
}

 

scala> msortSwapped(abcde)(_ > _)      // 파라미터 위치를 바꾼다.
res69: List[Char] = List(e, d, c, b, a)

=> 함수가 아닌 파라미터와 함수 파라미터를 받는 다형성 메소드를 설계하면다면 함수 인자를 별도의 커링 파라미터 목록으로 맨 마지막으로 넣어라 => 타입 추론 O => 타입 정보를 더 적게 기입하고 함수 리터럴이 더 간결

 

- fold 연산 타입 추론

def flattenRight[T](xss: List[List[T]]) = 
    (xss : List[T]()) (_ ::: _)             // 타입 파라미터를 넣어야 한다.

: ( xs :\ z ) (op) 는 xs A 타입과 z B 타입을 받아서 B 타입의 결과를 반환한다. ( op : (A, B) => B)

: 리스트 xs 타입은 z의 타입과 무관하기 때문에 z에 대한 타입 추론 불가

def flattenRight[T](xss: List[List[T]]) = 
    (xss : List()) (_ ::: _)                   // z는 빈 리스트

: z는 빈 리스트이기 때문에 List[Nothing] 타입 => " (List[T], List[Nothing]) => List[Nothing] " => 빈 리스트를 반환하기 때문에 유용 X

=> 타입 추론을 너무 일찍(커링 함수를 호출할 때 첫 번째 인자 목록 타입으로 함수 값의 타입을 결정하는 규칙 때문)

=> 규칙을 완화하더라도 op 인자 타입을 명시하지 않았기 때문에 op 타입을 알 수 없다.

=> 타입 표기를 넣어야만 해결 가능(캐치 22 상황 - 규정의 모순으로 인한 진퇴양난의 상황)

 

 

 

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