jam 블로그

러닝스칼라 6장 - 보편적인 컬렉션 본문

IT Book Study/러닝 스칼라

러닝스칼라 6장 - 보편적인 컬렉션

kid1412 2020. 9. 28. 23:31
728x90

컬렉션(collection) 프레임워크는 배열, 리스트, 맵, 집합, 트리와 같이 주어진 타입을 가지는 하나 또는 그 이상의 값을 수집하는 데이터 구조를 제공한다. 1. 성능이 좋고, 객체지향적, 타입-매개변수화 - 자바 컬렉션 라이브러리 1. jvm이기 때문에 자바 컬렉션을 스칼라에서 활용 가능

스칼라는 여러 이유로 컬렉션을 재정의함 1. 고차함수 이용 (map, filter, reduce) 1. mutable/immutable 타입의 계층 구조

리스트, 집합, 그리고 맵

List 타입: immutable 단반향 연결 리스트

scala> val numbers = List(32, 95, 24, 21, 17)
numbers: List[Int] = List(32, 95, 24, 21, 17)

scala> val colors = List("red", "green", "blue")
numbers: List[String] = List(red, green, blue)

scala> println(s"I have ${colors.size} colors: $colors")
I have 3 colors: List(red, greem, blue)

모든 컬렉션은 size 메소드롤 통해서 포함된 데이터의 개수를 반환 가능하다.

컬렉션은 초기화때 사용했던 데이터의 타입을 기억하여 타입-매개변수화 될 수 있다.

scala> val colors = List("red", "green", "blue")
numbers: List[String] = List(red, green, blue)

scala> colors.head
res0: String = red

scala> colors.tail
res1: List[String] = List(green, blue)

scala> colors(1)
res2: String = green

scala> colors(2)
res2: String = blue

head와 tail메소드를 이용하여 리스트의 첫번쨰 요소와 나머지 요소를 반환할 수 있다.<collect>(index)를 통하여 0번째부터 컬렉션의 인덱스의 요소를 반환할 수 있다.

for 루프를 이용하여 컬렉션안의 요소를 순회 할 수 있다.

scala> val numbers = List(32, 95, 24, 21, 17)
numbers: List[Int] = List(32, 95, 24, 21, 17)

scala> val total=0; for(i <- numbers) {total += i}
total: Int = 189

고차함수 이용

스칼라의 컬렉션은 반복하고(iterate), 매핑하고(map,) 축소하기(reduce, fold)위하여 고차함수를 이용

scala> val colors = List("red", "green", "blue")
numbers: List[String] = List(red, green, blue)

scala> colors.foreach( (c: String) => println(c) )
red
green
blue

scala> val sizes = colors.map( (c: String) => c.size )
List[Int] = List(3, 5, 4)

scala> val numbers = List(32, 95, 24, 21, 17)
numbers: List[Int] = List(32, 95, 24, 21, 17)

scala> val total = numbers.reduce( (a: Int, b:Int) => a + b )
total: Int = 189
  1. foreach는 프로시져를 취하고, 해당 프로시져에 리스트의 요소를 주압
  2. map은 리스트의 요소를 다른 값이나 타입으로 전환하는 함수를 취함
  3. reduce는 리스트의 ㅇㅛ소를 단일 항목으로 결합하는 함수를 취함

Set 타입: immutable, 순서 없음, List와 동일연산 지원

scala> val unique = Set(10, 20, 30, 20, 20, 10)
unique: scala.collection.immutable.Set[Int] = Set(10, 20, 30)

scala> val sum = unique.reduce( (a: String, b: Int) => a + b )
sum: Int = 60

Map 타입: immutable, key-value, 해시맵, 딕셔너리, 연관배열등으로 알려짐

  • key는 유일키로 value와 연결된다.
  • Map을 생성 할때 kv쌍을 튜플로 기술하면 된다.
  • 키와 값의 관계는 -> 연산자로 정의된다.
scala> val colorMap = Map("red" -> 0xFF0000, "green" -> OxFF00, "blue" -> 0xFF)
    colorMap: scala.collection.immutable.Map[String, Int] = Map(red -> 16711680, green -> 65280, blue -> 255)
}

scala> val redRGB = colorMap("red")
redRGB: Int = 16711680

scala> val hasWhite = colorMap.contains("white")
hasWhite: Boolean = false

scala> for (pairs <- colorMap) { println(pairs) }
(red, 16711680)
(green, 65280)
(blue, 255)

리스트의 종류

  1. 일반적인 리스트 - List(1,2,3)
  2. 중첩된 리스트 - List(List(1,2), List(3), List(4,5))
  3. tuple 리스트 - List(('A',1), ('B', 2))
  4. 등 등 일관성 있는 매개변수 추론이 되는 요소의 리스트르 생성가능

isEmpty 메서드를 이용하여 리스트의 값 소유여부를 측정 가능하다.

scala> val primes = List(2, 3, 5, 7, 11, 13)
primes: List[Int] = List(2, 3, 5, 7, 11, 13)

scala> var i = primes
i: List[Int] = List(2, 3, 5, 7, 11, 13)

scala> while(! i.isEmpty) { print(i.head + ", "); i = i.tail}
2, 3, 7, 11, 13

리스트의 마지막을 검출하기 위해 isEmpty를 사용할 수 있지만 다른 방법으로 Nil인스턴스를 활용할 수 있다.
모든 리스트는 그 종점으로 Nil으로 종료된다.

scala> val primes = List(2, 3, 5, 7, 11, 13)
primes: List[Int] = List(2, 3, 5, 7, 11, 13)

scala> var i = primes
i: List[Int] = List(2, 3, 5, 7, 11, 13)

scala> while(i != Nil) { print(i.head + ", "); i = i.tail}
2, 3, 7

Nil은 List[Nothing]의 싱글턴 인스턴스, Nothing은 모든 스칼라 리스트에 호환되기 때문에 종점으로 안전히 사용가능하다.
새로운 빈 리스트를 생성하면 새로 생긴 인스턴스 대신 Nil을 반ㄹ환한다.
Nil은 immutable 데이터 구조기때문에 근본적으로 빈 리스트 인스턴스와 차이가 없다.

scala> val l = List[Int] = List()
l: List[Int] = List()

scala> l == Nil
res0: Boolean = true

생성 연산자

Nil의 속성을 이용하여 생성 연산자를 이용한 리스트를 생성 할 수 있다.
오른쪽-결합형(right-associative)인 :: 연산자를 이용하여 리스트를 생성 가능하다.

오른쪽-결합형(right-associative) 표기법 - 스칼라에서 연산자가 :로 끝나면 오른쪽 결합형으로 그 연산자의 오른쪽의 객체에서 호출 된다.

scala> val first = Nil.::(1)
first: List[Int] = List(1)

scala> first.tail == Nil
res3: Boolean = true

리스트 산술 연산

산술은 추가하고, 제거하고, 나누고, 결합하고, 구성 요소를 변경하는 등의 작업을 의미한다.
immutable형이기 때문에 실제로는 변경 사항을 반영한 새로운 리스트를 반환한다.

Name Example 설명
:: 1 :: 2:: Nil 리스트에 개별 요소를 덧붙임
::: List(1 2) ::: List(2, 3) 이 리스트 앞에 다른 리스트를 추가함
++ List(1, 2) ++ Set(3, 4, 3) 이 리스트에 다른 컬렉션을 덧붙임
== List(1, 2) == List(1, 2) 두 컬렉션의 요소를 비교
distinct List(3, 5, 4, 3, 4).distinct 중복 요소가 없는 리스트를 반환
drop List(1 ,2 , 3 ,4) drop 2 주어진 n개의 요소를 앞에서 부터 제거
filter List(23, 8, 14, 21) filter (_ > 18) 참/거짓 함수를 참으로 만족한 요소를 반환
flatten List(List(1,2), List(3,4)).flatten 중첩된 리스트의 요소를 단일 리스트로 전환함
partition List(1, 2, 3, 4, 5) partition (_ < 3) 참/거짓 기준으로 리스트를 2개로 나눔
reverse List(1,2,3).reverse 요소의 순서가 거꾸로인 리스트 반환
slice List(2,3,5,7) slice (1, 3) 첫번째, 두번째-1 사이의 요소의 리스트를 반환
sortBy List(“apple”, “to”) sortBy (_.size) 주어진 함수의 값을 이용하여 요소를 정렬함
sorted List(“apple”, “to”).sorted 자연값 기준 정렬
splitAt List(2, 3, 5, 7) splitAt 2 주어진 인덱스 기준으로 리스트를 2개로 나눔
take List(2, 3, 5, 7, 11, 13) take 3 리스트에서 첫번쨰 n개의 요소를 추출
zip List(1, 2) zip List(“a”, “b”) 두 리스트의 요소들을 튜플 결합한 리스트를 반한

연산자 vs 점 표기법 - 매개변수가 없어 . 표기법이 요구되는 경우를 제외하면 개인의 선택문제이다.

  • ::도 연산자이기 떄문에 순서가 역순이 되나 다음과 같은 수행도 가능하다.
scala> val numbers = 1 :: 2 :: 3 :: Nil
numbers: List[Int] = List(1, 2, 3)

리스트는 단일 연결리스트로 앞에서 연산하는 것이 이익이다.
::, drop, take는 앞에서 부터 연산되지만 간혹 전체 순회가 필요한 동반연산(corollary operation)이 필요할 수 있다.
:+(왼쪽 결합형 연산자, 책오타), dropRight, takeRight

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

scala> val suffix = appended takeRight 3
suffix: List[Int] = List(3, 4, 5)

scala> val middle = suffix dropRight 2
middle: List[Int] = List(3)

리스트 매핑

Map 메서드는 함수를 취하여 요소에 적용하고, 그 결과를 새로운 리스트에 수집한다.

Name Example 설명
collect List(0, 1, 0) collect { case => “ok” } 각 요소에 case 적용 뒤 적용된 요소를 반환
flatMap List(“milk,tea”) flatMap (_.split(“,”)) 주어진 함수를 이용하여 요소에 적용한 뒤 flatten함
map List(“milk,tea”) map (_.toUooerCase) 주어진 함수를 이용하여 각 요소를 변환함

리스트 축소

리스트를 단일 값으로 축소한다. 스칼라 컬렉션은 수학적 축소 연산과 부울 축소 연산을 지원한다.
또한 스칼라는 fold로 알려진 일반적인 고차 연산을 지원하는데, 이를 사용하여 임의의 다른 형태의 리스트 축소 알고리즘을 생성할 수 있다.

수학적 축소 연산

Name Example 설명
max List(41, 59, 26).max 리스트의 최댓값 추출
min List(10.9, 32.5, 4.23, 5.67).min 리스트의 최솟값 추출
product List(5, 6, 7).product 원소들의 곱을 반환
sum List(11.3, 23.5, 7.2).sum 원소들의 합을 반환

부울 축소 연산

Name Example 설명
contains List(34, 29, 18) contains 29 리스트가 해당 요소를 포함하는지 검사
endsWith List(0, 4, 3) endsWith List(4, 3) 리스트가 주어진 리스트로 끝나는지 검사
exists List(24, 17, 32 exists (_ < 18) 최소 하나의 요소가 주어진 조건 함수를 만족하는지 검사
forall List(24, 17, 32 forall (_ < 18) 모든 요소가 주어진 조건 함수를 만족하는지 검사
startsWith List(0, 4, 3) startsWith List(0) 리스트가 주어진 리스트로 시작하는지 검사

커링 활용

contains를 직접 구현한다고 가정해보자.

scala> def contains(x: Int, l: List[Int]): Boolean = {
  | var a: Boolean = false
  | for (i <- l) { if (!a) a = (i == x) }
  | a
}
contains: (x:Int, l: List[Int])Boolean

scala> val included = contains(19, List(46, 19, 92))
included: Boolean = true

요소 순회와 조건 함수를 다음과 같이 분리하여 개선 할 수 있다.

scala> def boolReduce(l: List[Int], start: Boolean)(f: (Boolean, Int) => Boolean): Boolean = {
  | var a = start
  | for (i <- l) a = f(a, i)
  | a
}
boolReduce: (l: List[Int], start: Boolean)(f: (Boolean, Int) => Boolean)Boolean

scala> val included = boolReduce(List(46, 19, 92), false) { (a, i) => if (a) a else (i == 19)}
included: Boolean = true

같은 방식으로 boolean 함수 뿐만이 아닌 reduce 연산에 일반화 시킬 수 있다.

scala> def reduceOp[A,B](l: List[A], start: B)(f: (B, A) => B): B = {
  | var a = start
  | for (i <- l) a = f(a, i)
  | a
}
reduceOp: [A, B](l: List[A], start: B)(f: (B, A) => B)B

scala> val included = reduceOp(List(46, 19, 92), false) { (a, i) => if (a) a else (i == 19)}
included: Boolean = true

scala> val answer = reduceOp(List(11.3, 23.5, 7.2), 0.0)(_ + _)
answer: Double = 42.0

list-folding (fold 함수)

스칼라 컬렉션은 이미 다음과 같은 내장 연산을 제공하고 있다. 내장 연산들은 왼쪽에서 오른쪽, 오른쪽에서 왼쪽, 순서와 무관한 버전을 모두 제공할 만큼 유연하다.

Name Example 설명
fold List(4, 5, 6).fold(0)(_ + _) 초기값을 기준으로 리스트 축소
foldLeft List(4, 5, 6).foldLeft(0)(_ + _) 초기값을 기준으로 왼쪽에서 오른쪽으로 리스트 축소
foldRight List(4, 5, 6).foldRight(0)(_ + _) 초기값을 기준으로 오른쪽에서 왼쪽으로 리스트 축소
reduce List(4, 5, 6).reduce(_ + _) 페어요소 별 리스트 축소
reduceLeft List(4, 5, 6).reduceLeft(_ + _) 첫번째 요소 기준으로 왼쪽에서 오른쪽으로 리스트 축소
reduceRight List(4, 5, 6).reduceRight(_ + _) 마지막 요소 기준으로 오른쪽에서 왼쪽으로 리스트 축소
scan List(4, 5, 6).scan(0)(_ + _) 초기값 기준으로 축소 함수를 취하여 각각의 누적 리스트를 반환
scanLeft List(4, 5, 6).scanLeft(0)(_ + _) 초기값 기준으로 축소 함수를 취하여 왼쪽 부터 오른쪽으로 수행 후 각각의 누적 리스트를 반환
scanRight List(4, 5, 6).scanRight(0)(_ + _) 초기값 기준으로 축소 함수를 취하여 오른쪽 부터 왼쪽으로 수행 후 각각의 누적 리스트를 반환

컬렉션 전환

컬렉션들은 서로 다른 타입의 컬렉션으로 전환 될 수 있다.

Name Example 설명
mkString List(24, 99, 104).mkString(“,”) 주어진 구분자로 컬렉션을 String으로 전환
toBuffer List(‘f’, ‘t’).toBuffer immutable을 mutable로 전환
toList Map(“a” -> 1, “b” -> 2).toList 컬렉션을 List로 전환
toMap Set(1 -> true, 3 -> true).toMap 두 요소 튜블로 구성된 컬렉션을 Map으로 전환
toSet List(2, 5, 3, 2).toSet 컬렉션을 Set으로 전환
toString List(2, 5, 5, 3, 2).toString 컬렉션 타입을 포함하여 콜렉션을 문자열로 전환

자바와 스칼라 컬렉션 호환성

스칼라는 jvm으로 컴파일되고 그 위에서 동작한다.
즉 jdk와 상호작용뿐 아니라 어떤 자바 라이브러리도 추가 할 수 있어야한다는 것이 보편적인 요구사항이다.
그러나 자바 컬렉션과 스칼라 컬렉션의 타입은 기본적으로 호환 되지 않는다.

자바와 스칼라 컬렉션 사이의 전환을 하려면 다음 명령어를 추가해야한다.

import collection.JavaConverters._

collection.JavaConverters의 메서드가 추가 되면 자바와 스칼라 컬렉션에 다음 연산을 사용할 수 있다.

Name Example 설명
asJava List(12, 29).asJava 스칼라 컬렉션을 대응되는 자바 컬렉션으로 전환
asScala new java.util.ArrayList(5).asScala 자바 컬렉션을 대응되는 스칼라 컬렉션으로 전환

컬렉션으로 패턴 매칭하기

이전에 배웠던 단일값 패턴 매칭은 다음과 같다.

scala> val statuses = List(500, 404)
statuses: List[Int] = List(500, 404)

scala> msg = statuses.head match {
  | case x if x < 500 => "okay"
  | case _ => "whoah, an error"
}
msg: String = whoah, an error

하지만 컬렉션의 값 중 하나의 값밖에 검사할 수 없다. 다음은 개선 버전이다.

scala> msg = statuses match {
  | case x if x contains(500) => "has error"
  | case _ => "okay"
}
msg: String = has error

이와 다르게 값 바인딩을 통하여 패턴 가드에서 컬렉션의 일부 또는 모든 요소에 값 바인딩이 가능하다.

scala> msg = statuses match {
  | case List(500, x) => s"Error followed by $x"
  | case List(e, x) => s"$e was followed by $x"
}
msg: String = Error followed by 404

리스트는 head와 tail로 분해 가능하기 떄문에 다음과 같은 매칭도 가능하다.

scala> val head = List('r', 'g', 'b') match {
  | case x :: xs => x
  | case Nil => ' '
}
head: Char = r

튜플도 값 바인딩을 이용한 패턴 매칭을 지원한다. 패턴 매칭은 스칼라 표준 컬렉션 라이브러리의 평범한 연산이 아니라 함수형 언어의 핵심적 특징이다.

Comments