Programming in Sala 책으로 스칼라 스터디하면서 정리했던 내용이다. 지금은 3판 번역본도 나왔지만, 약간 앞서서 스터디를 시작해서 2판으로 진행했다.

18장

변경 가능한 상태

  • 순수 함수형은 변경 가능한 상태가 없음.
  • var 이 있더라도 캐시 등의 용도라면 순수 함수형일 수 있음.
  • var 이 없더라도 변경 가능한 상태를 가진 다른 객체에게 호출을 위임하면서 변경 가능한 상태를 가질 수 있음.

재할당 가능한 변수

  • 비공개가 아닌 모든 var 멤버에 게터/세터 메소드를 자동으로 정의함.
  • var x의 게터는 x, 세터는 x_=
  • 게터와 세터는 원래 var과 같은 가시성을 가지고, 필드는 private[this]가 붙음
class Time {
  var hour = 12
  var minute = 0
}

// 실제 자동 확장
class Time {
  private[this] hour = 12
  private[this] minute = 0

  def hour: Int = h
  def hour_= (x: Int) { h = x }

  def minute: Int = m
  def minute_= (x: Int) { m = x }
}

// 직접 정의
class Time {
  private[this] hour = 12
  private[this] minute = 0

  def hour: Int = h
  def hour_= (x: Int) {
    require(0 <= x && x < 24)
  }

  def minute: Int = m
  def minute_= (x: Int) {
    require(0 <= x && x < 60)
    m = x
  }
}
  • 연관된 필드 없이 게터/세터 정의
class Thermometer {
  var celsius: Float = _    // 초기화 값.

  def fahrenheit = celsius * 9 / 5 + 32
  def fahrenheit_= (f: Float) {
    celsius = (f - 32) * 5 / 9
  }

  override def toString = fahrenheit +"F/"+ celsius +"C"
}
  • 필드 초기화 값
    • 수 타입 : 0
    • 불리언 타입 : false
    • 레퍼런스 타입 : null
  • = _ 초기화를 생략하면 추상 변수(20장)를 선언함

예 : 이산 이벤트 시뮬레이션

package org.stairwaybook.simulation

abstract class Simulation {
  type Action = () => Unit
  case class WorkItem(time: Int, action: Action)
  
  private var curtime = 0
  def currentTime: Int = curtime
  private var agenda: List[WorkItem] = List()
  
  private def insert(ag: List[WorkItem], item: WorkItem): List[WorkItem] = {
    if (ag.isEmpty || item.time < ag.head.time) item :: ag
    else ag.head :: insert(ag.tail, item)
  }

  def afterDelay(delay: Int)(block: => Unit) {
    val item = WorkItem(currentTime + delay, () => block)
    agenda = insert(agenda, item)
  }

  private def next() {
    (agenda: @unchecked) match {
      case item :: rest =>
        agenda = rest
        curtime = item.time
        item.action()
    }
  }

  def run() {
    afterDelay(0) {
      println("*** simulation started, time = "+ currentTime +" ***")
    }
    while (!agenda.isEmpty) next()
  }
}

abstract class BasicCircuitSimulation extends Simulation {
  def InverterDelay: Int
  def AndGateDelay: Int
  def OrGateDelay: Int

  class Wire {
    private var sigVal = false
    private var actions: List[Action] = List()
    
    def getSignal = sigVal
    
    def setSignal(s: Boolean) {
      if (s != sigVal) {
        sigVal = s
        actions foreach (_ ())
      }
    }
    
    def addAction(a: Action) {
      actions = a :: actions
      a()
    }
  }

  def inverter(input: Wire, output: Wire) {
    def invertAction() {
      val inputSig = input.getSignal
      afterDelay(InverterDelay) {
        output setSignal !inputSig
      }
    }
    input addAction invertAction
  }

  def andGate(a1: Wire, a2: Wire, output: Wire) {
    def andAction() = {
      val a1Sig = a1.getSignal
      val a2Sig = a2.getSignal
      afterDelay(AndGateDelay) {
        output setSignal (a1Sig & a2Sig)
      }
    }
    a1 addAction andAction
    a2 addAction andAction
  }

  def orGate(o1: Wire, o2: Wire, output: Wire) {
    def orAction() {
      val o1Sig = o1.getSignal
      val o2Sig = o2.getSignal
      afterDelay(OrGateDelay) {
        output setSignal (o1Sig | o2Sig)
      }
    }
    o1 addAction orAction
    o2 addAction orAction
  }

  def probe(name: String, wire: Wire) {
    def probeAction() {
      println(name +" "+ currentTime +" new-value = "+ wire.getSignal)
    }
    wire addAction probeAction
  }
}

abstract class CircuitSimulation extends BasicCircuitSimulation {
  def halfAdder(a: Wire, b: Wire, s: Wire, c: Wire) {
    val d, e = new Wire
    orGate(a, b, d)
    andGate(a, b, c)
    inverter(c, e)
    andGate(d, e, s)
  }

  def fullAdder(a: Wire, b: Wire, cin: Wire, sum: Wire, cout: Wire) {
    val s, c1, c2 = new Wire
    halfAdder(a, cin, s, c1)
    halfAdder(b, s, sum, c2)
    orGate(c1, c2, cout)
  }
}
import org.stairwaybook.simulation._

object MySimulation extends CircuitSimulation {
  def InverterDelay = 1
  def AndGateDelay = 3
  def OrGateDelay = 5
}

import MySimulation._

val input1, input2, sum, carry = new Wire
probe("sum", sum)
probe("carry", carry)
halfAdder(input1, input2, sum, carry)
input1 setSignal true
run()
input2 setSignal true
run()


Programming in Sala 책으로 스칼라 스터디하면서 정리했던 내용이다. 지금은 3판 번역본도 나왔지만, 약간 앞서서 스터디를 시작해서 2판으로 진행했다.

17장

시퀀스

리스트 List

  • 앞부분에 빠르게 원소를 삭제하거나 추가할 수 있다. -> 패턴 매치 이점
  • 불변 객체
  • 임의 위치 접근 시에는 빠르지 않다.
val colors = List("red","blue","green")
colors.head     // red
colors.tail     // List("blue","green")

배열 Array

  • 임의 위치 효율적으로 접근
val fiveInts = new Array[Int](5)
val fiveToOne = Array(5, 4, 3, 2, 1)

fiveInts(0) = fiveToOne(4)

리스트 버퍼 ListBuffer

  • 가변 객체
  • 앞뒤로 원소를 추가하는 데 상수시간 소요
  • += : 원소를 리스트 뒤에 추가
  • +=: : 원소를 리스트 앞에 추가
import scala.collection.mutable.ListBuffer
val buf = new ListBuffer[Int]
buf += 1        // ListBuffer(1)
buf += 2        // ListBuffer(1,2)
3 +=: buf       // ListBuffer(3,1,2)
buf.toList      // List(3,1,2)

배열 버퍼 ArrayBuffer

  • 앞뒤로 원소를 추가/삭제 가능
  • 모든 배열 연산 사용 가능
  • 추가/삭제에 평균적으로 상수시간 걸리나 종종 선형 시간 필요
import scala.collection.mutable.ArrayBuffer
val buf = new ArrayBuffer[Int]()
buf += 12       // ArrayBuffer(12)
buf += 15       // ArrayBuffer(12,15)
buf.length      // 2
buf(0)          // 12

문자열 StringOps

  • StringStringOps로 암시적 변환하여 시퀀스처럼 문자열을 다룰 수 있다.
def hasUpperCase(s: String) = s.exists(_.isUpper)

집합 Set

  • 한 파일 안에서 가변/불변 집합을 다 사용하는 방법
import scala.collection.mutable
val mutaSet = mutable.Set(1,2,3)
  • 특정 객체는 최대 하나만 들어감. 동일한지는 ==로 비교
// 다른 단어 갯수 세기
val text = "See Spot run. Run, Spot. Run!"
val wordsArray = text.split("[ !,.]+")
val words = mutable.Set.empty[String]

for (word <- wordsArray) words += word.toLowerCase
words       // Set(spot, run, see)

집합 연산

  • val nums = Set(1,2,3) : 불변 집합 생성
  • nums + 5 : 원소를 추가
  • nums - 3 : 원소를 제거
  • nums ++ List(5,6) : 여러 원소 추가
  • nums -- List(1,2) : 여러 원소 제거
  • nums & Set(1,3,5,7) : 두 집합의 교집합
  • nums.size : 집합의 크기
  • nums.contains(3) : 집합에 원소가 있는지 확인
  • import scala.collection.mutable : 가변 컬렉션 접근
  • val words = mutable.Set.empty[String] : 빈 문자열 집합 생성
  • words += "the" : 원소 추가
  • words -= "the" : 원소 제거
  • words ++= List("do","re","mi") : 여러 원소 추가
  • `words --= List("do","re") : 여러 원소 제거
  • words.clear : 모든 원소 제거

맵 Map

def countWords(text: String) = {
  val counts = mutable.Map.empty[String, Int]
  for (rawWord <- text.split("[ ,!.]+")) {
    val word = rawWord.toLowerCase
    val oldCount =
      if (counts.contains(word))
        counts(word)
      else
        0
    counts += (word -> (oldCount + 1))
  }
  counts
}

맵 연산

  • val nums = Map("i" -> 1, "ii" -> 2) : 불변 맵 생성
  • nums + ("v" -> 6) : 원소 추가
  • nums - "ii" : 원소 제거
  • nums ++ List("iii"->3, "v"->5) : 여러 원소 추가
  • nums -- List("i", "ii") : 여러 원소 제거
  • nums.size : 맵 크기
  • nums.contains("ii") : 키 포함 여부
  • nums("ii") : 키에 해당하는 값
  • nums.keys : 모든 키의 iterable
  • nums.keySet : 모든 키의 Set
  • nums.values : 모든 값의 iterable
  • nums.isEmpty : 맵이 비었는지 검사
  • import scala.collection.mutable : 가변 컬렉션 접근
  • val words = mutable.Map.empty[String,Int] : 가변 맵 생성
  • words += ("one" -> 1) : 원소 추가
  • words -= "one" : 원소 제거
  • words ++= List("one" -> "two" -> 2, "three" -> 3) : 여러 원소 추가
  • words --= List("one", "two") : 여러 원소 제거

디폴트 집합/맵

가변 집합/맵

  • scala.collection.mutable.Set() -> scala.collection.mutable.HashSet
  • scala.collection.mutable.Map() -> scala.collection.mutable.HashMap

불변 집합/맵

  • 5개 이상 : 해시 사용하는 구현
  • 5개 이하 : 특별한 클래스
    • 0 : scala.collection.immutable.EmptySet / ... .EmptySet
    • 1 ~ 4 : scala.collection.immutable.Set1 / ... .Map1 ~ scala.collection.immutable.Set4 / / ... .Map4

정렬된 집합/맵

  • SortedSet / SortedMap 트레이트
  • TreeSet / TreeMap 클래스 : red-black tree 사용. Ordered 트레이트를 따라 순서 결정
// TreeSet
import scala.collection.immutable.TreeSet
val ts = TreeSet(9,3,1,8,0,2,7,4,6,5)   // TreeSet(0,1,2,3,4,5,6,7,8,9)
val cs = TreeSet('f','u','n')           // TreeSet(f,n,u)

// TreeMap
import scala.collection.immutable.TreeMap
val tm = TreeMap(3 -> 'x', 1 -> 'x', 4 -> 'x')  // Map(1 -> x, 3 -> x, 4 -> x)
tm += (2 -> 'x')
tm                                              // Map(1 -> x, 2 -> x, 3 -> x, 4 -> x)

가변/불변 컬렉션

  • 크키가 작은 맵/집합의 경우 불변인 경우 공간을 적게 차지함.
  • 불변 컬렉션의 지원하지 않는 a += b 연산자는 a = a + b로 표현식을 해석함.(=로 끝나는 모든 연산)
var people = Set("Nancy", "Jane")
people += "Bob"

컬렉션 초기화

  • 동반 객체 이름 뒤의 괄호 안에 모든 원소. -> apply 메소드 호출로 변환하여 처리함.
List(1,2,3)
Set('a','b','c')
  • 타입 추론이 정확하지 않을때에는 타입을 지정.
val stuff = mutable.Set[Any](42)
  • 리스트로 집합/맵 만들때는 ++ 연산자 활용
val colors = List("blue","yellow","red","green")
val treeSet = TreeSet[String]() ++ colors
  • 리스트/배열로 변환
    • iterator 호출했을때의 원소 순서
    • 모든 원소를 복사하기 때문에 느릴 수 있음
// 리스트
treeSet.toList

// 배열
treeSet.toArray
  • 가변/불변 변환 : ++ 연산자 활용
import scala.collection.mutable

val treeSet = TreeSet("blue","yellow","red","green")
val mutaSet = mutable.Set.empty ++= treeSet
val immutaSet = Set.empty ++ mutaSet

튜플

  • 원소의 타입이 서로 다를 수 있음
  • 메소드에서 여러값 반환이 일반적 용도
def longestWord(words: Array[String]) = {
  var word = words(0)
  var idx = 0
  for (i <- 1 until words.length)
    if (words(i).length > word.length) {
      word = words(i)
      idx = i
    }
  (word, idx)
}
  • 결합에 어떤 의미가 있는 경우, 결합에 메소드를 추가하기 원한다면 클래스 사용
  • _1, _2, ... 으로 원소 접근


Programming in Sala 책으로 스칼라 스터디하면서 정리했던 내용이다. 지금은 3판 번역본도 나왔지만, 약간 앞서서 스터디를 시작해서 2판으로 진행했다.

16장

리스트 리터럴

  • 리스트는 변경 불가능
  • 리스트 구조는 재귀적(연결 리스트)
val num = List(1,2,3,4)
val diag3 = List(
  List(1,0,0),
  List(0,1,0),
  List(0,0,1)
)
val empty = List()

리스트 타입

  • 리스트는 균일 : 리스트에 속한 모든 원소의 타입은 동일
  • 리스트 타입은 공변적 : S가 T의 서브타입이면, List[S]도 List[T]의 서브타입
  • 빈 리스트의 타입 : List[Nothing]
// Nothing은 모든 타입의 서브타입이므로, List[Nothing] 타입은 List[String]의 서브타입
val xs: List[String] = List()

리스트 생성

  • Nil:: 사용
val nums = 1 :: (2 :: (3 :: (4 :: Nil)))
val diag3 = (1 :: (0 :: (0 :: Nil))) ::
            (0 :: (1 :: (0 :: Nil))) ::
            (0 :: (0 :: (1 :: Nil))) :: Nil
val empty = Nil
  • 콜론으로 끝나기 때문에 :: 연산자는 오른쪽 결합 법칙 적용
val num = 1 :: 2 :: 3 :: 4 :: Nil

리스트 기본 연산

  • head : 첫번째 원소 반환
  • tail : 첫번째 원소 제외한 나머지 원소로 이루어진 리스트 반환
  • isEmpty : 리스트가 비었으면 true
  • 빈 리스트에 head, tail 호출 시 NoSuchElementException 발생

리스트 패턴

  • List(...) 패턴
val fruit = List("apples", "oranges", "pears")
val List(a, b, c) = fruit
// a: String = apples
// b: String = oranges
// c: String = pears
  • ::Nil 패턴
val fruit = List("apples", "oranges", "pears")
val a :: b :: rest = fruit
// a: String = apples
// b: String = oranges
// rest: List[String] = List(pears)

List 클래스 1차 메소드(함수를 인자로 받지 않는 메소드)

두 리스트 연결 :::

  • xs ::: ys : xs의 모든 원소 뒤에 ys의 모든 원소가 있는 새로운 리스트
  • 오른쪽으로 결합 : xs ::: ys ::: zsxs ::: (ys ::: zs) 와 같음

분할 정복 원칙

def append[T](xs: List[T], ys: List[T]): List[T] =
  xs match {  // 분할 부분 : 2개로 분할(첫번째 원소, 나머지 원소들)
    case List() => ys
    case x :: xs1 => x :: append(xs1, ys)   // 정복 부분 : 나머지 원소들에 대한 연산
  }

리스트 길이 length

List(1,2,3).length // 3

리스트 양 끝 init, last

  • init : 마지막 원소를 제외한 모든 원소
  • last : 마지막 원소
  • 빈 리스트에서 호출하면 NoSuchElementException 발생
  • 전체 리스트를 순회해야해서 비효율적임

리스트 뒤집기 reverse

  • 뒤집힌 새로운 리스트를 반환
  • reverse는 자기 자신의 역 : xs.reverse.reverse == xs

접두사와 접미사 drop, take, splitAt

  • xs take n : xs 리스트의 처음부터 n번째까지 원소를 반환
  • xs drop n : 첫번째에서 n번째까지 원소를 제외한 xs 리스트의 모든 원소를 반환
  • xs splitAt n : (xs take n, xs drop n) 리스트를 두번 순회하지 않음

원소 선택 apply, indices

  • apply : 인덱스로 원소 선택
  • xs apply n == xs(n) == (xs drop n).head. 인덱스 n에 비례한 시간이 걸려 잘 사용하지 않음
  • indices : 유효한 모든 인덱스의 리스트 반환(Range())

리스트의 리스트를 한 리스트로 flatten

List(List(1,2), List(3), List(), List(4, 5)).flatten
// List(1,2,3,4,5)

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

  • xs zip ys : xs 리스트와 ys리스트의 원소들의 순서쌍 리스트를 만들고, 길이가 긴쪽의 남는 원소를 버린다.
  • xs.zipWithIndex : xs 리스트의 각 원소를 그 인덱스와 함께 순서쌍으로 묶은 리스트를 만든다.
  • xs.unzip : 튜플의 리스트를 리스트의 튜플로 나눈다

리스트 출력 toString, mkString

  • toString : 리스트에 대한 표준 문자열 표현 List( ... )
  • xs mkString (pre, sep, post)
    • pre : 제일 앞에 출력할 접두사
    • sep : 원소 사이에 출력할 분리 문자
    • post : 제일 뒤에 출력할 접미사
  • xs mkString sep == xs mkString ("", sep, "")
  • xs mkString == xs mkString ""
  • addString : 생성한 문자열을 결과로 반환하지 않고 StringBuilder 객체에 추가
val buf = new StringBuilder
abcde addString (buf, "(", ";", ")")
  • mkString과 addString은 다른 모든 컬렉션에서도 사용 가능(Traversable로부터 상속한 메소드)

리스트 변환 iterator, toArray, copyToArray

  • 리스트 <-> 배열 변환 : toArray, toList
val arr = abcde.toArray // 배열로 변환
arr.toList              // 리스트로 변환
  • copyToArray : 리스트 원소를 어떤 배열의 특정 지점부터 연속으로 복사
xs copyToArray (arr, start) // xs 의 모든 원소를 arr의 start 지점에 복사
  • iterator
val it = abcde.iterator
it.next

예제

// 병합 정렬
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))
  }
}

List 클래스의 고차 메소드

리스트 매핑

  • xs map f : xs 리스트의 모든 원소에 함수 f를 적용해서 나온 결과 값의 리스트를 반환
List(1, 2, 3) map (_ + 1) // List(2, 3, 4)
  • flatMap : 원소의 리스트를 반환하는 함수를 받아, 모든 원소에 적용해서 나온 모든 리스트를 연결한 단일 리스트를 반환한다.
words map (_.toList)      // List(List(...), List(...))
words flatMap (_.toList)  // List( ... )
  • foreach : 프로시저를 받아 모든 원소에 적용한다.
var sum = 0
List(1,2,3,4,5) foreach (sum += _)
sum // 15

리스트 걸러내기

  • xs filter p : T => Boolean 타입의 술어 함수 p를 받아 원소 x중에 p(x) == true 인 원소의 리스트를 만든다.
List(1,2,3,4,5) filter (_ % 2 == 0) // List(2, 4)
  • partition : filter와 같지만, true인 원소와 false인 원소들의 리스트를 한 쌍으로 만든다.
List(1,2,3,4,5) partition (_ % 2 == 0) // (List(2, 4),List(1,3,5))
  • find : 술어 함수를 만족하는 첫번째 원소를 반환
List(1,2,3,4,5) find (_ % 2 == 0) // Some(2)
List(1,2,3,4,5) find (_ <= 0)     // None
  • takeWhile : 술어를 만족하는 가장 긴 접두사를 반환
List(1,2,3,-4,5) span (_ > 0)  // List(1,2,3)
  • dropWhile : 술어를 만족하는 가장 긴 접두사를 제거
List(1,2,3,-4,5) span (_ > 0)  // List(-4,5)
  • span : takeWhiledropWhile결과를 순서쌍으로 반환. 한번만 순회한다.
List(1,2,3,-4,5) span (_ > 0)  // (List(1,2,3),List(-4,5))

리스트 전체에 대한 술어

  • xs forall p : 리스트의 모든 원소가 p를 만족하면 true
  • xs exists p : 리스트에서 p를 만족하는 원소가 하나라도 있으면 true

리스트 폴드

  • (z /: xs) (op) : 왼쪽으로 편향된 연산 트리
  • (xs :\ z) (op) : 오른쪽으로 편향된 연산 트리
    • z : 시작값
    • xs : 대상 리스트
    • op : 이항 연산자
def reverseLeft[T](xs: List[T]) =
  (List[T]() /: xs) { (ys, y) => y :: ys}

리스트 정렬

  • xs sortWith before : 두 원소를 비교할 수 있는 함수 before를 사용하여 xs를 정렬
    • before : x before y가 true 이면 x가 y 앞에 위치
    • 병합 정렬 수행

List 객체의 메소드

원소로 리스트 만들기

  • List.apply(...) == List(...)
List.apply(1,2,3) // List(1,2,3)

수의 범위를 리스트로 만들기

  • List.range(from, until) : until을 제외한 범위 리스트
List.range(1,5) // List(1,2,3,4)
  • List.range(from, until, increment) : increment 만큼 증가하는 리스트

균일 리스트 만들기

  • List.fill(n)(v) : v를 n번 반복한 리스트
List.fill(5)('a')     // List(a,a,a,a,a)
// 인자 갯수를 추가하면 다차원 리스트 생성
List.fill(2,3)('b')   // List(List(b,b,b),List(b,b,b))

함수 도표화

  • List.tabulate : 함수로 계산한 원소의 리스트 생성
List.tabulate(5)(n => n * n)  // List(0, 1, 4, 9, 16)

리스트 연결

  • List.concat
List.concat(List('a','b'), List('c'))   // List(a,b,c)

여러 리스트 처리

  • 튜플의 zipped 메소드는 여러 리스트에서 동작하는 공통 연산을 일반화
(List(10, 20), List(3, 4, 5)).zipped.map(_ * _)       // List(30, 80)

(List("abc","de"), List(3,2)).zipped.forall(_.length == _)    // true

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

스칼라의 타입 추론 알고리즘

  • 지역적 흐름 기반 타입 추론 방식
// abcde 의 타입으로 부터 메소드에 적용할 인자의 타입 추론
abcde sortWith (_ > _)    // (Char, Char) => Boolean
msort(_ > _)(abcde)         // 비교 함수를 인자로 넘길때 타입을 알 수 없음
msort[Char](_ > _)(abcde)   // 타입인자를 전달하여 추론 가능 (Char, Char) => Boolean
msortSwapped(abcde)(_ > _)  // 이미 알려진 첫번째 파라미터 목록 값의 타입을 참고하여 추론 가능
(xss :\ List[T]()) (_ ::: _)  // 연산 타입 추론 : (List[T], List[T]) => List[T]
(xss :\ List()) (_ ::: _)     // 연산 타입 추론 : (List[T], List[Nothing]) => List[Nothing]


Programming in Sala 책으로 스칼라 스터디하면서 정리했던 내용이다. 지금은 3판 번역본도 나왔지만, 약간 앞서서 스터디를 시작해서 2판으로 진행했다.

15장

케이스 클래스

  • case라는 수식자가 붙은 클래스
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
  • 기능이 추가됨
    • 클래스 이름과 동일한 팩토리 메소드 추가
    • 클래스 파라미터 목록에 있는 모든 클래스 파라미터에 암시적으로 val 접두어 추가
    • toString, hashCode, equals 메소드의 자연스러운 구현 추가 (전체 구조를 비교함)
    • copy 메소드 추가 : 클래스에서 일부를 변경한 복사본 생성
// 팩토리 메소드
val v = Var("x")
val op = BinOp("+", Number(1), v)   // 중첩해서 객체 생성

// 파라미터가 클래스의 필드가 됨 (val)
v.name
op.left

// 전체를 구조적으로 서로 비교함
op.right == Var("x")    // true

// copy 메소드
op.copy(operator = "-") // BinOp(-,Number(1.0),Var(x))

패턴 매치

  • match 표현식 : 자바의 switch와 비슷. 셀렉터 match { 대안들 }
def simplifyTop(expr: Expr): Expr = expr match {
    case UnOp("-", UnOp("-", e)) => e   // 부호를 두번 반전
    case BinOp("+", e, Number(0)) => e  // 0을 더함
    case BinOp("*", e, Number(1)) => e  // 1을 곱합
}
  • Java switch VS Scala match
    • match는 표현식
    • 대안(case)은 다음으로 넘어가지 않는다
    • 매치 실패시 MatchError 발생

패턴의 종류

와일드카드 패턴

  • 모든 객체 매치
  • 매치한 값을 사용하지 않음
expr match {
  case BinOp(_, _, _) => println(expr +" is a binary operation")
  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"
}

변수 패턴

  • 모든 객체 매치
  • 화살표 오른쪽에 있는 표현식에 변수가 나타나면 해당 변수의 값은 매치된 값을 참조
expr match {
  case 0 => "zero"
  case somethingElse => "not zero: "+ somethingElse
}

변수 / 상수 패턴 구분

  • 소문자로 시작하는 간단한 이름은 변수
  • 그 외에는 상수
    • 대문자로 시작 : Pi
    • 긴 이름 사용 : this.pi
    • 역따옴표 사용

생성자 패턴

  • UnOp("-", e) 과 같은 형태
  • 케이스 클래스에 속하는지 검사하고, 인자로 전달 받은 값을 검사
  • 깊은 매치 지원 : 최상위 객체만을 매치시킬 뿐 아니라, 내포 패턴과 객체의 내용에 대해서도 매치
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
  case m: Map[_, _] => m.size
  case _ => -1
}
  • 타입 검사 : isInstanceOf[T]
  • 타입 변환 : asInstanceOf[T]
if (x.isInstanceOf[String]) {
  val s = x.asInstanceOf[String]
  s.length
} else ...
  • 타입 소거 : Map 매치 시에 타입 인자에 대한 매치가 불가
def isIntIntMap(x: Any) = x match {
  case m: Map[Int, Int] => true     // 모든 Map에 대해서 매칭된다
  case _ => false
}

def isStringArray(x: Any) = x match {
  case a: Array[String] => "yes"    // Array는 타입 소거되지 않기때문에 Array[String]만 매칭된다
  case _ => "no"
}

변수 바인딩

  • 패턴에 변수를 추가
  • [변수 이름] @ [패턴]
expr match {
  case UpOp("abs", e @ UnOp("abs", _)) => e
  case _ =>
}

패턴 가드

  • 패턴 변수는 한 패턴 안에 오직 한 번만 나올 수 있음
expr match {
  case BinOp("+", x, x) => BinOp("*", x, Number(2)) // Wrong
  case _ => e
}
  • 패턴 가드 : [패턴] if [불리언 표현식]
expr match {
  case BinOp("+", x, y) if x == y => BinOp("*", x, Number(2))
  case _ => e
}

패턴 겹침

코드에 있는 순서대로 패턴을 매치시키므로, 구체적인 패턴일 수록 앞에 와야하며, 모든 경우를 처리하는 패턴은 가장 뒤에 있어야 함.

봉인한 클래스(sealed class)

  • 봉인한 클래스는 클래스와 같은 파일이 아닌 다른 곳에서 서브 클래스를 만들 수 없다.
  • 패턴 매치 시에 이미 알려진 서브 클래스만 고려하면 되므로, 놓친 패턴 조합을 찾기 쉽다.(컴파일러 경고)
  • 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
  • @unchecked : 문맥상 MatchError 발생하지 않는 경우, 패턴을 모두 다루는지 검사를 생략
def describe(e: Expr): String = (e: @unchecked) match {
  case Number(_) => "a number"
  case Var(_) => "a variable"
}

Option 타입

  • Some(x) : x가 실제값
  • None : 값이 없음

패턴 사용

변수 정의

val myTuple = (123, "abc")
val (number, string) = myTuple

val exp = new BinOp("*", Number(5), Number(1))
val BinOp(op, left, right) = exp

case 시퀀스로 부분 함수 만들기

  • PartialFunction[]
// 함수 타입
val second: List[Int] => Int = {
  case x :: y :: _ => y
}

// 부분 함수 타입
val second: PartialFunction[List[Int], Int] = {
  case x :: y :: _ => y
}
  • isDefinedAt : 부분 함수가 어떤 값에 대해 결과 값을 정의하고 있는지 확인
second.isDefinedAt(List(5, 6, 7)) // true
second.isDefinedAt(List())        // false

for 표현식

for ((country, city) <- capitals)   // 튜플 패턴 사용
  println("The capital of "+ country +" is "+ city)

val results = List(Some("apple"), None, Some("orange))
for (Some(fruit) <- results)
  println(fruit)      // apple과 orange만 출력

예제

import Element.elem

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

class ExprFormatter {
  // 연산자 우선 순위
  private val opGroups =
    Array(
      Set("|", "||"),
      Set("&", "&&"),
      Set("^"),
      Set("==", "!="),
      Set("<", "<=", ">", ">="),
      Set("+", "-"),
      Set("*", "%")
    )
  private val precedence = {
    val assocs =
      for {
        i <- until opGroups.length
        op <- opGroups(i)
      } yield op -> i
    assocs.toMap
  }

  private val unaryPrecedence = opGroups.length // 단항 연산자
  private val fractionPrecedence = -1           // 나눗셈

  private def format(e: Expr, enclPrec: Int): Element =
    e match {
      case Var(name) =>
        elem(name)
      case Number(num) =>
        def stripDot(s: String) =
          if (s endsWith ".0") s.substring(0, s.length - 2)
          else s
        elem(stripDot(num.toString))
      case UnOp(op, arg) =>
        elem(op) beside format(arg, unaryPrecedence)
      case BinOp("/", left, right) =>
        val top = format(left, fractionPrecedence)
        val bot = format(right, fractionPrecedence)
        val line = elem("-", top.width max bot.width, 1)
        val frac = top above line above bot
        if (enclPrec != fractionPrecedence) frac
        else elem(" ") beside frac beside elem(" ")
      case BinOp(op, left, right) =>
        val opPrec = precedence(op)
        val l = format(left, opPrec)
        val r = format(right, opPrec + 1)
        val oper = l beside elem(" "+ op + " ") beside r
        if (enclPrec <= opPrec) oper
        else elem("(") beside oper beside elem(")")
    }

    def format(e: Expr): Element = format(e, 0)
}

object Express extends Application {
  val f = new ExprFormatter
  val e1 = BinOp("*", BinOp("/", Number(1), Number(2)),
                      BinOp("+", Var("x"), Number(1)))
  val e2 = BinOp("+", BinOp("/", Var("x"), Number(2)),
                      BinOp("/", Number(1,5), Var("x")))
  var e3 = BinOp("/", e1, e2)

  def show(e: Expr) = println(f.format+ "\n\n")
  for (e <- Array(e1, e2, e3)) show
}


Programming in Sala 책으로 스칼라 스터디하면서 정리했던 내용이다. 지금은 3판 번역본도 나왔지만, 약간 앞서서 스터디를 시작해서 2판으로 진행했다.

14장

단언문

  • assert 메소드
  • 조건 충족되지 않으면 AssertionError 발생
def above(that: Elemment): Element = {
  val this1 = this widen that.width
  val that1 = that widen this.width
  assert(this1.width == that1.width)    // assert
  elem(this1.contents ++ that1.contents)
}
  • assert(c, e) : c를 만족하지 않으면, 설명 e(e.toString)를 포함하는 AssertionError 발생
  • ensuring : 어떠한 결과 타입이든 적용 가능
private def widen(w: Int): Element =
  if (w <= width)
    this
  else {
    val left = elem(' ', (w - width) / 2, height)
    val right = elem(' ', w - width - left.width, hegith)
    left beside this beside right
  } ensuring (w <= _.width)         // ensuring. false면 AssertionError
  • 단언문 동작 끄기 : JVM -ea, -da 옵션(동작X), scalac -Xdisable-assertions

단위 테스트

ScalaTest

  • Suite
import org.scalatest.Suite
import Element.elem

class ElementSuite extends Suite {  // Suite 확장
  def testUniformElement() {        // test로 시작하는 메소드
    val ele = elem('x', 2, 3)
    assert(ele.width == 2)
  }
}

// (new ElementSuite).execute() // 실행
  • FunSuite
import org.scalatest.FunSuite
import Element.elem

class ElementSuite extends FunSuite {   // FunSuite 확장
  test("elem result should have passed width") {
    val ele = elem('x', 2, 3)
    assert(ele.width == 2)
  }
}
  • === 연산자 : 비교 실패시 더 많은 메시지 표시
assert(ele.width === 2)
  • expect 메소드 : 예상치를 강조 표시
expect(2) {
  ele.width
}
  • intercept 메소드 : 예외 예상
intercept[IllegalArgumentException] {
  elem('x', -2, 3)
}

JUnit

  • TestCase 확장하여 동일하게 사용 가능
import junit.framework.TestCase
import junit.framework.Assert.assertEquals
import junit.framework.Assert.fail
import Element.elem

class ElementTestCase extends TestCase {
  def testUniformElement() {
    val ele = elem('x', 2, 3)
    assertEquals(2, ele.width)
    assertEquals(3, ele.height)
    try {
      elem('x', -2, 3)  // IllegalArgumentException 이 발생한다고 가정
      fail()
    }
    catch {
      case e: IllegalArgumentException => // 발생하리라 예상
    }
  }
}
  • JUnit3Suite : ScalaTest의 단언 문법 사용
import org.scalatest.junit.JUnit3Suite
import Element.elem

class ElementSuite extends Junit3Suite {
  def testUniformElement() {
    val ele = elem('x', 2, 3)
    assert(ele.width === 2)
    expect(3) { ele.height }
    intercept[IllegalArgumentException] {
      elem('x', -2, 3)
    }
  }
}

TestNG

  • TestNG 실행 장치로 실행 가능
import org.testing.annotations.Test
import org.testing.Assert.assertEquals
import Element.elem

class ElementTests {
  @Test def verifyUniformElement() {
    val ele = elem('x', 2, 3)
    assertEquals(ele.width, 2)
    assertEquals(ele.height, 3)
  }
  @Test(
    expectedExceptions =
      Array(classOf[IllegalArgumentException])
  )
  def elemShouldThrowIAE() { elem('x', -2, 3) }
}
  • TestNGSuite : ScalaTest의 단언 문법 사용
import org.scalatest.testing.TEstNGSuite
import org.testing.annotations.Test
import Element.elem

class ElementSuite extends TestNGSuite {
  @Test def verifyUniformElement() {
    val ele = elem('x', 2, 3)
    assert(ele.width === 2)
    expect(3) { ele.height }
    intercept[IllegalArgumentException] {
      elem('x', -2, 3)
    }
  }
}

BDD 스타일

  • Spec, WordSpec, FlatSpec, FeatureSpec 등 제공
  • FlatSpec 예
import org.scalatest.FlatSpec
import org.scalatest.matchers.ShouldMatchers
import Element.elem

class ElementSpec extends FlatSpec with ShouldMatchers {
  "A UniformElement" should                         // 주제
    "Have a width equal to the passwd value" in {   // 동작 설명문
      val ele = elem('x', 2, 3)
      ele.width should be (2)
    }

  it should "have a height equal to the passed value" in {  // it : 직전 주제 언급
    val ele = elem('x', 2, 3)
    ele.height should be (3)
  }

  it should "throw an IAE if passed a negative width" in {
    evaluating {
      elem('x', -2, 3)
    } should produce [IllegalArgumentException]
  }
}
  • specs 프레임 워크 사용 가능(specs2로 대치됨)

ScalaCheck - 프로퍼티 기반 테스트

import org.scalatest.WordSpec
import org.scalatest.prop.Checkers
import org.scalatest.Prop._
import Element.elem

class ElementSpec extends WordSpec with Checkers {
  "elem result" must {
    "have passed width" in {
      check((w: Int) => w > 0 ==> (elem('x', w, 3).width == w)) // ==> 함의 연산자 : 왼쪽 표현식이 true 이면 오른쪽도 true
    }
    "have passed height" in {
      check((h: Int) => h > 0 ==> (elem('x', 2, h).height == h))
    }
  }
}

테스트 조직

  • Suite 안에 Suite를 포함시켜서 전체를 조직
  • 포함시킬 Suite를 수동 처리
    • nestedSuites 메소드 오버라이드
    • 포함시킬 Suite를 SuperSuite 클래스의 생성자에 전달


Programming in Sala 책으로 스칼라 스터디하면서 정리했던 내용이다. 지금은 3판 번역본도 나왔지만, 약간 앞서서 스터디를 시작해서 2판으로 진행했다.

13장

패키지

  • 파일 전체를 패키지 안에 넣기
package bobsrockets.navigation
class Navigator
  • 패키징 문법
package bobsrockets.navigation {
  class Navigator
}
  • 여러 패키지
package bobsrockets {
  package navigation {
    class Navigator

    package tests {
      class NavigatorSuite
    }
}

관련 코드 접근

  • 클래스가 속한 패키지 안에서는 접두어 없이 해당 클래스에 접근 가능
  • 패키지를 포함하는 부모 패키지는 접두어 없이 해당 패키지 접근 가능
  • 중괄호 패키징 문법을 사용하면 패키징 스코프 밖에서 접근 가능한 모든 이름을 패키징 안에서 사용 가능
package bobsrockets {
  package navigation {
    class Navigator {
      val map = new StarMap // bobsrockets.navigation.StarMap
    }
    class StarMap
  }

  class Ship {
    val nav = new navigation.Navigator  // bobsrockets.navigation.Navigator
  }

  package fleets {
    class Fleet {
      def addShip() {
        new Ship        // bobsrockets.Ship
      }
    }
  }
}
  • 연쇄 패키지 절 : 괄호 없이 여러 패키지 절 사용
package bobsrockets // 중괄호 생략
package fleets      // 중괄호 생략
class Fleet {
  def addShip() { new Ship }
}
  • 숨겨진 패키지 이름 접근
package launch {
  class Booster3
}

package bobsrockets {
  package navigation {
    package launch {
      class Booster1
    }

    class MissionControl {
      val booster1 = new launch.Booster1                // 가장 좁은 패키지 스코프 안에 있음
      val booster2 = new bobsrockets.launch.Booster2    // 전체 패키지 지정
      val booster3 = new _root_.launch.Booster3         // 최상위 launch 패키지
    }
  }

  package launch {
    class Booster2
  }
}

임포트

  • 자바의 싱글 타입 임포트
// Fruit
import bobsdelights.Fruit
  • 자바의 주문식 임포트(*)
// bobsdelights의 모든 멤버
import bobsdelights._
  • 자바의 정적 클래스 필드 임포트
// Fruit의 모든 멤버
import bobsdelights.Fruits._
  • import 문은 코드 어디에나 위치 가능
def showFruit(fruit: Fruit) {
  import fruit._
  println(name +"s are"+ color) // fruit.name, fruit.color
}
  • 패키지, 객체 참조 가능
import java.util.regex
class AStarB {
  val pat = regex.Pattern.compile("a*b")    // 싱글톤 객체 java.util.regex.Pattern
}
  • 불러온 멤버 이름 중 일부를 숨기거나 다른 이름 지정 가능
    • x : x를 불러온다
    • x => y : x를 y라는 이름으로 불러온다
    • x => _ : x를 제외하고 불러온다
    • _ : 나머지를 모두 가져온다
// 일부 멤버만 불러온다
import Fruits.{Apple, Orange}

// 다른 이름 지정
import Fruits.{Apple => McIntosh, Orange}

// import Fruits._ 과 동일
import Fruits.{_}

// import Fruits._ 과 동일하나 Apple의 이름만 바꾼다
import Fruits.{Apple => McIntosh, _}

// Pear를 제외한 모든 멤버를 불러온다
import Fruits.{Pear => _, _}

암시적 임포트

  • 세가지 임포트 절을 모든 스칼라 소스 위에 추가하는 것과 같은 동작 있음
import java.lang._
import scala._
import.Predef._
  • 나중에 임포트한 패키지에 들어있는 이름이 앞에서 임포트한 이름을 가리게 됨

접근 수식자

비공개 멤버 private

자바와 달리 외부 클래스가 내부 클래스에 있는 비공개 멤버에 접근 불가

class Outer {
  class Inner {
    private def f() { ... }
    class InnerMost {
      f() // 문제 없음
    }
  }

  (new Inner).f()   // 오류
}

보호 멤버 protected

자바와 달리 보호 멤버를 정의한 클래스의 서브클래스에서만 접근 가능

package p {
  class Super {
    protected def f() { ... }
  }

  class Sub extends Super {
    f() // 문제 없음
  }

  class Other {
    (new Super).f() // 오류
  }
}

공개 멤버

privateprotected가 없는 멤버

보호 스코프

package bobsrockets

package navigation {
  private[bobsrockets] class Navigator {    // bobsrockets 내부에서 접근 가능
    protected[navigation] def useStarChart() {} // bobsrockets.navigation 내부와 bobsrockets.navigation.Navigator 서브 클래스에서 접근 가능
    
    class LegOfJourney {
      private[Navigator] val distance = 100 // bobsrockets.navigation.Navigator 내부에서 접근 가능
    }
    private[this] var speed = 200   // bobsrockets.navigation.Navigator 클래스의 객체 자신만 접근
  }
}

package launch {
  import navigation._
  object Vehicle {
    private[launch] val guide = new Navigator   // bobsrockets.launch 내부에서 접근 가능
  }
}
  • 수식자 없음 : 전체 접근 가능
  • private[bobsrockets] : bobsrockets 패키지 내부에서 접근 가능
  • private[navigation] : 자바의 패키지 접근
  • private[Navigator] : 자바의 private와 같음
  • private[LegOfJourney] : 스칼라의 private와 같음
  • private[this] : 어떤 객체 자신만 접근 가능

동반 객체

비공개, 보호 멤버에 대해서 동반 객체와 클래스는 접근 권리를 공유

패키지 객체

  • 패키지 전체 스코프에 도우미 메소드를 두는 경우, 패키지 최상위 수준에 넣을 수 있음
  • 패키지 객체는 package.class라는 이름의 클래스 파일로 컴파일되므로, 소스 파일도 동일하게 작성하는 것이 좋음
// bobsdelights/package.scala
package object bobsdelights {
  def showFruits(fruit : Fruit) {
    import fruit._
    println(name + "s are"+ color)
  }
}

// PrintMenu.scala
package printmenu
import bobsdelights.Fruits
import bobsdelights.showFruit
object PrintMenu {
  def main(args: Array[String]) {
    for (fruit <- Fruits.menu) {
      showFruit(fruit)
    }
  }
}


Programming in Sala 책으로 스칼라 스터디하면서 정리했던 내용이다. 지금은 3판 번역본도 나왔지만, 약간 앞서서 스터디를 시작해서 2판으로 진행했다.

12장

트레이트 동작

  • 기본 부모 클래스는 AnyRef
trait Philosophical {
  def philosophize() {
    println("I consume memory, therefore I am!")
  }
}
  • 믹스인 : extends. 트레이트의 부모클래스를 암시적 상속
class Frog extends Philosophical {
  override def toString = "green"
}
  • 트레이트도 타입을 정의함
val phil: Philosophical = new Frog
  • 트레이트는 여러개를 믹스인 가능 : with
// 부모 클래스를 상속한 클래스에 믹스인
class Animal
class Frog extends Animal with Philosophical {
  override def toString = "green"
}

// 여러 트레이트 믹스인
trait HasLegs
class Frog extends Animal with Philosophical with HasLegs {
  override def toString = "green"
}
  • 트레이트 구현 오버라이드 가능 : override
class Animal
class Frog extends Animal with Philosophical {
  override def toString = "green"
  override def philosophize() {
    println("It ain't easy being "+ toString +"!")
  }
}

val phrog: Philosophical = new Frog
phrog.philosophize()    // 오버라이드 되었기 때문에 "It ain't easy being green!" 출력
  • 클래스 정의하면서 할 수 있는 모든 것 가능 : 트레이트에 필드 선언 등
  • '클래스' 파라미터를 가질 수 없음(생성자 파라미터 불가)
  • super 호출을 동적으로 바인딩 함(믹스인 하는 클래스에 따라 달라짐)

간결한 인터페이스 / 풍부한 인터페이스

  • 간결한 인터페이스 : 메소드 수가 적어 구현하는 측이 편리
  • 풍부한 인터페이스 : 많은 메소드 중 필요에 일치하는 메소드를 선택/사용하여 사용하는 측 편리
  • 간결한 인터페이스를 풍부한 인터페이스로 만들 때 트레이트를 사용할 수 있음
    1. 간결한 인터페이스 역할을 하는 추상 메소드 구현
    2. 추상 메소드를 활용해 풍부한 인터페이스 역할을 할 여러 메소드를 동일 트레이트 안에 구현

예제

class Point(val x: Int, val y: Int)

trait Rectangular {
  def topLeft: Point
  def bottomRight: Point

  def left = topLeft.x
  def right = bottomRight.x
  def width = right - left
  // ...
}

abstract class Component extends Rectangular {
  // ...
}

class Rectangle(val topLeft: Point, val bottomRight: Point) extends Rectangular {
  // ...
}

Orderd 트레이트

  • 순서를 표한하는 트레이트
  • 사용
    1. Ordered 트레이트를 타입 파라미터(19장) 명시하여 믹스인 Ordered[C]
    2. compare 메소드 정의
// Before
class Rational(n: Int, d: Int) {
  // ...
  def < (that: Rational) =
    this.numer * that.denom > that.numer * this.denom
  def > (that: Rational) = that < this
  def <= (that: Rational) = (this < that) || (this == that)
  def >= (that: Rational) = (this > that) || (this == that)
}

// After
class Rational(n: Int, d: Int) extends Ordered[Rational] {
  // ...
  def compare(that: Rational) =
    (this.numer * that.denom) - (that.numer * this.denom)
}
  • equals는 직접 정의해야함(30장 우회 방법)

변경 쌓기

  • 클래스의 메소드를 변경하고, 변경 위에 다른 변경을 계속 쌓을 수 있음
abstract class IntQueue {
  def get(): Int
  def put(x: Int)
}

import scala.collection.mutable.ArrayBuffer

class BasicIntQueue extends IntQueue {
  private val buf = new ArrayBuffer[Int]
  def get() = buf.remove(0)
  def put(x: Int) { buf += x }
}

trait Doubling extends IntQueue {
  abstract override def put(x: Int) { super.put(2 * x) }
}

// 믹스인
class MyQueue extends BasicIntQueue with Doubling
val queue = new MyQueue
queue.put(10)
queue.get()     // 20

// 인스턴스 생성시 믹스인
val queue2 = new BasicIntQueue with Doubling
queue2.put(10)
queue2.get()    // 20
  • 여러 트레이트로 변경을 여러 순서로 쌓을 수 있음
trait Incrementing extends IntQueue {
  abstract override def put(x: Int) { super.put(x + 1) }
}

trait Filtering extends IntQueue {
  abstract override def put(x: Int) {
    if (x >= 0) super.put(x)
  }
}

// 음수이면 제거 후에 1 증가
val queue = (new BasicIntQueue with Incrementing with Filtering)
queue.put(-1);queue.put(0);queue.put(1)
queue.get()     // 1
queue.get()     // 2

// 1 증가 후에 음수이면 제거
val queue2 = (new BasicIntQueue with Filtering with Incrementing)
queue.put(-1);queue.put(0);queue.put(1)
queue.get()     // 0
queue.get()     // 1
queue.get()     // 2

선형화

  • 상속의 경우 super를 호출할 때에 어떤 메소드를 부를지 컴파일 시점에 이루어진다.
  • 트레이트는 선형화로 부를 메소드를 결정한다.
class Animal
trait Furry extends Animal
trait HasLegs extends Animal
trait FourLegged extends HasLegs
class Cat extends Animal with Furry with FourLegged
  1. Animal -> AnyRef -> Any
  2. Furry -> (Animal -> AnyRef -> Any) // 이미 선형화한 부분은 제외. 중복 제외
  3. FourLegged -> HasLegs -> (Furry -> Animal -> AnyRef -> Any)
  4. Cat -> (FourLegged -> HasLegs -> Furry -> Animal -> AnyRef -> Any)

super 사용시, 선형화 순서상 자기보다 바로 오른쪽에 있는 첫 번째 구현을 호출

트레이트 사용 고려 사항

  • 재사용하지 않는 경우 -> 클래스
  • 관련 없는 클래스에서 여러번 재사용 -> 트레이트
  • 자바 코드에서 스칼라 내용을 상속 -> 추상 클래스
  • 컴파일한 바이트 코드 형태로 배포 -> 추상 클래스
  • 효율이 중요 -> 클래스
  • 잘 모르겠으면 -> 트레이트


Programming in Sala 책으로 스칼라 스터디하면서 정리했던 내용이다. 지금은 3판 번역본도 나왔지만, 약간 앞서서 스터디를 시작해서 2판으로 진행했다.

11장

클래스 계층 구조

Any

최상위 클래스

  • final def ==(that: Any): Boolean : equals
  • final def !=(that: Any): Boolean : not equals
  • def equals(that: Any): Boolean
  • def ##: Int : hashCode
  • def hashCode: Int
  • def toString: String

자바(equals, hashCode)와 달리 스칼라(==, !=, ##)는 일관성을 위해 박싱 클래스를 특별 취급함

AnyVal, AnyRef

Any의 서브 클래스

AnyVal

모든 스칼라의 내장 값 클래스(Byte, Short, Char, Int, Long, Float, Double, Boolean, Unit)의 부모 클래스

  • Byte, Short, Char, Int, Long, Float, Double, Boolean
    • 자바의 원시 타입에 대응
    • 실행 시점에 자바 원시 타입 값으로 표현함
    • new 를 사용하여 인스턴스화할 수 없음(추상클래스이며 final 클래스임)
    • 암시적 변환 제공(widening)
      • Int -> Long -> Float -> Double
      • Char -> Int
    • 더 많은 기능 제공을 위한 암시적 변환(scala.runtime.RichInt 등)
42 max 43   // 43
42 min 43   // 42
1 until 5   // Range(1, 2, 3, 4)
1 to 5      // Range(1, 2, 3, 4, 5)
3.abs       // 3
(-3).abs    // 3
  • Unit
    • 자바의 void 와 유사
    • 값 : ()

AnyRef

모든 참조 클래스의 기반 클래스(java.lang.Object)

Nothing

모든 클래스의 서브클래스

기본 클래스 구현

  • 표준 연산은 자바 기본 연산을 사용하지만, 객체가 필요한 경우 대응하는 클래스를 사용
  • 값 타입의 경우, ==는 자연적인 동일성(값을 비교)
// 자바 코드
boolean isEqual(int x, int y) {
    return x == y;
}

isEqual(421, 421);  // true

boolean isEqual2(Integer x, Integer y) {
    return x == y;
}

isEqual2(421, 421); // false

// 스칼라 코드
def isEqual(x: Int, y: Int) = x == y

isEqual(421, 421)   // true

def isEqual2(x: Any, y: Any) = x == y

isEqual2(421, 421)  // true
  • 참조 동일성 확인 : eq, ne
val x = new String("abc")
val y = new String("abc")

x == y  // true
x eq y  // false
x ne y  // true

바닥에 있는 타입

scala.Null

  • AnyRef를 상속한 모든 참조 타입의 서브 클래스
  • 값 타입과는 호환되지 않음
val i: Int = null   // error

scala.Nothing

  • 모든 다른 타입의 서브 타입
def error(message: String): Nothing =
  throw new RuntimeException(message)

def divide(x: Int, y: Int): Int =
  if (y != 0) x / y
  else error("can't divide by zero")    // return Nothing for Int


Programming in Sala 책으로 스칼라 스터디하면서 정리했던 내용이다. 지금은 3판 번역본도 나왔지만, 약간 앞서서 스터디를 시작해서 2판으로 진행하고 있다.


10장

추상 클래스

  • 추상 클래스 : 구현이 없는 추상 멤버가 있는 클래스
  • abstract
  • 인스턴스를 만들 수 없음
  • 추상 메소드 : 추상 클래스 내의 구현이 없는 메소드
  • 구체 메소드 : 구현이 있는 메소드
abstract class Element {
    def contents: Array[String]
}

파라미터 없는 메소드 정의

abstract class Element {
    def contents: Array[String]
    def height: Int = contents.length
    def width: Int = if (height == 0) 0 else contents(0).length
}
  • 파라미터 없는 메소드와 빈 괄호 메소드를 자유롭게 섞어쓸 수 있음
  • 서로 오버라이드 가능
  • 메소드 호출시 빈 괄호 생략 가능
  • 함수 호출시 프로퍼티 접근 이상의 작업이 있따면 괄호를 사용하는 것이 좋음

클래스 확장

class ArrayElement(conts: Array[String]) extends Element {
    def contents: Array[String] = conts
}
  • extends 절이 없으면 scala.AnyRef를 상속
  • 상속 : 슈퍼클래스의 모든 멤버는 서브클래스의 멤버
    • 비공개 멤버 제외
    • 이름과 파라미터가 동일한 멤버 정의가 존재하면 제외 = 오버라이드(추상 멤버의 경우는 구현)
val ae = new ArrayElement(Array("hello","world"))
ae.width // 슈퍼클래스 멤버 사용
  • 서브타입 : 슈퍼클래스의 값을 필요로 하는 곳이라면 어디나 서브클래스의 값 사용 가능
val e: Element = new ArrayElement(Array("hello")) // Element 대신 사용
  • 구성(composition) : ArrayElement 이진 클래스 코드에 배열을 가리킬 참조 필드가 들어감

메소드와 필드 오버라이드

  • 필드와 메소드가 동일한 네임스페이스에 속함
  • 파라미터 없는 메소드를 필드(val)로 오버라이드 가능
class ArrayElement(conts: Array[String]) extends Element {
    val contents: Array[String] = conts
}
  • 동일한 이름의 필드와 메소드를 동시에 정의 불가
class WontCompile {
    private var f = 0 // 필드와 메소드가 같은 이름이므로
    def f = 1         // 컴파일할 수 없음
}
  • 스칼라의 네임스페이스
    • 값 : 필드, 메소드, 패키지, 싱글톤 객체
    • 타입 : 클래스, 트레이트 이름

파라미터 필드

class ArrayElement (
    val contents: Array[String] // 파라미터와 필드를 결합한 파라미터 필드(parametric field)
) extends Element
  • val, var 사용 가능
  • private, protected, override 수식자 사용 가능

슈퍼클래스의 생성자 호출

class LineElement(s: String) extends ArrayElement(Array(s)) {
    override def width = s.length
    override def height = 1
}

override 수식자

  • 부모 클래스에 있는 구체적 멤버를 오버라이드하는 모든 멤버에 override 수식자를 붙여야 함
  • 추상 멤버 구현시에는 생략 가능
  • 우연한 오버라이드 방지

다형성(서브타입 다형성)

Element 타입 변수가 ArrayElement, LineElement 등의 객체 참조 가능

val e1: Element = new ArrayElement(Array("hello","world"))
val ae: ArrayElement = new LineElement("hello")
val e2: Element = ae

동적 바인딩

메소드는 실행 시점에 실제 그 객체가 어떤 타입인가를 따름

abstract class Element {
    def demo() {
        println("Element's implementation invoked")
    }
}

class ArrayElement extends Element {
    override def demo() {
        println("ArrayElement's implementation invoked")
    }
}

class LineElement extends Element {
    override def demo() {
        println("LineElement's implementation invoked")
    }
}

class UniformElement extends Element

def invokeDemo(e: Element) {
    e.demo()
}

invokeDemo(new ArrayElement) // ArrayElement's implementation invoked
invokeDemo(new LineElement) // LineElement's implementation invoked
invokeDemo(new UniformElement) // Element's implementation invoked

final

  • 서브클래스가 특정 멤버를 오버라이드 못하게 함
class ArrayElement extends Element {
    final override def demo() {  // 메소드 오버라이드 불가
        println("ArrayElement's implementation invoked")
    }
}
final class ArrayElement extends Element {  // 클래스 상속 불가
    override def demo() {
        println("ArrayElement's implementation invoked")
    }
}

상속과 구성 사용

  • 구성과 상속은 이미 존재하는 클래스를 이용해 새로운 클래스를 정의하는 방법
  • 구성 : 코드 재사용
  • 상속
    • is-a 관계 : 'ArrayElement는 Element이다'
    • 슈퍼클래스의 타입으로 서비클래스를 사용하는 경우

예제 구현

  • 메소드 구현
def above(that: Element): Element =
    new ArrayElement(this.contents ++ that.contents) // ++ 연산은 두 배열을 연결

def beside(that: Element): Element =
    new ArrayElement(
        for (
            (line1, line2) <- this.contents zip that.contents // zip 연산은 순서쌍으로 이뤄진 하나의 배열로 변환
        ) yield line1 + line2
    )

override def toString = contents mkString "\n"
  • 팩토리 구현
object Element {
    private class ArrayElement(
        val contents: Array[String]
    ) extends Element

    private class LineElement(s: String) extends Element {
        val contents = Array(s)
        override def width = s.length
        override def height = 1
    }

    private class UniformElement(
        ch: Char,
        override val width: Int,
        override val height: Int
    ) extends Element {
        private val line = ch.toString * width
        def contents = Array.fill(height)(line)
    }

    def elem(contents: Array[String]): Element =
        new ArrayElement(contents)
    def elem(chr: Char, width: Int, height: Int): Element =
        new UniformElement(chr, width, height)
    def elem(line: String): Element =
        new LineElement(line)
}

import Element.elem
abstract class Element {
    def contents: Array[String]
    
    def width: Int =
        if (height == 0) 0 else contents(0).length
    
    def height: Int = contents.length
    
    def above(that: Element): Element =
        elem(this.contents ++ that.contents)
    
    def beside(that: Element): Element =
        elem(
            for (
                (line1, line2) <- this.contents zip that.contents // zip 연산은 순서쌍으로 이뤄진 하나의 배열로 변환
            ) yield line1 + line2
        )

    override def toString = contents mkString "\n"
}
  • widen, heighten 메소드 추가
import Element.elem
abstract class Element {
    def contents: Array[String]
    
    def width: Int =
        if (height == 0) 0 else contents(0).length
    
    def height: Int = contents.length
    
    def above(that: Element): Element = {
        val this1 = this widen that.width
        val that1 = that widen this.width
        elem(this1.contents ++ that1.contents)
    }
    
    def beside(that: Element): Element = {
        val this1 = this heighten that.height
        val that1 = that heighten this.height
        elem(
            for (
                (line1, line2) <- this1.contents zip that1.contents // zip 연산은 순서쌍으로 이뤄진 하나의 배열로 변환
            ) yield line1 + line2
        )
    }
    
    def widen(w: Int): Element =
        if (w <= width) this
        else {
            val left = elem(' ', (w - width) / 2, height)
            val right = elem(' ', w - width - left.width, height)
            left beside this beside right
        }
    
    def heighten(h: Int): Element =
        if (h <= height) this
        else {
            val top = elem(' ', width, (h - height) / 2)
            val bot = elem(' ', width, h - height - top.height)
            top above this above bot
        }

    override def toString = contents mkString "\n"
}
  • Spiral 애플리케이션
import Element.elem
object Spiral {
    val space = elem(" ")
    val corner = elem("+")

    def spiral(nEdges: Int, direction: Int): Element = {
        if (nEdges == 1)
            elem("+")
        else {
            val sp = spiral(nEdges - 1, (direction + 3) % 4)
            def verticalBar = elem('|', 1, sp.height)
            def horizontalBar = elem('-', sp.width, 1)
            if (direction == 0)
                (corner beside horizontalBar) above (sp beside space)
            else if (direction == 1)
                (sp above space) beside (corner above verticalBar)
            else if (direction == 2)
                (space beside sp) above (horizontalBar beside corner)
            else
                (verticalBar above corner) beside (space above sp)
        }
    }

    def main(args: Array[String]) {
        val nSides = args(0).toInt
        println(spiral(nSides, 0))
    }
}


Programming in Sala 책으로 스칼라 스터디하면서 정리했던 내용이다. 지금은 3판 번역본도 나왔지만, 약간 앞서서 스터디를 시작해서 2판으로 진행하고 있다.


9장

코드 중복 줄이기

  • 고차 함수 : 함수를 인자로 받는 함수
object FileMatcher {
    private def filesHere = (new java.io.File(".)).listFiles
    def fileEnding(query: String) =
        for (file <- filesHere; if file.getName.endsWith(query))
            yield file
    def fileContaining(query: String) =
        for (file <- filesHere; if file.getName.contains(query))
            yield file
    def fileRegex(query: String) =
        for (file <- filesHere; if file.getName.matches(query))
            yield file
    
}
  1. 함수값 사용
// 중복 코드를 함수로 작성
def filesMatching(query: String, matcher: (String, String) => Boolean) = {
    for (file <- filesHere; if matcher(file.getName, query))
        yield file
}

def fileEnding(query: String) =
    filesMatching(query, _.endsWith(_)) // 함수값
def fileContaining(query: String) =
    filesMatching(query, _.contains(_)) // 함수값
def fileRegex(query: String) =
    filesMatching(query, _.matches(_)) // 함수값
  1. 클로저 사용
// 클로저 사용하여 인자 제거
private def filesMatching(matcher: (String, String) => Boolean) = {
    for (file <- filesHere; if matcher(file.getName))
        yield file
}

def fileEnding(query: String) =
    filesMatching(_.endsWith(query))
def fileContaining(query: String) =
    filesMatching(_.contains(query))
def fileRegex(query: String) =
    filesMatching(_.matches(query))

클라이언트 코드 단순하게 만들기

def containsNeg(nums: List[Int]): Boolean = {
    var exists = false
    for (num <- nums)
        if (num < 0)
            exists = true
    exists
}

def containsNeg(nums: List[Int]) = nums.exists(_ < 0)
def containsOdd(nums: List[Int]): Boolean = {
    var exists = false
    for (num <- nums)
        if (num % 2 == 1)
            exists = true
    exists
}

def containsOdd(nums: List[Int]) = nums.exists(_ % 2 == 1)

커링

  • 커링 : n개의 인자를 받는 함수를 n개의 함수로 각각의 인자를 받도록 하는 것
def plainOldSum(x: Int, y: Int) = x + y
plainOldSum(1, 2)

def curriedSum(x: Int)(y: Int) = x + y  // 커링
curriedSum(1)(2)

val onePlus = curriedSum(1)_    // 인자 하나 받아서 1을 더하는 함수
onePlus(2)

새로운 제어구조 작성 - 중괄호

인자를 1개 전달하는 경우에 함수 인자를 감싸는 소괄호 대신 중괄호 사용 가능

def withPrintWriter(file: File, op: PrintWriter => Unit) {
    val writer = new PrintWriter(file)
    try {
        op(writer)
    } finally {
        writer.close()
    }
}

withWriter(
    new File("date.txt"),
    writer => writer.println(new java.util.Date)
) // 빌려주기 패턴. 자원 닫기가 보장됨
def withPrintWriter(file: File)(op: PrintWriter => Unit) {
    val writer = new PrintWriter(file)
    try {
        op(writer)
    } finally {
        writer.close()
    }
}

val file = new File("date.txt")
withPrintWriter(file) {
    writer => writer.println(new java.util.Date)
} // 중괄호로 변경

이름에 의한 호출

var assertionsEnabled = true
def myAssert(predicate: () => Boolean) =
    if (assertionsEnabled && !predicate())
        throw new AssertionError

myAssert(() => 5 > 3)

def byNameAssert(predicate: => Boolean) = // 이름에 의한 호출 파라미터
    if (assertionsEnabled && !predicate)
        throw new AssertionError

byNameAssert(5 > 3) // 빈 파라미터 타입 생략 가능

def boolAssert(predicate: Boolean) =
    if (assertionsEnabled && !predicate)
        throw new AssertionError

boolAssert(5 > 3)   // 괄호 안의 표현식이 바로 실행됨


+ Recent posts