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 상황 - 규정의 모순으로 인한 진퇴양난의 상황)

 

 

 

+ Recent posts