Programming in scala 정리 (2)

11장부터 이어서 계속

Scala’s Hirarchy

  • Any: abstract class, 모든 타입의 부모 타입
    • ==, !=, equals, hashCode, toString: 공통 함수들
  • AnyVal: abstract class, 모든 vaule 타입의 부모 타입
    • value 타입은 비교시 값을 직접 비교함
    • value 타입은 new로 인스턴스 생성 못함
  • AnyRef: class, 모든 ref 타입의 부모 타입
    • ref 타입은 비교시 주소를 비교함
  • Null: abstract class, 모든 ref 타입의 자식 타입
  • null: object, Null의 유일한 인스턴스, value 타입에 대입 불가
  • Nothing: abstract class, 모든 타입의 자식 타입, 인스턴스 없음
  • Unit: abstract class, Vaule 타입임, 함수의 리턴이 없음을 나타냄
    • 모든 객체가 Unit으로 암시적으로 변환될 수 있으며, 이 경우 데이터가 삭제됨
  • None: object, nullable한 비어있는 값을 리턴할 때 사용됨

Value type

  • 컴파일러가 자바 바이트코드로 변환시 알아서 처리
  • 대부분 primitve로 처리해서 효율성 추구
  • 필요할 때는 java.lang.Integer로 박싱됨
    • 확장 함수를 부르거나
    • Any로 대입될때
  • 동등성은 값을 기준으로 계산됨

Traits

  • class와 거의 동일한 기능을 할 수 있음
    • 메소드, 필드 선언 등등
  • java8의 interface와 매우 유사하다.
    • 디폴트 메소드를 허용하며
    • rich interface를 추구한다.
  • 중요한 기능들
    • 다중상속: extends, with
    • 인스턴스에 붙이기 가능
    • 동적 super call 바인딩
    • Stackable
  • 한가지 단점
    • 외부에서 사용 불가능, 밖에 제공할 목적이면 class 써야함
  Traits Abstract Class
multiple inheritance support not support
add to instance possible impossible
constructor parameters impossible to have possible to have
Java interoperable not interoperable interoperable
super call binding dynamic static
Stackable stackable not stackable

Stackable Modification

  • 다중 상속이 가능
  • 다중 상속으로 인한 함수 이름 충돌시, 기본적으로 컴파일 에러가 남
  • 같은 class 를 상속받는 경우
    • 같은 이름의 함수를 확장할 수 있음
    • 더 나중(오른쪽)에 붙인 trait의 함수가 먼저 호출됨
    • super는 왼쪽 trait의 함수를 지칭함
  • abstract class를 상속받고, 구현이 존재하지 않는 함수가 충돌할 경우
    • abstract override로 함수 선언해야함
    • 이 경우에도 super를 쓸 수 있음
    • 그리고 함수가 런타임에 바인딩됨
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// Example 1: compile ok
class X
class Y
object O extends X with Y

// Example 2: function name conflict
trait X { def f() = print("X") }
trait Y { def f() = print("Y") }
object O extends X with Y // error

// Example 3: ok with base class
class Base { def f() = print("B") }
trait X extends Base { override def f() = print("X") }
trait Y extends Base { override def f() = print("Y") }
object O extends X with Y
O.f() // output: Y

// Example 4: cascade super call
class Base { def f() = print("B") }
trait X extends Base { override def f() = { print("X"); super.f() } }
trait Y extends Base { override def f() = { print("Y"); super.f() } }
object O extends X with Y
O.f() // output: YXB

// Example 5: abstract override function
abstract class Base { def f(): Unit }
class I extends Base { def f() = print("I") }
trait X extends Base { abstract override def f() = { print("X"); super.f() } }
trait Y extends Base { abstract override def f() = { print("Y"); super.f() } }
val x = new I with X with Y
x.f() // output: YXI

Linearliztion

  • 다중상속으로 얽히 Trait의 호출 관계를 풀어내는 과정
  • 호출시에는 오른쪽에서 왼쪽으로
  • 선형화로 순선 정할시에는 왼쪽에서 오른쪽으로
    • 앞에서 이미 탐색한 지점을 만날때까지 루트로부터 최단경로를 앞에다가 붙여주면 됨
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class B { def f() = print("B") }
trait X extends B { override def f() = { print("X"); super.f() } }
trait Y extends B { override def f() = { print("Y"); super.f() } }
trait T extends X { override def f() = { print("T"); super.f() } }
trait U extends X with Y { override def f() = { print("U"); super.f() } }

(new B with T).f() // TYB
  // B: B
  // T: T -> Y -> B

(new B with Y with T).f() // TYB
  // B: B
  // Y: Y -> B
  // T: T -> Y -> B

(new B with T with Y).f() // TYB
  // B: B
  // T: T -> Y -> B
  // Y: T -> Y -> B

(new B with U).f() // UYXB
  // B   : B
  // U(X): X -> B
  // U(Y): Y -> X -> B
  // U   : U -> Y -> X -> B

(new B with T with U).f() // UXTYB
  // B   : B
  // T   : T -> Y -> B
  // U(X): X -> T -> Y -> B
  // U(Y): X -> T -> Y -> B
  // U   : U -> X -> T -> Y -> B

Packages and Imports

package

  • 자바랑 똑같이 제일 앞줄에 패키지 써주면 됨
  • 중첩 형식 가능
    • C++의 namespace처럼 쓸 수 있고, 스코프를 공유함
    • 즉 중첩되어 있을때는 루트부터 경로를 쓰지 않아도 됨
    • _root_로 최 외곽 패키지에도 접근 가능

import

  • 파이썬처럼 on-demand로 import가 가능
  • 와일드카드 혹은 선택적 임포트
    • import A._ 패키지 내의 모든것 가져오기
    • import A.{B, C} 선택적으로 가져오기
    • import A.{B => D} 이름 바꿔서 선택적으로 가져오기
    • import A.{B => D, _} 이름 바꾸고, 전체 다 가져오기
    • import A.{B => _, _} B는 제외하고 다 가져오기
  • 3개 패키지는 암시적으로 import됨
    • java.lang._, scala._, Predef._

Access Modifier

  • 블록단위의 스코프를 가짐
  • 블록 내에서 변수를 재정의 할 수 있음(like C++, not-like Java)
    • 인터프리터 내에서는 항상 재정의 가능
  • 클래스 변수의 접근 제한자 별 스코프는 아래 표와 같음
    • top-level private과 protected는 같은 효과를 냄
    • 참고 블로그
    • protected는 java 와 효과범위가 다름
  • private[X]로 접근 가능 범위를 제한할 수 있음
    • X는 패키지, 클래스 this가 될 수 있음
Modifier Class Companion Subclass Package World
no modifier O O O O O
protected O O O X X
private O O X X X
top-level private O O O O X

Assertions and Unit Testing

  • 생략

Case Classes and Pattern Matching

case class

  • case class로 선언, 클래스에 여러 기능 추가
  • val없이 선언해도 인자들이 읽기전용 필드로 등록됨
  • equals, hashCode, copy, toString 추가
  • 컴패니언 객체에 apply등록: new 없이 생성 가능
  • 컴패니언 객체에 unapply등록: 패턴 매칭에 사용 가능해짐

Pattern matching

  • a match { case ... } 구문을 이용해 패턴매칭이 가능함
  • if문, 변수 선언을 효과적으로 줄일 수 있는 강력한 기능 제공
  • matchcase는 별개로 사용 가능
  • pattern guard로 case 와 함께 if를 사용 가능
    • 조건에 합치되지 않을 경우 패턴을 스킵하고 다음 패턴을 봄
  • 패턴의 중첩 여부를 컴파일러가 알고 있음
    • unreached 패턴이 나오면 컴파일 에러가 발생

Pattern의 종류

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// (1) wildcard pattern
// 나머지 모든 값에 매칭
case _ =>
// 인자의 값을 특정하거나 변수로 받지 않을 때
case BinOp(_, _, _) =>

// (2) Constant pattern
case 5 =>
case true =>
case Nil =>
// 대문자로 시작하는 변수일 경우 상수로 간주
case Pi =>
// back tick으로 묶인 겨우 상수로 간주
case `pi` =>

// (3) Variable pattern
// 변수를 받아서 거기다 집어넣어줌
case pi =>
// 특정 타입을 명시 가능
case pi: Double =>
// 단 제너릭의 경우 런타임 타입이 삭제되므로 주의
case arr: Array[Double] =>

// (4) Constructor pattern
// 변수 상수를 조합한 깊은 매칭이 가능
case BinOp("+", e, Number(0)) =>
// List 처럼 가변인자를 가진 생성자에도 가능
case List(0, _, _) =>
case List(0, _*) =>

// (5) Tuple pattern
// 순서쌍, 튜플에도 적용 가능
case (a, b, c)

// (6) Variable bnding
// @ 를 사용해서 복잡한 매칭의 특정 부분을 변수에 바인딩 가능
case UnOp("abs", e @ UnOp("abs", _))

// pattern guard
case n: Int if 0 < n =>

sealed class

  • sealed를 붙이면 case 클래스들을 enum처럼 쓸 수 있음
  • kotlin 처럼 다중 중첩하는것은 권장되지 않음
  • 한 파일 내에서만 확장 가능, 혹은 블록으로 묶어서 확장
  • 컴파일러가 빠진 케이스에 대해 오류 뿌려줌
  • @uncheched를 쓰면 조건 누락 오류 무시 가능

Option type

  • Kotlin의 ? nullable 타입과 동일함
  • ?.처럼 널접근 연산자는 없음(만들어 쓰면 되긴 함)
  • None을 null대신 씀
  • Some(Value)
    • 생성자 패턴으로 캡슐화를 풀어 패턴매칭
    • nullable로 만들거나

Patterns everywhere

  • variable binding
    • c++의 structured binding과 같은 컨셉
    • 단 생성자, tuple 패턴과 조합해서 폭넓게 활용 가능
  • case를 partial function으로 활용
    • case 블록을 함수의 body로 쓸 수 있음
    • 인자가 한개인 함수로 취급되며
    • 인자는 자동으로 case 문들과 매칭 시도함
    • 마치 람다처럼 사용하는 것이 가능
  • for문에서 사용 가능
    • <-로 변순 대입받을때 사용 가능
    • variable binding과 동일하게 작동함
    • 단 매칭되지 않을 경우, 해당 요소를 스킵해버림
1
2
3
4
5
// partial function with case
val withDefault: Option[Int] => Int = {
  case Some(x) => x
  case None => 0
}

Working with Lists

  • 충격적이게도, 기본 리스트가 연결리스트이다.
  • random access, last(), 심지어 length 에 선형시간이 든다.
  • 리스트는 재귀적으로 선언되어 있으므로
    • 함수형 프로그래밍과 찰떡궁합이긴 하다.
  • 타입 파라미미터는 공변성을 보장한다.
    • B >: A일 때 List[B] >: List[A]임을 보장해준다.
  • a :: xs로 리스트 앞에 원소 추가 가능 $O(1)$
  • xa ::: xb로 두 리스트를 이어붙이기 가능 $O(xa)$
  • head 가장 앞 원소, tail 앞 원소 제외한 뒤쪽 리스트
    • last, init는 가장 뒤 원소와, 그것을 뺀 앞쪽 리스트 (선형시간 걸림)
    • reverse, drop, take, splitAt, zip등의 멤버함수 있음
    • map, filter등 자바 스트림 함수들도 있음
    • List.range, List.make: 팩토리 함수들
  • 패턴 매칭 가능
    • scala.::가 정의되어 있기 때문
    • case a :: xs => 처럼 쓸 수 있음
  • reduce 연산자 /:, :\
    • 트리 모양을 연상할것
    • /:는 왼쪽부터 계산
    • :\는 오른쪽부터 계산
    • 초기값 /: 리스트 ((누적값, 현재 요소) => 새 누적값)
1
2
3
def sum(xs: List[Int]): Int = (0 /: xs) (_ + _)
def flatten[T](xss: List[List[T]]) = (xss :\ List[T]()) (_ ::: _)
def reverseLeft[T](xs: List[T]) = (List[T]() /: xs) {(ys, y) => y :: ys}

Collections

  • Iterable: 순회 가능한 최상위 trait
    • map, flatMap, filter등 제공
  • Iterable의 자손 trait
    • Seq: 순서가 존재
    • Set: 존재 여부를 저장
    • Map: key-value를 저장
  • Iterator
    • 한번만 순회 가능한 콜랙션
    • next, hasNext 메서드로 순회함
  • kotlin의 불변 자료구조와 달리, 불변 자료구조가 데이터 변형 API를 제공해줌
    • 예를 들면 List는 불변이지만, ::로 늘릴 수 있음
    • ArraySeq, Queue, Stack 모두 불변이지만, 값을 변경한 새로운것 생성 가능
  • scala collection은 거의 타입 파라미터의 공변성을 보장
    • 불변 자료구조는 타입 공변성이 보장됨
    • 그러므로 타입이 짬뽕된 리스트를 만드는게 가능함
    • "abc" :: List(1,2) == List<Matchable>("abc", 1, 2)
    • 공변성이 발동할 경우 두 타입의 LCA를 찾는듯 함
    • kotlin에서는 불변 자료구조의 수정이 불가능해서 생각할 필요가 없었음
  • 아래 파트는 책이 내용이 너무 오래 전 것이라 스칼라 공식문서를 참고함

immutable sequence

  • List
    • 연결리스트, 재귀적 구조
    • head, tail, preappend에 상수시간이 걸림
    • 임의 접근, length 등에 선형시간이 걸림
  • LazyList (구 Stream)
    • #:: 혹은 LazyList.cons로 이어서 만든다.
    • LazyList.emptyNil을 대체
    • #::의 앞뒤로 들어가는 head와 tail모두 call-by-name
    • 즉 값의 산출이 직접 읽을때만 이뤄진다.
    • 무한정 늘어나는 수열을 다룰때 유용하다.
  • ArraySeq
    • Java의 Array와 거의 똑같은 고정 크기 배열이다.
    • head, apply: 임의 접근이 상수시간만에 가능
    • 단 복사 후 변경을 위한 함수들을 제공 (tail, update, prepend, append)
  • Vector
    • balacned radix tree이 내부 구현됨
    • 32개 자녀를 가지는 tree node들로 구성됨
    • radix tree이므로 모든 연산이 상수시간
    • 변경, 추가 모두에서 딱 필요한 부분만 변경해줌
    • 단 상수 자체가 크므로 필요할때만 쓰는게 좋음
  • immutable Queue
    • 내부 구현은 두개의 List, in, out을 유지하도록 되어있음
    • in에 쭉 밀어넣다가, 빼내야 할 때, in에 있는것을 뒤집어서 out에 넣어줌
    • enqueue, dequeue 모두 리턴을 받아서 써야함
    • enqueue는 상수 시간만 걸림
    • dequeue는 amortized 상수 시간이 걸림
    • dequeue는 실질적으로 Queue의 headtail을 나누는 것임
    • head, tail, append 제외한 나머지는 선형시간이 걸림

immutable map/set

  • immutable Set, Map 모두 공통적으로 + 연산으로 확장함
    • + 된 결과를 새 변수에 리턴받는 식으로 확장
    • var로 선언한 경우 심지어 +=도 사용 가능
    • var+= 사용하면, 공변성은 제대로 작동 안함
  • HashSet, HashMap
    • 순서 보존 안되는 해시 자료구조
    • 내부적으로는 32개 자녀를 가지는 tree 구조임
    • 5개 미만의 경우, 따로 전용 클래스로 생성됨
    • 마찬가지로 새 요소를 추가한 후 리턴받는 식으로 확장
  • TreeSet, TreeMap
    • balanced binary red-black tree로 구현됨
    • 마찬가지로 새 요소를 추가한 후 리턴받는 식으로 확장
    • 모든 연산이 로그 시간 안에 완료됨
  • BitSet
    • 비트맵으로 Set을 저장
    • density가 높을 때 유리하다.
    • lookup은 상수시간으로 매우 빠르고,
    • add, remove는 복사해야 하므로 비트맵이 커질수록 느려진다.
  • VectorMap, LinkedHashMap
    • hash-map에 순서를 보존하는 특성을 붙인것
    • hash-map 앞단에 vector가 있는지, 연결 리스트가 있는지에 따라 달라짐
  • ListMap
    • 그냥 리스트로 페어를 저장한 것
    • 거의 사용되지 않음, 모든 연산이 선형 복잡도
    • 제일 앞 요소를 압도적으로 많이 접근할 때 사용

mutable sequence

  • immutable과 달리 내부 데이터를 직접 바꿀 수 있음
  • ArrayBuffer, StringBuilder
    • c++ vector와 동일한 사이즈를 2배씩 키우는 가변 길이 배열
    • 뒤쪽 삽입, 임의 접근은 상수 시간
    • 앞이나 중간에 삽입은 선형 시간
    • StringBuilder는 타입이 char로 고정되고, 여러 기능이 추가됨
  • ListBuffer
    • 내부에 배열 대신 리스트를 사용함
    • 가장 앞 조회, 앞뒤로 붙일때 상수 시간
    • 나머지 모든 연산은 선형시간 걸림
    • ListBuffer로 쌓다가, 최종적으로 List로 바꿀때 유리
  • ArrayDeque, Stack, Queue
    • ArrayDeqeue = resizable circular buffer
    • Stack, QueueArrayDequeue를 사용함
    • 특성이 거의 ArrayBuffer와 같음
    • 다만 prepend에 상수 시간만 걸림

mutable set/map

  • ArraySeq, HashMap, HashSet
    • immutable인 경우와 모든 특성이 같음
    • 다만 값을 수정할 수 있음
  • WeakHashMap
    • 다른 참조가 없으면 GC 대상이 되도록 하는 해시맵
    • 캐시를 만들때 유용함
  • Concurrent Map
    • java버전: 읽기시에 락 걸지 말고, 쓰기시에만 락 검
    • TrieMap: 아예 락 프리로 구현함
  • mutable BitSet
    • 복사를 안해도 되므로, 수정이나 삭제에 상수 시간 복잡도
    • 나머지는 똑같음

Statful Objects

  • kotlin처럼 getter, setter로 프로퍼티를 생성 가능
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Thermometer {
  var celsius: Float = _
  def fahrenheit = celsius * 9 / 5 + 32
  def fahrenheit_=(f: Float) = {
    celsius = (f - 32) * 5 / 9
  }
  override def toString = s"$fahrenheit F / $celsius C"
}

val tm = Thermometer()   : Thermometer = 68.0 F / 20.0 C

tm.fahrenheit = 3
println(tm)   3.0 F / -16.11111 C
tm.celsius = 20
println(tm)   68.0 F / 20.0 C

Type Parameterization

  • java vs kotlin vs scala generics
    • 함수, 클래스 선언시 upper-bound를 정하는 기능: java, kotlin, scala
    • 함수, 클래스 선언시 lower-bound를 정하는 기능: scala, kotlin
    • 메소드 선언시 클래스의 타입 파라미터 이용해 bound 정하기: scala
    • use-site variance like <? extends Int>: java, kotlin
  • array variance
    • java = covariant
    • kotlin, scala = invariant
  • var on covariant
    • 그냥은 var x: T를 선언 못함
    • kotlin에서는 private var x: T로 하면 covariant 타입 선언이 가능함
    • scala에서는 private[this] var x: T로 해야함
    • companion object가 블록 밖에서 private 변수 접근이 가능하기 때문

Checking variance

  • variance 판단이 기본적으로 완전 복잡함
  • 클래스 내의 타입 위치에 따라 +, - 위치를 나눌 수 있음
    • +: 리턴타입, val, private[this] val 변수 타입, lower bound
    • -: 메소드 인자의 타입, 제너릭의 메소드의 타입 파라미터, upper bound
  • 반공변 위치에 있는 중첩된 타입 파라미터는 공변성이 역전됨
    • kotlin도 똑같지만, kotlin을 공부할때는 몰랐었음 -_-..
1
2
3
4
5
6
// 뒤쪽에 달린 부호는 각 위치의 속성
abstract class Cat[-T, +U] {
  def meow[W](volume: T−, listener: Cat[U+, T−]−) : Cat[Cat[U+, T−], U+]+
  def lower[W >: U+](x: W-): W+
  def upper[W <: T-](x: W-): W+
}

Lower/upper bound

  • 타입 파라미터에 lower/upper bound를 부여할 수 있다.
  • 특히 메소드에서는 클래스의 타입 파라미터를 바운드 조건으로 쓸 수 있다.
    • kotlin, java에서는 안되는 기능
  • +P를 인자로, -M은 리턴으로 못쓰지만
    • [W >: P], [W <: M]인 W는 인자, 리턴 다 자유롭게 쓸 수 있다.
1
2
3
4
5
6
7
8
9
10
11
case class Holder[+T](v: T, next: Option[Holder[T]] = None) {
  def prepend[U >: T](v: U) = Holder(v, Some(this))
}

val h = Holder(2)   : Holder[Int] = Holder(2,None)
h.prepend(3.0)      : Holder[AnyVal] = Holder(3.0,Some(Holder(2,None)))

abstract class Cat[-M, +P] {
  def lower[W >: P](x: W): W
  def upper[W <: M](x: W): W
}

Abstract Member

1
2
3
4
5
6
trait Abstract {
  type T
  def transform(x: T): T
  val initial: T
  var current: T
}
  • abstract type mebmers
    • 항상 class 혹은 trait의 멤버로만 쓸 수 있음
    • alias 로 사용하려는 목적
    • generic 처럼 타입 상한, 하한 정의 가능 type T <: Food
    • path-dependent type: 타입을 마치 변수처럼 활용할 수 있게 해줌
  • abstract val, var
    • val: subclass에 멤버 변수 선언을 강제하게 됨
    • var: subclass에 getter, setter 구현을 강제
  • trait의 abstract val 초기화
    • 생성자 사용이 불가능함
    • 생성 후 필드 초기화: new ExampleTrait { val valMember = 1 }
    • 초기화 후 생성 new { val valMember = 1 } with ExampleTrait
    • lazy val 활용
  • lazy val vs def
    • 한번 실행(lazy) vs 매번 실행(def)