< 스칼라 컬렉션 특징 >

- 사용하기 쉬움 : 20~50개 메소드만 알아두면 대부분의 컬렉션 문제를 몇 가지 연산만 조합해 해결 가능하다.

- 간결함 : 하나 이상의 루프를 한 단어로 대체한다. => 쉽게 함수적 연산을 기술할 수 있고 연산을 조합할 수 있다. 이렇게 작성한 코드는 대수식 같아 보인다.

- 안전함 : 정적 타입 검사를 통과해야 해 대부분의 실수를 컴파일 시간에 걸러낼 수 있다.

          * 걸러낼 수 있는 이유

                1. 컬렉션 연산 자체가 널리 쓰이며 잘 검증되어 있다. 

                2. 컬렉션 연산의 입력과 출력을 함수 파라미터와 결과 값으로 명시해야 한다.

                3. 2번 항목의 명시적인 입출력이 정적 타입 검사를 통과해야 한다.

- 빠름 : 컬렉션 연산은 튜닝과 최적화가 되어 있어 매우 효율적이다.

        * 컬렉션을 다중 코어에서 병렬 실행할 수 있다. 병렬 컬렉션은 컬렉션과 동일한 연산을 제공하기 때문에 사용이 쉽다.

- 보편적임 : 각 컬렉션은 같은 연산을 제공한다. ( 문자의 시퀀스인 문자열도 모든 시퀀스 연산을 제공한다. )

 

24.1 변경 가능, 변경 불가능 컬렉션

: 스칼라 컬렉션은 변경 불가능한 것과 변경 가능한 것으로 구분한다.

- 변경 가능 컬렉션 : 메모리상에서 변경하거나 확장할 수 있는 컬렉션, 부수 효과를 사용해 원소를 변경, 추가, 삭제할 수 있다.

- 변경 불가능한 컬렉션 : 결코 변하지 않는다. 추가, 삭제, 변경하는 연산이 있지만 새 컬렉션을 반환하며 원래 컬렉션은 변하지 않고 그대로 남는다.

 

: 모든 컬렉션 클래스는 scala.collection 패키지나 그 하위 패키지인 muable, immutable, generic 패키지 안에 있다.

: 컬렉션 클래스는 3가지 변형이 존재하며 각각은 변경 가능성 측면에서 차이가 있다.(scala.collection, scala.collection.immutable, scala.collection.mutable 패키지에 위치)

- scala.collection.immutable 안 컬렉션 : 변경 불가능

- scala.collection.mutable : 변경 가능

- scala.collection : 변경 불가능할 수도, 변경 가능할 수도 있다.

Ex. scala.collection.IndexedSeq[T]는 scala.collection.immutable.IndexedSeq[T]와 scala.collection.mutable.IndexedSeq[T]의 슈퍼 트레이트이다.

: 일반적으로 scala.collection에 있는 루트 컬렉션들은 변경 불가능한 컬렉션과 동일한 인터페이스를 정의한다. 그리고 scala.collection.mutable에 변경 불가능한 인터페이스에 부수 효과가 있는 변경 연산을 추가한다.

 

: 루트 컬렉션과 변경 불가능한 컬렉션의 차이는 변경 불가능한 것을 사용하는 클라이언트에게는 컬렉션이 변경되지 않는다는 것을 보장하는 반면, 루트 컬렉션은 루트 컬렉션 타입을 통해서 컬렉션을 변경할 수 없다는 점이다. 단, 실행 시점에 루트 컬렉션이 변경 가능한 컬렉션일 수 있기 때문에 그 컬렉션이 변경될 수도 있다.

 

: 기본적으로 스칼라는 항상 변경 불가능한 컬렉션을 선택한다.

 

: collection.generic은 컬렉션 클래스들의 연산에 대한 구현을 generic에 있는 클래스에 위임한다.

 

24.2 컬렉션 일관성

컬렉션 계층 구조

: 어떤 종류의 컬렉션이라도 일관되고 동일한 문법으로 생성할 수 있다.

: 모든 컬렉션은 컬렉션에서 지원하는 API에 대해 구체적인 개별 클래스를 반환한다.

Ex. List에 대해 map을 호출하면 List가 나오고 Set에 대해 map을 호출하면 Set을 반환한다.

 

: 대부분의 컬렉션은 루트, 변경 가능, 변경 불가능한 세 가지 버전이 존재한다. 유일한 예외는 Buffer 트레이트로 변경 가능한 컬렉션만 있다.

 

24.3 Traversable 트레이트

: Traversable 트레이트에는 foreach 유일한 추상 멤버가 존재해 클래스가 Traversable을 구현하려면 foreach만 정의하면 된다.

    * def foreach[U](f:Elem => U) : 모든 원소를 순회하면서 f 연산을 각 원소에 적용

: foreach가 Traversable의 모든 연산을 만드는 기반

: Traversable은 아래의 구체적인 메소드를 포함한다.

* 크기 연산 : 순회 가능한 컬렉션은 유한할 수도 있고 무한할 수도 있다. 무한한 순회 가능 컬렉션의 예로 자연수의 스트림인 Stream.from(0) => hasDefiniteSize가 false라면 size는 오류를 발생시키거나 아무 값도 반환하지 않는다.

* 원소를 가져오는 연산 : 해시 집합은 순서를 가지고 있지 않기 때문에 해시 집합의 첫 원소는 프로그램을 실행할 때마다 바뀔 수 있다.(순서를 포기하는 대신 효율성을 선택)

: 어떤 컬렉션이 매번 원소를 같은 순서로 반환하는 경우 이를 순서가 있는 컬렉션이라 한다.

: 순서가 꼭 필요한 경우가 있기 때문에 몯느 컬렉션에 대해 순서가 정해진 대안을 제공한다.(순서가 있는 HashSet은 LinkedHashSet)

* view는 필요에 따라 나중에 계산이 이뤄지는 컬렉션

 

24.4 Iterable 트레이트

: iterable의 모든 메소드는 추상 메소드 iterator을 기반으로 한다. iterator는 컬렉션의 원소를 하나씩 돌려준다.

: Traversable에서 상속한 추상 foreach 메소드를 iterator를 사용해 정의한다.

def foreach[U](f: Elem => U): Unit = {
  val it = iterator
  while (it.hasNext) f(it.next())

: 효율적인 구현을 제공하기 위해서 Iterable 서브 클래스 중 상당수는 Iterable에 있는 표준 foreach 구현을 오버라이드한다.

 

: 이터레이터를 반환하는 메소드로 grouped와 sliding이 있다.

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

scala> val git = xs grouped 3
git: Iterator[List[Int]] = non-empty iterator

scala> git.next()
res2: List[Int] = List(1, 2, 3)

scala> git.next()
res3: List[Int] = List(4, 5)

scala> val sit = xs sliding 3
sit: Iterator[List[Int]] = non-empty iterator

scala> sit.next()
res4: List[Int] = List(1, 2, 3)

scala> sit.next()
res5: List[Int] = List(2, 3, 4)

scala> sit.next()
res6: List[Int] = List(3, 4, 5)

< Trabersable 과 Iterable이 각각 존재하는 이유 >

: 모든 것을 iterator로 하지 않고 모든 메소드를 iterator 대신 foreach를 가지고 만들기 위해 한 단께 더 추상적인 트레이트를 두는 이유는 때때로 iterator을 구현하는 것보다 foreach 메소드의 구현을 효율적으로 하는 편이 쉬운 경우가 있기 때문

 

Ex. 잎(단말 노드)에 정수 원소를 저장하는 이진 트리 클래스 계층


sealed abstract class Tree
case class Branch(left: Tree, right: Tree) extends Tree
case class Node(elem: Int) extends Tree

" foreach는 균형 잡힌 트리를 순회하는 데 트리 안의 노드 개수에 비례하는 시간이 걸린다. "

" N개의 잎이 있는 균현 잡힌 트리의 경우 Branch 타입의 내부 노드는 N-1 => N + N-1
sealed abstract class Tree extends Traversable[Int] {
  def foreach[U](f: Int => U) = this match {
    case Node(elem) => f(elem)
    case Branch(l, r) => l foreach f; r foreach f
  }
}

" ++ 연산의 효율성 문제로 트리에서 가져온 잎을 모을 때 log(N)번의 간접 경로 발생 => Nlog(N)
sealed abstract class Tree extends Iterable[Int] {
  def iterator: Iterator[Int] = this match {
    case Node(elem) => Iterator.single(elem)
    case Branch(l, r) => l.iterator ++ r.iterator
  }
}

 

< Iterable의 하위 분류 >

: Iterable 아래에는 Seq, Set, Map 하위 트레이트가 위치

: Set, Map, Seq의 공통점은 PartialFunction을 구현해서 apply와 isDefinedAt 메소드를 제공한다.

- Seq의 apply는 위치 인덱스로 사용 => Seq(1,2,3)(1) == 2

- Set의 apply는 원소인지 검사 => Set(1,2,3)(1) == true , Set()(1) == false

- Map의 apply는 키로 값 선택 => Map('a'->1, 'b'->10)('b') == 10

 

24.5 시퀀스 트레이트 : Seq, IndexedSeq, LinearSeq

: 시퀀스는 길이가 정해져 있고, 각 원소의 위치를 0부터 시작하는 고정된 인덱스로 지정할 수 있는 iterable의 일종

: Seq의 apply는 인덱스로 원소를 찾는 것으로 Seq[T] 타입의 시퀀스는 Int 인자(인덱스)를 받아서 T 타입의 원소를 돌려주는 부분 함수다.(Seq[T]는 PartialFunction[Int, T]를 확장한다.)

: Seq 트레이트에는 두 가지 하위 트레이트 LinearSeq와 IndexedSeq가 있다. 각각 성능이 다르다.

: 선형 시퀀스는 더 효율적인 head와 tail 연산을 제공한다. List나 Stream

: 인덱스 시퀀스는 효율적인 apply와 length, update연산을 제공한다. Array와 ArrayBuffer

: Vector는 선형 시퀀스와 인덱스 시퀀스 사이에 절충을 제공한다. 사실상 인덱스 시간과 선형 접근 시간이 상수

 

< 버퍼 >

: 기존 원소의 변경을 허용하고 원소의 삽입, 제거도 지원하며 버퍼의 맨 뒤에 효율적으로 원소를 추가할 수 있다.

: 버퍼 구현으로 List를 배경으로 하고 원소들을 효율적으로 리스트로 바꿀 수 있는 ListBuffer, Array를 배경으로 하고 원소들을 빠르게 배열로 바꿀수 있는 ArrayBuffer가 있다.

 

24.6 집합

: 원소 중복을 허용하지 않는 Iterable

: contain는 어떤 집합에 주어진 원소가 들어 있는지 표시한다. 집합에 대한 apply 메소드는 contains와 동일하다. => set(elem)은 set contains elem과 같다.

scala> val fruit = Set("apple", "orange", "peach", "banana")
fruit: scala.collection.immutable.Set[java.lang.String] =
Set(apple, orange, peach, banana)

scala> fruit("peach")
res7: Boolean = true

scala> fruit("potato")
res8: Boolean = false

: 집합 연산으로 intersect, union, diff가 있고 그에 대응하는 기호 이름은 각각 &, |, &~이다.

: Traversable에서 상속한 ++는 union이나 |의 별명으로 볼 수 있지만 union이나 |는 집합만을 취하는 반면, ++는 Traversable 인자를 취할 수 있다.

: +, ++, -, --는 변경 불가능/변경 가능 집합에 원소 추가/제거를 위해 존재하지만 연산이 진행될 때 집합을 복사하기 때문에 변경 가능 집합에서는 잘 사용하지 않는다.

: 변경 가능 집합에서는 효율적인 대안으로 +=와 -=을 사용한다.

: 변경 불가능한 집합에서도 +=와 -= 사용 가능 => 변경 불가능한 집합에서 s+=4는 s = s +4를 짧게 쓴 것

scala> var s = Set(1, 2, 3)
s: scala.collection.immutable.Set[Int] = Set(1, 2, 3)

scala> s += 4; s -= 2

scala> s
res9: scala.collection.immutable.Set[Int] = Set(1, 3, 4)


scala> val s = collection.mutable.Set(1, 2, 3)
s: scala.collection.mutable.Set[Int] = Set(1, 2, 3)

scala> s += 4
res10: s.type = Set(1, 4, 2, 3)

scala> s -= 2
res11: s.type = Set(1, 4, 3)

=> 변경 가능 컬렉션을 val로 저장하는 것은 변경 불가능한 컬렉션을 var에 저장하는 것으로 바꾸거나 그 반대로 바꿀 수 있다.

: 변경 가능 집합은 +=와 -= 다른 버전으로 add와 remove를 제공하며 차이점은 연산이 집합에 대해 효과가 있었는지 알려주는 불리언 값을 반환한다.

 

: 변경 가능한 집합은 해시 테이블을 사용해 원소를 저장한다.

: 변경 불가능한 집합은 원소의 개수에 따라 가변적인 방식(빈 집합 - 싱글톤, 4개 이하는 모든 원소를 필드로 저장하는 객체 하나, 5개 이후는 해시 트라이를 사용해 구현) => 원소가 4개 이하인 변경 불가능한 집합은 변경 가능 집합보다 훨씬 작고 효율적

 

24.7 맵

: 키와 값 쌍 Iterable

: 스칼라의 Predef 클래스에 key->value를 (key, value) 대신 사용할 수 있는 암시적 변환이 들어있다. => Map("x"->24)은 Map(("x", 24))와 같다.

: Map의 get 연산은 키에 대한 값이 있다면 값을 Some으로 묶어서 반환한다. 키가 없다면 None를 반환한다. apply 메소드는 Option에 값을 감싸지 않고 바로 반환한다.

: 변경 가능한 맵도 +, -, updated와 같은 연산을 제공하지만 복사해야 하기 때문에 자주 사용하지는 않는다. 대신 변경 가능한 맵은 보통 m(key) = value 나 m+= (key->value)를 사용해 그 자리에서 변경한다.

: 맵을 캐시로 사용하는 경우 getOrElseUpdate가 유용

" 비용이 많이 드는 f "

scala> def f(x: String) = {
  | println("taking my time."); Thread.sleep(100)
  | x.reverse }
f: (x: String)String

 

=> f에 부수 효과가 없다면, 같은 인자로 다시 f를 호출하면 항상 같은 결과가 나온다.

=> f를 사용해 계산한 인자와 결과 값을 맵에 담아두고 f의 인자를 맵의 키에서 찾을 수 없을 경우에만 f를 호출

=> 맵이 함수 f의 계산에 대한 캐시

 


scala> val cache = collection.mutable.Map[String, String]()
cache: scala.collection.mutable.Map[String,String] = Map()


scala> def cachedF(s: String) = cache.getOrElseUpdate(s, f(s))    // 두 번째 인자는 이름에 의한 호출
cachedF: (s: String)String

scala> cachedF("abc")
taking my time.
  res15: String = cba

scala> cachedF("abc")
res16: String = cba

 

24.8 변경 불가능한 구체적인 컬렉션 클래스

< 리스트 >

: 유한한 변경 불가능한 시퀀스, head와 tail은 상수 시간이 걸린다. 다른 많은 연산은 선형 시간이 걸린다.

 

< 스트림 >

: 리스트와 비슷하지만 원소를 지연 계산한다.

: 스트림은 무한할 수가 있고 외부에서 요청하는 원소만 계산한다.

: 리스트는 "::"연산자로 구성하는 반면 스트림은 "#::" 을 사용해 구성한다.

scala> val str = 1 #:: 2 #:: 3 #:: Stream.empty
str: scala.collection.immutable.Stream[Int] = Stream(1, ?)

: 인터프리터가 tail 부분을 표시하지 않았는 데 이는 아직 계산이 이뤄지지 않았기 때문이다. 스트림은 요청을 받으면 그 때 계산한다. toString 메소드는 스트림의 추가 계산을 요구하지 않도록 한다.

 

Ex. 피보나치

scala> def fibFrom(a: Int, b: Int): Stream[Int] =   a #:: fibFrom(b, a + b)
fibFrom: (a: Int,b: Int)Stream[Int]

: "#::" 대신 "::"을 사용했따면 이 함수에 대한 호출이 다른 호출을 다시 만들어 내서 무한 재귀가 일어날 것이다. 하지만 "#::"을 사용했기 때문에 "#::"의 오른쪽 부분은 요청이 있기 전까지 계산하지 않는다.

scala> val fibs = fibFrom(1, 1).take(7)
fibs: scala.collection.immutable.Stream[Int] = Stream(1, ?)

scala> fibs.toList
res22: List[Int] = List(1, 1, 2, 3, 5, 8, 13)

 

< 벡터 >

: 리스트는 헤드에 접근, 추가, 제거하는 데 상수 시간이 걸려 헤드만을 처리한다면 매우 효율적이다. 하지만 리스트 뒤쪽에 있는 우너소에 접근하거나 이를 변경하는 시간은 리스트의 길이에 선형으로 비례해 길어진다.

: 벡터는 헤드가 아닌 원소도 효율적으로 접근할 수 있는 컬렉션 타입이다. 임의의 원소에 접근하기 위해서 사실상 상수 시간이 걸린다.

scala> val vec = scala.collection.immutable.Vector.empty
vec: scala.collection.immutable.Vector[Nothing] = Vector()

scala> val vec2 = vec :+ 1 :+ 2
vec2: scala.collection.immutable.Vector[Int] = Vector(1, 2)

scala> val vec3 = 100 +: vec2
vec3: scala.collection.immutable.Vector[Int]
= Vector(100, 1, 2)

scala> vec3(0)
res23: Int = 100

: 벡터는 트리로 표현하고 모든 트리 노드는 32개의 벡터 원소를 넣거나 32개의 다른 트리 노드를 넣을 수 있다.

: 32개 이하인 벡터는 노드 하나로 표현할 수 있다. 원소가 1024(32*32)개가 될 때까지는 한 번만 간접 노드를 거치면 접근이 가능하다.

: 2개 노드를 거치면 2^15, 3개 노드를 거치면 2^20, 4개 노드를 거치면 2^25, 5개 노드를 거치면 2^30 원소를 다룬다.

=> 일반적인 원소 선택은 최대 5단계 정도의 기본 배열 선택으로 가능하다. => 사실상 상수 시간

: 벡터는 변경 불가능

scala> val vec = Vector(1, 2, 3)
vec: scala.collection.immutable.Vector[Int] = Vector(1, 2, 3)

scala> vec updated (2, 4)      
res24: scala.collection.immutable.Vector[Int] = Vector(1, 2, 4)

scala> vec
res25: scala.collection.immutable.Vector[Int] = Vector(1, 2, 3)

: updated도 사실상 상수 시간이다. 백터의 중간에 있는 원소를 변경하려면 그 원소가 있는 노드를 복사하고 트리의 루트부터 시작해서 원래의 노드를 가리키던 모든 노드를 복사한다. =>  전체 벡터를 모두 복사하는 경우보다는 비용이 훨씬 덜 든다.

 

: 벡터가 빠른 임의 접근 읽기와 빠른 임의 접근 변경 사이에서 균형을 잘 잡고 있기 때문에 변경 불가능한 인덱스가 있는 시퀀스의 기본 구현은 벡터이다.

scala> collection.immutable.IndexedSeq(1, 2, 3)
res26: scala.collection.immutable.IndexedSeq[Int]
= Vector(1, 2, 3)

 

< 변경 불가능한 스택 >

: 후입선출 시퀀스, push/pop/peek 모두 상수 시간

: 변경 불가능한 스택에 push하는 것은 리스트의 ::와 같고 스택의 pop은 tail과 같기 때문에 사용하는 경우는 드물다.

scala> val stack = scala.collection.immutable.Stack.empty
stack: scala.collection.immutable.Stack[Nothing] = Stack()

scala> val hasOne = stack.push(1)
hasOne: scala.collection.immutable.Stack[Int] = Stack(1)

scala> stack
res27: scala.collection.immutable.Stack[Nothing] = Stack()

scala> hasOne.top
res28: Int = 1

scala> hasOne.pop
res29: scala.collection.immutable.Stack[Int] = Stack()

 

< 변경 불가능한 큐 >

: 선입선출 시퀀스

scala> val empty = scala.collection.immutable.Queue[Int]()
empty: scala.collection.immutable.Queue[Int] = Queue()

scala> val has1 = empty.enqueue(1)
has1: scala.collection.immutable.Queue[Int] = Queue(1)

scala> val has123 = has1.enqueue(List(2, 3))
has123: scala.collection.immutable.Queue[Int]
= Queue(1, 2, 3)

scala> val (element, has23) = has123.dequeue
element: Int = 1
has23: scala.collection.immutable.Queue[Int] = Queue(2, 3)

 

< 범위 >

: 간격이 일정한 정수를 순서대로 나열한 시퀀스

scala> 1 to 3
res30: scala.collection.immutable.Range.Inclusive
  with scala.collection.immutable.Range.ByOne
= Range(1, 2, 3)

scala> 5 to 14 by 3
res31: scala.collection.immutable.Range
= Range(5, 8, 11, 14)


scala> 1 until 3
res32: scala.collection.immutable.Range
  with scala.collection.immutable.Range.ByOne = Range(1, 2)

: 3개의 수인 시작, 끝, 간격만 가지고 모든 범위를 표현할 수 있기 때문에 범위를 표현하는 공간 복잡도는 상수 => 이런 표현 때문에 범위에 대한 대부분의 연산이 매우 빠르다

 

< 해시 트라이 >

: 변경 불가능한 집합이나 맵을 효율적으로 구현하는 표준적인 방법이다.

: 벡터와 비슷하게 32개의 원소나 32개의 하위 트리를 포함하는 노드를 사용하는 트리다.하지만 선택 시 해시 코드를 활용한다.

: 맵에서 어떤 키를 찾을 때 키의 해시 코드의 최하위 5비트를 루트의 하위 트리를 선택하기 위해 사용하고 그 다음 5비트는 다음 단계 트리를 선택하는 데 사용된다.

: 노드 안에 들어 있는 모든 원소의 해시 코드가 지금까지 트리를 순회하는 데 사용한 비트들을 제외하고 모두 다 달라지면 검색을 중단한다.

: 해시 트라이는 충분히 빠른 검색과 충분히 효율적인 함수형 추가(+), 제거(-) 사이의 균형을 잘 잡고 있다. 그래서 스칼라의 맵과 집합의 기본 구현에 사용된다.

 

< 적흑 트리 >

: 일부 노드는 빨간색, 일부 노드는 검은색으로 표시된 균형 이진 트리

: TreeSet과 TreeMap, SortedSet이 적흑 트리를 사용한다.(적흑 트리를 사용하면 모든 원소를 정렬 순서대로 방문할 수 있는 효율적인 이터레이터를 만들 수 있기 때문)

 

< 변경 불가능한 비트 집합 >

: 비트 집합은 작은 정수의 컬렉션을 그보다 더 큰 정수의 비트로 표현된다. ( Ex. 3,2,0을 포함하는 비트 집합은 이진수 1101, 십진수 13)

: 내부적으로 64비트 Long 배열 사용, 첫 번째 Long은 0부터 63까지의 비트를 표현하고 두 번째 Long은 64부터 127까지 표현한다.

: 원소 포함 여부 검사는 상수 시간이 걸린다. => Long 배열에서 특정 Long을 고른 다음, 그 Long 안에서 비트 연산을 수행

: 원소를 추가하는 데는 Long의 개수에 선형으로 비례하는 시간이 걸린다.

   * 원소를 추가할 때 해당 원소가 비트 집합의 현재 Long 배열이 표현할 수 있는 범위 안에 있다면 상수시간에 원소 추가 가능

scala> val bits = scala.collection.immutable.BitSet.empty
bits: scala.collection.immutable.BitSet = BitSet()

scala> val moreBits = bits + 3 + 4 + 4
moreBits: scala.collection.immutable.BitSet = BitSet(3, 4)

scala> moreBits(3)
res34: Boolean = true

scala> moreBits(0)
res35: Boolean = false

 

< 리스트 맵 >

: 키-값 쌍의 연결 리스트로 맵을 표현한다.

: 리스트 맵에 대한 연산은 전체 리스트를 이터레이션해야 한다. 따라서 맵의 크기에 선형으로 비례하는 시간이 걸린다.

: 변경 불가능 맵이 더 빠르기 때문에 사용할 일이 거의 없으나 맵의 첫 원소를 다른 원소들보다 더 자주 선택해야 하는 경우 사용한다.

 

24.9 변경 가능한 구체적인 컬렉션 클래스

< 배열 버퍼 >

: 배열과 크기를 저장한다. 대부분의 연산은 배열과 속도가 같고 데이터를 배열 끝에 효율적으로 추가할 수 있다.(상수 분할 상환 시간이 걸린다.)

 

< 리스트 버퍼 >

: 배열 버퍼와 비슷하나 내부에서 배열 대신 연결 리스트를 사용한다.

: 버퍼를 다 구성한 다음에 리스트로 변환할 예정이라면 배열 버퍼 대신에 리스트 버퍼를 사용

 

< 문자열 빌더 >

: 문자열을 만들 때 유용하다.

: 문자열 빌더는 기본 네임스페이스에 이미 들어가 있어 바로 사용 가능

scala> val buf = new StringBuilder
buf: StringBuilder = StringBuilder()

scala> buf += 'a'
res43: buf.type = StringBuilder(a)

scala> buf ++= "bcdef"
res44: buf.type = StringBuilder(a, b, c, d, e, f)

scala> buf.toString
res45: String = abcdef

 

< 연결 리스트 >
: 다음 노드를 가리키는 next 포인터로 서로 연결된 노드들로 이뤄진 변경 가능 시퀀스

: 대부분 언어에서 빈 연결 리스트를 표현하는 데 스칼라는 next 포인터가 자기 자신을 가리키는 방법을 사용 => 빈 연결리스트.isEmpty는 NullPointerException 대신 true을 반환한다.

: 순차적으로 원소를 접근할 때 가장 좋고 연결 리스트에 원소를 추가하거나 다른 연결 리스트를 추가하기가 쉽다.

 

: map, flatMap, fliter 같은 고차 함수가 리스트를 처리하는 강력한 구성 요소이지만 때로 프로그램을 이해하기 힘들 수 있다.

Ex.

scala> case class Person(name: String,
                         isMale: Boolean,
                         children: Person*)

 

val lara = Person("Lara", false)
val bob = Person("Bob", true)
val julie = Person("Julie", false, lara, bob)
val persons = List(lara, bob, julie)

 

" 모든 엄마와 자식의 쌍을 찾고 싶다. "

scala> persons filter (p => !p.isMale) flatMap (p =>
  |     (p.children map (c => (p.name, c.name))))
res0: List[(String, String)] = List((Julie,Lara), (Julie,Bob))

 

" filter 대신 withFilter를 사용하면 중간 데이터 구조를 만들 필요가 없다. "

scala> persons withFilter (p => !p.isMale) flatMap (p =>
  |     (p.children map (c => (p.name, c.name))))
res1: List[(String, String)] = List((Julie,Lara), (Julie,Bob))

 

" for 표현식이 이해가 쉽고 명확하다. "

scala> for (p <- persons; if !p.isMale; c <- p.children)
  | yield (p.name, c.name)
res2: List[(String, String)] = List((Julie,Lara), (Julie,Bob))

: 스칼라 컴파일러는 결과를 yield하는 모든 for 표현식을 map, flatMap, withFilter 등의 고차 메소드 호출을 조합한 표현식으로 반환한다. yield가 없는 for 표현식은 withFilter와 foreach만을 사용한다.

 

23.1 for 표현식

" 일반적인 for 표현식 "

for ( seq ) yield expr

: seq는 제너레이터, 정의, 필터를 나열한 것으로 연속된 구성요소 사이에는 세미콜론을 넣는다. 괄호 대신 중괄호를 사용하면 세미콜론을 생략해도 된다.

for {
  p <- persons              // a generator
  n = p.name                // a definition
  if (n startsWith "To")    // a filter
} yield n

" 제너레이터의 형태 "

pat <- expr

: expr 표현식은 보통 리스트를 반환한다

: pat 패턴은 리스트의 모든 원소를 하나씩 매치해 얻는다. 매치에 성공하면 패턴 안의 변수에는 리스트 원소의 해당 부분에 있는 값이 들어간다. 매치에 실패하도 MatchError를 발생시키지 않고 무시하고 다음 이터레이션을 진행한다.

: x<-expr 처럼 pat가 변수 하나로만 되어 있는 경우가 가장 일반적이고 변수 x는 expr이 반환하는 모든 원소를 단지 이터레이션

 

" 정의의 형태 "

pat = expr

: pat 패턴을 expr의 값에 바인딩하고 val이 된다.

: x = expr 처럼 변수 하나로 되어 있는 경우가 가장 일반적

 

" 필터의 형태 "

if expr

: expr은 Boolean 타입의 값이다. expr이 false를 반환하는 값을 이터레이션에서 제외하도록 만든다.

 

23.2 n 여왕 문제

: N*N 체스판에서 n개의 여왕 말을 서로 잡지 못하게 놓는 문제 (여왕 말은 같은 열, 행, 또는 대각선 상에 있으면 서로를 잡을 수 있다.)

=> N*N 체스판의 왼쪽 위 구석 칸은 (1,1), 오른쪽 아래 칸은 (N,N)

=> 같은 행에 있으면 안 되기 때문에 모든 행마다 여왕을 배치 => 각 행에 차례로 여왕말 배치, 이 때 새로운 행에 놓은 여왕이 이미 체스판에 위치한 다른 여왕들을 잡을 수 있는 위치인지 검사

=> 1 부터 k-1번째 행까지의 여왕말을 잡을 수 없는 k번째 행 여왕말을 어디에 배치할 지 결정하는 문제

=> 문제의 해를 각 여왕의 체스판 위치를 표시하는 좌표 리스트로 표현

=> k번째 행에 있는 여왕의 좌표를 리스트의 맨 앞에, 그 뒤에 k-1번째 행에 있는 여왕을 놓는 식으로 해를 도출


def queens(n: Int): List[List[(Int, Int)]] = {
  def placeQueens(k: Int): List[List[(Int, Int)]] =
    if (k == 0)
      List(List())
    else
      for {
        queens <- placeQueens(k - 1)     // 체스판에 k-1 여왕을 놓는 모든 해에 대해 이터레이션
        column <- 1 to n                             // k번째 여왕이 들어가야 할 모든 열의 좌표에 대해 이터레이션
        queen = (k, column)
        if isSafe(queen, queens)              // 새 여왕의 위치가 기존 모든 여왕을 고려했을 때 안전한지 결정
      } yield queen :: queens

  placeQueens(n)
}

def isSafe(queen: (Int, Int), queens: List[(Int, Int)]) =
  queens forall (q => !inCheck(queen, q))

def inCheck(q1: (Int, Int), q2: (Int, Int)) =
  q1._1 == q2._1 ||  // same row
    q1._2 == q2._2 ||  // same column
    (q1._1 - q2._1).abs == (q1._2 - q2._2).abs // on diagonal

 

23.3 for 식으로 질의하기

: for 표기법은 기본적으로 데이터베이스 질의 언어의 공통 연산들과 동등하다.

case class Book(title: String, authors: String*)


val books: List[Book] =
  List(
    Book(
      "Structure and Interpretation of Computer Programs",
      "Abelson, Harold", "Sussman, Gerald J."
    ),
    Book(
      "Principles of Compiler Design",
      "Aho, Alfred", "Ullman, Jeffrey"
    ),
    Book(
      "Programming in Modula-2",
      "Wirth, Niklaus"
    ),
    Book(
      "Elements of ML Programming",
      "Ullman, Jeffrey"
    ),
    Book(
      "The Java Language Specification", "Gosling, James",
      "Joy, Bill", "Steele, Guy", "Bracha, Gilad"
    )
  )

 

" 작사의 성이 Gosling인 모든 책의 제목을 찾는다. "

scala> for (b <- books; a <- b.authors
  |      if a startsWith "Gosling")
  | yield b.title
res4: List[String] = List(The Java Language Specification)

" 제목에 Program이라는 문자열이 들어간 모든 책의 제목을 찾는다. "
scala> for (b <- books if (b.title indexOf "Program") >= 0)
  | yield b.title
res5: List[String] = List(Structure and Interpretation of
  Computer Programs, Programming in Modula-2, Elements
  of ML Programming)

" 최소한 두 권 이상의 책을 쓴 작가를 모두 찾는다. "
scala> for (b1 <- books; b2 <- books if b1 != b2;
            |     a1 <- b1.authors; a2 <- b2.authors if a1 == a2)
  | yield a1
res6: List[String] = List(Ullman, Jeffrey, Ullman, Jeffrey)

" 리스트의 중복을 제거한다. "
scala> def removeDuplicates[A](xs: List[A]): List[A] = {
  |   if (xs.isEmpty) xs
  |   else
  |     xs.head :: removeDuplicates(
  |       xs.tail filter (x => x != xs.head)      // for (x <- xs.tail if x != xs.head) yield x
  |     )
  | }
removeDuplicates: [A](xs: List[A])List[A]

scala> removeDuplicates(res6)
res7: List[String] = List(Ullman, Jeffrey)

 

23.4 for 표현식 변환

: 모든 for 표현식은 map, flatMap, withFilter 고차 함수로 표현가능하고 스칼라 컴파일러는 for 표현식을 고차 함수로 변환한다.

 

< 제너레이터가 하나 밖에 없는 for 표현식의 변환 >

for ( x <- expr1 ) yield expr2

=>

expr1. map( x => expr2 )

 

< 제너레이터로 시작하고 필터가 하나 있는 for 표현식의 변환 >

for ( x <- expr1 if expr2 ) yield expr3

=>

for ( x <- expr1 withFilter (x => expr2 ) ) yield expr3

=>

expr1 withFilter ( x=>expr2 ) map ( x => expr3 )

 

" 필터 다음에 다른 원소가 있더라도 마찬가지로 변환 "

for ( x <- expr1 if expr2; seq ) yield expr3    // seq : 제너레이터, 정의, 필터

=>

for ( x <- expr1 withFilter (x => expr2 ) ; seq ) yield expr3

 

< 제너레이터 2개로 시작하는 for 표현식의 변환 >

for ( x <- expr1 ; y <- expr2; seq ) yield expr3

=>

expr1.flatMap( x => for ( y <- expr2; seq ) yield expr3 ) 

: 위 3가지 변환을 통해 제너레이터와 필터로 구성되고 제너레이터가 단순한 변수에 값을 바인딩하는 모든 for 표현식을 변환할 수 있다.

Ex.

for (b1 <- books; b2 <- books if b1 != b2;
     a1 <- b1.authors; a2 <- b2.authors if a1 == a2)
  yield a1


books flatMap (b1 =>
  books withFilter (b2 => b1 != b2) flatMap (b2 =>
    b1.authors flatMap (a1 =>
      b2.authors withFilter (a2 => a1 == a2) map (a2 =>
        a1))))

 

< 제너레이터에 있는 패턴의 변환 >

: 제너레이터의 좌항이 단순한 변수가 아니고 패턴 pat라면 변환 방법이 복잡

Ex. 변수의 튜플에 바인딩하는 제너레이터

for ( ( x1,  ...  ,  xn)  <- expr1 ) yield expr2

=>

expr1.map { case ( x1, ... , xn) => expr2 }

Ex. 임의의 패턴 pat라면

for ( pat <- expr1 ) yield expr2

=>

expr1 withFilter {

  case pat => true

  case _ => false

} map {

   case pat => expr2

}

: 제너레이터가 만들어내는 원소들 중에서 pat와 일치하는 것만 걸러내서 map에 넘긴다.

: 결코 MatchError가 발생하지 않는다.

 

< 정의 변환 >

for ( x <- expr1; y = expr2; seq) yield expr3

=>

for ( ( x,y ) <- for ( x<- expr1)  yield (x, expr2); seq)   yield expr3

: expr2가 계산되는 시점은 새로운 x 값을 제너레이터에서 가져올 때 => expr2 안에서 x를 사용한다면 각 이터레이션마다 가져온 값을 반영한다.

: for 표현식에 어떤 정의를 넣을 때 그 정의보다 앞에 있는 제너레이터가 바인딩한 변수를 사용하지 않는 정의를 넣는 것은 바람직하지 않다.

for (x <- 1 to 1000; y = expensiveComputationNotInvolvingX)
  yield x * y

" 권장 "
val y = expensiveComputationNotInvolvingX
for (x <- 1 to 1000) yield x * y

 

< for 루프 변환 >

: 아무 값도 돌려주지 않고(yield를 사용하지 않고) 부수 효과만 사용하는 for 루프는 foreach만 사용한다.

for ( x <- expr1 ) body

=>

expr1 foreach ( x => body )

 

for ( x<-expr1; if expr2; y <- expr3 ) body

=>

expr1 withFilter ( x => expr2 ) foreach ( x =>

     expr3 foreach ( y => body ))

 

23.5 역방향 적용

: 모든 map, flatMap, filter 호출은 for 표현식으로 표현할 수 있다.

object Demo {
  def map[A, B](xs: List[A], f: A => B): List[B] =
    for (x <- xs) yield f(x)

  def flatMap[A, B](xs: List[A], f: A => List[B]): List[B] =
    for (x <- xs; y <- f(x)) yield y

  def filter[A](xs: List[A], p: A => Boolean): List[A] =
    for (x <- xs if p(x)) yield x
}

 

23.6 for 일반화

: 스칼라 컴파일러는 for 표현식을 오직 map, flatMap, withFilter 메소드만을 사용해 변환한다.(for 루프는 foreach) 사용자가 정의한 데이터 타입 안에 map, flatMap, withFilter, foreach 메소드를 정의하면 for 표현식을 완벽히 지원할 수 있다.

: 리스트나 배열, 스칼라 표준 라이브러리에는 네 가지 메소드를 정의했기 때문에 for 표현식/루프 사용이 가능하다.

: 네 가지 메소드 중 일부만 정의했다면 for 표현식/루프 기능 중 일부만 사용 가능하다.

 

- 정확한 규칙

1. map만 정의했다면 제너레이터가 1개만 있는 for 표현식을 사용할 수 있다.

2. map과 flatMap을 함께 정의했다면 제너레이터가 여럿 있는 for 표현식도 사용 가능하다.

3. foreach를 정의했다면 for 루프(제너레이터 개수는 관계없음)를 사용할 수 있다.

4. withFilter를 정의하면 for 표현식 안에서 if로 시작하는 for 필터 표현식을 사용할 수 있다.

 

- 컴파일러는 for 표현식에서 고차 함수로의 변환을 타입 검사 이전에 수행한다. 그렇기 때문에 최대한의 유연성을 제공할 수 있다. for 표현식을 변환해 나온 결과만을 타입 검사하면 되기 때문이다. 스칼라에는 for 표현식 자체에 대한 타입 검사 규칙은 없다. 또한 map, flatMap, withFilter, foreach가 특정 타입 시그니처를 만족하도록 요구하지도 않는다.

 

- for 표현식을 변환할 때 사용하는 고차 메소드들의 가장 일반적인 의도를 담는 전형적인 설정이 있다.

abstract class C[A] {                     // 어떤 종류의 컬렉션을 나타내는 파라미터화된 클래스
  def map[B](f: A => B): C[B]
  def flatMap[B](f: A => C[B]): C[B]
  def withFilter(p: A => Boolean): C[A]
  def foreach(b: A => Unit): Unit
}

: withFilter 호출 뒤에는 항상 map, flatMap, foreach 중 하나가 호출되어 withFilter가 만든 객체는 그 즉시 다른 메소드로 인해 사용된다. => withFilter와 map/flatMap/foreach 사이에 withFilter로 생긴 새로운 컬렉션에 대한 작업이 없다. => 새로운 컬렉션을 만들지 말고 withFilter로 반환된 원소를 즉시 map/flatMap/foreach에 적용한다.

=> 이를 위한 표준적인 방법은 withFilter가 C를 반환하는 대신 나중에 처리할 수 있는 필터링된 원소를 기억하는 래퍼 객체를 만들어 반환하는 것이다.

 

* 모나드

- 타입을 인자로 받는 타입이다. (M[T])

- 임의 타입의 값을 받아서 그 타입을 인자로 받은 모나드 타입의 값을 반환하는 함수가 있어야 한다. ( T 타입의 값을 받아서 M[T] 타입의 값을 반환하는 함수 - unit operator )

- T 타입의 변수를 받아 M[U] 타입의 모나드를 반환하는 함수를 인자로 받아 M[U] 타입의 값을 반환하는 함수가 있어야 한다.( bind operator ) => 모나드에서 다른 모나드로 진행할 수 있다.

: 타입을 포함하는 컬렉션이 대표적이고 map, flatMap, withFilter을 통해 상태나 I/O, 백트래킹, 트랙잭션을 수행한다. => for 표현식의 개념이 단순히 컬렉션에 대해 이터레이션하는 것보다 더 일반적인 개념

: map, flatMap, withFilter에다가 원소 값을 하나 받아 모나드를 만들어 주는 unit operator을 추가하면 모든 모나드를 표현할 수 있다. => unit operator은 인스턴스 생성자/ 팩토리 메소드 => map, flatMap, withFilter는 모나드 개념을 객체지향에 적용한 것

 

+ Recent posts