Spring Social이 개발 중단된고한다. Spring Security 5에서 직접 OAuth2를 지원하는 것이 그 이유라고 하는데, Spring Social의 API Binding을 만든 목적이 Spring Social의 Connection Framework 사용방법을 보여주기 위한 것이라는 걸 이제야 알았다. 어쩐지 예전에 spring-social-facebook의 Facebook Graph API 지원이 느린 것에 의문을 가지고 있었는데, 그 정도의 위상을 가진 프로젝트였기 때문에 그렇게 관리되었던 모양이다.

이제 대신 Spring Security 5를 사용하여 소셜사이트 연동하는 방법으로 구현해야 한다. 즉, Spring Social에서 원래 의도했던 바와 같이 Spring Security의 OAuth2 지원을 사용해서 API binding을 직접 만들어 사용하는 것이다.

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

22장

리스트

package scala
abstract class List[+T]
  • 리스트는 추상 클래스 : new List 불가능
  • 리스트는 공변적(+T) : List[Int] 타입 값을 List[Any] 타입 변수에 할당 가능
  • 리스트 연산은 세가지 기본 메소드(List 클래스의 추상 메소드)로 만들 수 있음
    • def isEmpty: Boolean
    • def head: T
    • def tail: List[T]

Nil 객체

case object Nil extends List[Nothing] {
  override def isEmpty = true
  override def head: Nothing = throw new NoSuchElementException("head of empty list")
  override def tail: List[Nothing] = throw new NoSuchElementException("tail of empty list")
}

:: 클래스

final case class ::[T](hd: T, tl: List[T]) excents List[T] {
  def head = hd
  def tail = tl
  override def isEmpty: Boolean = false
}

// 좀더 짧은 :: 클래스
final case class ::[T](val head: T, val tail: List[T]) excents List[T] {
  override def isEmpty: Boolean = false
}

추가 메소드

def length: Int = if (isEmpty) 0 else 1 + tail.length

def drop(n: Int): List[T] =
  if (isEmpty) Nil
  else if (n <= 0) this
  else tail.drop(n - 1)

def map[U](f: T => U): List[U] =
  if (isEmpty) Nil
  else f(head) :: tail.map(f)

리스트 구성 메소드

def ::[U >: T](x: U): List[U] = new scala.::(x, this)
  • 원소 타입 T의 슈퍼 타입인 U가 타입 파라미터
  • List 정의가 공변적이기 때문
def :::[U >: T](prefix: List[U]): List[U] =
  if (prefix.isEmpty) this
  else prefix.head :: prefix.tail ::: this

실제 List 클래스

// 효율을 위해서 루프 사용
final override def map[U](f: T => U): List[U] = {
  val b = new ListBuffer[U]
  var these = this
  while (!these.isEmpty) {
    b += f(these.head)
    these = these.tail
  }
  b.toList
}

final case class ::[T](hd: T, private[scala] var tl: List[T]) excents List[T] {
  def head = hd
  def tail = tl
  override def isEmpty: Boolean = false
}
package scala.collection.immutable
final class ListBuffer[T] extends Buffer[T] {
  private var start: List[T] = Nil
  private var last0: ::[T] = _
  private var exported: Boolean = false
  ...

  override def toList[T] = {
    exported = !start.isEmpty
    start
  }

  override def += (x: T) {
    if (exported) copy()
    if (start.isEmpty) {
      last0 = new scala.::(x, Nil)
      start = last0
    } else {
      val last1 = last0
      last0 = new scala.::(x, Nil)
      last1.tl = last0  // 콘즈의 tl이 var 인 이유
    }
  }
}


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

21장

암시적 변환

암시적 변환 예

  • 서로를 고려하지 않은 두 독립 소프트웨어를 한데 묶는데 유용
// (1) 자바 코드를 스칼라 코드로 그대로 포팅
val button = new JButton
button.addActionListener(
  new ActionListener {
    def actionPerformed(event: ActionEvent) {
      println("pressed!")
    }
  }
)

// 암시적 변환 코드
implicit deff function2ActionListener(f: ActionEvent => Unit) =
  new ActionListener {
    def actionPerformed(event: ActionEvent) = f(event)
  }

// (2) 변환 코드 사용
button.addActionListener(
  function2ActionListener(
    (_: ActionEvent) => println("pressed!")
  )
)

// (3) 암시적 변환 사용
button.addActionListener(
  (_: ActionEvent) => println("pressed!")
)

암시적 변환 규칙

  • 표시 규칙 : implicit로 표시한 정의만 검토 대상이다
  • 스코프 규칙 : 삽입할 implicit 변환은 스코프 내에 단일 식별자로만 존재하거나, 변환의 결과나 원래 타입과 연관이 있어야 한다(단, 원 타입이나 변환 결과 타입의 동반 객체에 있는 암시적 정의도 검토 대상임)
  • 한번에 하나만 규칙 : 오직 하나의 암시적 선언만 사용한다
  • 명시성 유선 규칙 : 코드가 그 상태 그대로 타입 검사를 통과한다면 암시를 통한 변환을 시도하지 않는다

암시적 변환 이름

  • 명시적으로 변환을 사용할 때 사용
  • 특정 지점에 사용 가능한 암시적 변환을 확인해야할 경우(사용하는 변환만 임포트)

암시가 사용되는 부분

  • 예상 타입 암시적 변환 : implicit def int2double(x: Int): Double = x.toDouble
  • 호출 대상 객체 변환 : "abc".exists === stringWrapper("abc").exists
// 호출 대상 객체 변환 - 새 타입과 함께 통합
implicit def intToRational(x: Int) = new Rational(x, 1)

val oneHalf = new Rational(1, 2)
1 + oneHalf
// Rational = 3/2
// 호출 대상 객체 변환 - 새로운 문법 흉내 내기
package scala
object Predef {
  class ArrowAssoc[A](x: A) {
    def -> [B](y: B): Tuple2[A, B] = Tuple2(x, y)
  }
  implicit def any2ArrowAssoc[A](x: A): ArrowAssoc[A] = new ArrowAssoc(x)
}

Map(1 -> "one", 2 -> "two", 3 -> "three")
  • 암시적 파라미터

암시적 파라미터

// 암시적 파라미터 예
class PreferredPrompt(val preference: String)

object Greeter {
  def greet(name: String)(implicit prompt: PreferredPrompt) {
    println("Welcome, "+ name +". The system is ready.")
    println(prompt.preference)
  }
}

// 명시적 사용 예
val bobsPrompt = new PreferredPrompt("relax> ")
Greeter.greet("Joe")(bobsPrompt)

// 암시적 파라미터 사용 예
object JoesPrefs {
  implicit val prompt = new Preferredprompt("Yes, master> ")
}

import JoesPrefs._
Greeter.greet("Joe")
// 암시적 파라미터 예 - 두개 파라미터
class PreferredPrompt(val preference: String)
class PreferredDrink(val preference: String)

object Greeter {
  def greet(name: String)(implicit prompt: PreferredPrompt, drink: PreferredDrink) {
    println("Welcome, "+ name +". The system is ready.")
    print("But while you work, ")
    println("why not enjoy a cup of "+ drink.preference +"?")
    println(prompt.preference)
  }
}

object JoesPrefs {
  implicit val prompt = new PreferredPrompt("Yes, master> ")
  implicit val drink = new PreferredDrink("tea")
}

import JoesPrefs._

// 명시적 사용 예
Greeter.greet("joe")(prompt, drink)

// 두 암시적 파라미터 사용 예
import JoesPrefs._
Greeter.greet("joe")
  • 암시적 파라미터 타입은 충분히 드물거나 특별해야한다. -> 암시적 파라미터의 타입 안에는 역할을 알려주는 이름을 최소한 하나 이상 사용하는 것이 좋다.
  • 암시적 파라미터는 앞쪽 파라미터 목록에서 명시적으로 사용한 인자 타입에 대한 정보를 제공하는 경우에 많이 사용된다.
// 원소 타입이 Ordered의 서브 타입이 아닌 리스트에서 사용할 수 없음!
def maxListUpBound[T <: Ordered[T]](elements: List[T]): T =
  elements match {
    case List() => throw new IllegalArgumentException("empty list!")
    case List(x) => x
    case x :: rest =>
      val maxRest = maxListUpBound(rest)
      if (x > maxRest) x
      else maxRest
  }

// 암시적 파라미터 사용
def maxListImpParm[T](elements: List[T])(implicit orderer: T => Ordered[T]): T =
  elements match {
    case List() => throw new IllegalArgumentException("empty list!")
    case List(x) => x
    case x :: rest =>
      val maxRest = maxListImpParm(rest)(orderer)
      if (orderer(x) > maxRest) x
      else maxRest
  }

뷰 바운드

  • 파라미터에 대해 implicit을 사용하는 경우, 메소드 본문 안에서 implicit 값으로 취급됨
// 메소드 본문 안에서 implicit 사용
def maxList[T](elements: List[T])(implicit orderer: T => Ordered[T]): T =
  elements match {
    case List() => throw new IllegalArgumentException("empty list!")
    case List(x) => x
    case x :: rest =>
      val maxRest = maxListImpParm(rest)    // orderer
      if (x > maxRest) x                    // orderer(x)
      else maxRest
  }

// 뷰 바운드 사용
def maxList[T <% Ordered[T]](elements: List[T]): T =
  elements match {
    case List() => throw new IllegalArgumentException("empty list!")
    case List(x) => x
    case x :: rest =>
      val maxRest = maxListImpParm(rest)    // orderer
      if (x > maxRest) x                    // orderer(x)
      else maxRest
  }
  • T <% Ordered[T] : T를 Ordered[T]로 다룰 수 있는 모든 T 타입을 사용할 수 있다
  • 타입 T가 이미 Ordered[T]인 경우에는 Predef의 identity 함수 사용
implicit def identity[A](x: A): A = x // 받은 객체를 그대로 반환함

적용 가능한 암시적 변환이 여러개인 경우

  • ~ Scala 2.7 : 컴파일 오류
  • Scala 2.8 : 더 구체적인 변환 적용
    • 인자 타입이 다른 것의 서브 타입이다
    • 변환이 모두 메소드인데, 하나를 둘러싼 클래스가 다른 것을 둘러싼 클래스를 확장한다.
val cba = "abc".reverse
// 2.7 : cba는 컬렉션 - String -> 스칼라 컬렉션 변환
// 2.8 : cba는 String - 더 구체적인 String -> SringOps 변환 추가(reverse 메소드가 String 반환)

암시 디버깅

  • 컴파일러가 암시를 찾지 못하는 경우, 명시적으로 변환을 써보고 오류가 발생하는지 확인한다.
  • 컴파일러에 -Xprint:typer 옵션 사용하여, 타입 검사기가 추가한 모든 암시적 변환이 있는 코드를 확인한다.


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

20장

  • 추상 멤버 : 클래스/트레이트 안에서 완전한 정의를 갖고 있지 않은 멤버
  • 메소드, 필드, 타입
// 추상 멤버 예
trait Abstract {
  type T
  def transform(x: T): T
  val initial: T
  var current: T
}

추상 타입

  • 실제 이름이 너무 길거나 의미가 불명확할 때
  • 서브 클래스에서 꼭 정의해야하는 추상 타입 선언

추상 val

  • 클래스 안에서 변수에 대해 값은 알 수 없지만, 인스턴스에서 변하지 않는 경우
// 추상 val 선언
val initial: String

// 구현
val initial = "hi"
  • 추상 메소드는 val 정의로 구현할 수도 있음

추상 var

trait AbstractTime {
  var hour: Int
  var minute: Int
}

// 위와 동일한 실제 확장 모습
trait AbstractTime {
  def hour: Int
  def hour_=(x: Int)
  def minute: Int
  def minute_=(x: Int)
}

추상 val 초기화

trait RationalTrait {
  val numerArg: Int
  val denomArg: Int
}

// 추상 val 정의 구현
// 익명 클래스 인스턴스 생성
new RationalTrait {
  val numerArg = 1
  val denomArg = 2
}

필드 미리 초기화

  • 필드 정의를 중괄호에 넣어서 슈퍼클래스 생성자 호출 앞에 위치
// 익명 클래스
new {
  val numerArg = 1 * x
  val denomArg = 2 * x
} with RationalTrait

// 정의 중인 객체/클래스의 extends 키워드 다음에 정의
object twoThirds extends {
  val numerArg = 2
  val denomArg = 3
} with RationalTrait
  • 초기화시 생성 중인 객체를 언급할 수 없음

지연 계산

object Demo {
  val x = { println("initializing x"); "done" }
}

// 지연 계산
object Demo {
  lazy val x = { println("initializing x"); "done" }
}

// Demo
// Demo.x
// 미리 초기화할 필요 없는 RationalTrait
trait LazyRationalTrait {
  val numerArg: Int
  val denomArg: Int
  lazy val numer = numerArg / g
  lazy val denom = denomArg / g
  override def toString = numer +"/"+ denom
  private lazy val g = {
    require(denomArg != 0)
    gcd(numerArg, denomArg)
  }
  private def gcd(a: Int, b: Int): Int =
    if (b == 0) a else gcd(b, a % b)
}

추상 타입 예

// 잘못된 예
class Food
abstract class Animal {
  def eat(food: Food)
}

class Grass extends Food
class Cow extends Animal {
  override def eat(food: Grass) {}  // 컴파일 안됨
}
// 올바른 예
class Food
abstract class Animal {
  type SuitableFood <: Food
  def eat(food: SuitableFood)
}

class Grass extends Food
class Cow extends Animal {
  type SuitableFood = Grass
  override def eat(food: SuitableFood) {}
}

경로에 의존하는 타입

  • 외부 객체에 이름을 붙임
  • 실제 타입이 같으면 동일한 타입
class DogFood extends Food
class Dog extends Animal {
  type SuitableFood = DogFood
  override def eat(food: DogFood) {}
}

val bessy = new Cow
val lassie = new Dog
lassie eat (new bessy.SuitableFood) // error
val bootsie = new Dog
lassie eat (new bootsie.SuitableFood)   // ok

구조적 서브타이핑

  • 이름에 의한 서브타입 : 클래서 A가 클래스 B를 상속하는 경우.
  • 구조적 서브타입 : 두 타입의 멤버가 같음
// 세분화한 타입 사용 예
Animal { type SuitableFood = Grass }

class Pasture {
  var animals: List[Animal { type SuitableFood = Grass }] = Nil
  // ...
}

// 여러 클래스를 그룹으로 다루는 예
def using[T <: { def close(): Unit }, S](obj: T)(operation: T => S) = {
  val result = operation(obj)
  obj.close()
  result
}

열거형

  • scala.Enumeration 클래스
  • 각 값의 타입은 Value
object Color extends Enumeration {
  val Red = Value
  val Green = Value
  val Blue = Value
}

// 위와 동일한 코드
object Color extends Enumeration {
  val Red, Green, Blue = Value
}

// Color.Value != Direction.Value
object Direction extends Enumeration {
  val North, East, South, West = Value
}
  • 열거형 값과 이름을 연관시킬수 있음
object Direction extends Enumeration {
  val North = Value("North")
  val East = Value("East")
  val South = Value("South")
  val West = Value("West")
}

for (d <- Direction.values) print(d + " ")
// North East South West

Direction.East.id
// 1

Direction(1)
// East

예 - 통화 변환

  • 첫번째 구현
abstract class Currency {
  val amount: Long
  def designation: String
  override def toString = amount +" "+ designation
  def + (that: Currency): Currency = ...
  def * (x: Double): Currency = ...
}

abstract class Dollar extends Currency {
  def designation = "USD"
}
  • 두번째 구현
abstract class AbstractCurrency {
  type Currency <: AbstractCurrency

  val amount: Long
  def designation: String
  override def toString = amount +" "+ designation
  def + (that: Currency): Currency = ...
  def * (x: Double): Currency = ...
}

abstract class Dollar extends AbstractCurrency {
  type Currency = Dollar
  def designation = "USD"
}
  • 최종 구현
abstract class CurrencyZone {
  type Currency <: AbstractCurrency
  def make(x: Long): Currency

  abstract class AbstractCurrency {
    val amount: Long
    def designation: String

    def + (that: Currency): Currency = make(this.amount + that.amount)
    def * (that: Double): Currency = make((this.amount * x).toLong)
    def - (that: Currency): Currency = make(this.amount - that.amount)
    def / (that: Double): Currency = make((this.amount / that).toLong)
    def / (that: Currency): Currency = this.amount.toDouble / that.amount
    def from(other: CurrencyZone#AbstractCurrency): Currency =
      make(math.round(
        other.amount.toDouble * Converter.exchangeRate(other.designation)(this.designation)))
    private def decimals(n: Long): Int =
      if (n < 10) 0 else 1 + decimals(n / 10)
    override def toString =
      ((amount.toDouble / CurrencyUnit.amount.toDouble) formatted ("%."+ decimals(CurrencyUnit.amount) +"f") +" "+ designation)
  }
  val CurrencyUnit: Currency
}

object Converter {
  var exchangeRate = Map(
    "USD" -> Map("USD" -> 1.0, "EUR" -> 0.7596, "JPY" -> 1.211, "CHF" -> 1.223),
    "EUR" -> Map("USD" -> 1.316, "EUR" -> 1.0, "JPY" -> 1.594, "CHF" -> 1.623),
    "JPY" -> Map("USD" -> 0.8257, "EUR" -> 0.6272, "JPY" -> 1.0, "CHF" -> 1.018),
    "CHF" -> Map("USD" -> 0.8108, "EUR" -> 0.6160, "JPY" -> 0.982, "CHF" -> 1.0)
  )
}

object US extends CurrencyZone {
  abstract class Dollar extends AbstractCurrency {
    def designation = "USD"
  }
  type Currency = Dollar
  def make(cents: Long) = new Dollar {
    val amount = cents
  }
  val Cent = make(1)
  val Dollar = make(100)
  val CurrencyUnit = Dollar
}

object Europe extends CurrencyZone {
  abstract class Euro extends AbstractCurrency {
    def designation = "EUR"
  }
  type Currency = Euro
  def make(cents: Long) = new Euro {
    val amount = cents
  }
  val Cent = make(1)
  val Euro = make(100)
  val CurrencyUnit = Euro
}

object Japan extends CurrencyZone {
  abstract class Yen extends AbstractCurrency {
    def designation = "USD"
  }
  type Currency = Yen
  def make(cents: Long) = new Yen {
    val amount = cents
  }
  val Yen = make(1)
  val CurrencyUnit = Yen
}

// 환전
Japan.Yen from US.Dollar * 100


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

19장

타입 파라미터화(type parameterization)

  • 제너릭 사용

함수형 큐

  • head
  • tail
  • enqueue
// enqueue 연산이 n 시간 걸림
class SlowAppendQueue[T] (elems: List[T]) {
  def head = elems.head
  def tail = new SlowAppendQueue(elems.tail)
  def enqueue(x: T) = new SlowAppendQueue(elems ::: List(x))
}
// head, tail 연산이 n 시간 걸림
class SlowHeadQueue[T] (smele: List[T]) {
  def head = smele.last
  def tail = new SlowHeadQueue(smele.init)
  def enqueue(x: T) = new SlowHeadQueue(x :: smele)
}
// head, tail, enqueue를 비슷한 빈도로 호출하는 경우, 상수 복잡도
class Queue[T] {
  private val leading: List[T]
  private val trailing: List[T]
} {
  private def mirror =
    if (leading.isEmpty) new Queue(trailing.reverse, Nil)
    else this
  def head = mirror.leading.head
  def tail = {
    val q = mirror
    new Queue(q.leading.tail, q.trailing)
  }
  def enqueue(x: T) = new Queue(leading, x :: trailing)
}

정보 은닉

비공개 생성자

  • 파라미터 목록 바로 앞에 private 수식자를 붙임
class Queue[T] private (
  private val leading: List[T],
  private val trailing: List[T]
)
  • 보조 생성자 생성
def this() = this(Nil, Nil)
def this(elems: T*) = this(elems.toList, Nil)   // T*는 파라미터를 반복 (8.8절)
  • 팩토리 메소드
object Queue {
  def apply[T](xs: T*) = new Queue[T](xs.toList, Nil)
}

비공개 클래스

trait Queue[T] {
  def head: T
  def tail: Queue[T]
  def enqueue(x: T): Queue[T]
}

object Queue {
  def apply[T](xs: T*): Queue[T] =
    new QueueImpl[T](xs.toList, Nil)
  
  private class QueueImpl[T] (
    private val leading: List[T],
    private val trailing: List[T]
  ) extends Queue[T] {
    def mirror =
      if (leading.isEmpty) new QueueImpl(trailing.reverse, Nil)
      else this
    def head: T = mirror.leading.head
    def tail: QueueImpl[T] = {
      val q = mirror
      new QueueImpl(q.leading.tail, q.trailing)
    }
    def enqueue(x: T) = new QueueImpl(leading, x :: trailing)
  }
}

변성(variance) 표기

  • 타입 생성자 : 타입 파라미터를 지정하면 타입을 만들 수 있음. (ex. Queue -> Queue[Int], Queue[String])
  • 제네릭 클래스/트레이트 : 여러 구체적인 타입 정의할 수 있음. (ex. 제네릭 큐 -> 구체적 큐)
  • 제네릭 타입은 기본적으로 무공변(nonvariant)

공변 서브타입 관계

  • 파라미터 앞에 +
  • T가 S의 서브타입 -> Queue[T]가 Queue[S]의 서브타입
trait Queue[+T] { ... }

반공변 서브타입 관계

  • 파라미터 앞에 -
  • T가 S의 서브타입 -> Queue[S]가 Queue[T]의 서브타입
trait Queue[-T] { ... }

변성과 배열

  • 배열은 무공변(자바는 공변)
  • 명시적인 변환이 필요함
val a1 = Array("abc")   // Array[String]
val a2: Array[Object] = a1.asInstanceOf[Array[Object]]

변성 표기 검사

  • 변경 가능 필드(세터 메소드)
  • 제네릭 타입의 파라미터가 메소드 파라미터의 타입인 경우
  • 본문의 모든 위치를 긍정적, 부정적, 중립적으로 구분하여 검사
  • + 표기는 긍정적 위치, - 표기는 부정적 위치에만 가능하며, 변성 표기 없는 것은 어디나 가능

위치 구분

  • 선언 중인 클래스의 최고 수준의 위치 = 긍정적
  • 하위 내포 수준은 그 수준을 둘러싼 상위 수준과 같음
  • 메소드 값 파라미터 위치는 메소드 바깥의 위치의 구분을 뒤집는다.
  • 메소드 타입 파라미터 위치는 구분을 뒤집는다.
  • - 표기의 경우 현재 구분을 뒤집는다.
abstract class Cat[-T, +U] {
  def meow[W](volume: T, listener: Cat[U, T]): Cat[Cat[U,T], U]
}
// meow 의 위치는 긍정적
// meow[W]( ... ) : W와 volume, listener의 위치는 모두 부정적
// listener: Cat[U,T] : U의 위치는 긍정적, T의 위치는 부정적
// 결과 타입 Cat[Cat[U,T], U] 의 위치는 긍정적
// Cat[Cat[U,T], U] : Cat[U,T]의 위치는 부정적, U의 위치는 긍정적
// 결과 타입의 Cat[U,T] : U는 긍정적, T는 부정적
// T는 부정적인 위치에서만 사용, U는 긍정적 위치에서만 사용

하위 바운드

  • 타입 파라미터에 하위 바운드를 사용하면, 부정적인 위치의 타입을 공변적으로 만들 수 있음
class Queue[+T] (private val leading: List[T], private val trailing: List[T]) {
  def enqueue[U >: T](x: U) = new Queue[U](leading, x :: trailing)
  // U는 T의 수퍼 타입
  // ...
}

반공변성

trait OutputChannel[-T] {
  def write(x: T)
}

// OutputChannel[String]이 필요한 곳이라면 OutputChannel[AnyRef]를 바꿔놓아도 문제 없음
  • 리스코프 치환 원칙 : U타입의 값이 필요한 모든 경우를 T타입으로 대치할 수 있다면, T타입을 U타입의 서브타입으로 가정해도 안전함.
  • T가 U의 모든 연산을 지원하고, 모든 T의 연산이 그에 대응하는 U의 연산에 비해 더 적게 요구하고 더 많이 제공하는 경우 성립.
class Publication(val title: String)
class Book(title: String) extends Publication(title)
object Library {
  val books: Set[Book] =
    Set(
      new Book("Programming in Scala"),
      new Book("Walden")
    )
  def printBookList(info: Book => AnyRef) {
    for (book <- books) println(info(book)) // println은 인자에 toString을 호출해 출력
  }
}

object Customer extends Application {
  def getTitle(p: Publication): String = p.title
  Library.printBookList(getTitle)
}

객체의 비공개 데이터

  • 어떤 필드가 그 객체에서만 접근 가능하다면, 변성에 아무 문제를 일으키지 않음
class Queue[+T] private (
  private[this] var leading: List[T],
  private[this] var trailing: List[T]
) {
  private def mirror() =
    if (leading.isEmpty) {
      wihle (!trailing.isEmpty) {
        leading = trailing.head :: leading
        trailing = trailing.tail
      }
    }
  def head: T = {
    mirror()
    leading.head
  }
  def tail: Queue[T] = {
    mirror()
    new Queue(leading.tail, trailing)
  }
  def enqueue[U >: T](x: T) = new Queue[U](leading, x :: trailing)
}

상위 바운드

class Person(val firstName: String, val lastName: String) extends Ordered[Person] {
  def compare(that: Person) = {
    val lastNameComparison = lastName.compareToIgnoreCase(that.lastName)
    if (lastNameComparison != 0) lastNameComparison
    else firstName.compareToIgnoreCase(that.firstName)
  }

  override def toString = firstName +" "+ lastName
}

// T는 Ordered의 서브타입이어야 함
def orderedMergeSort[T <: Ordered[T]](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 (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(orderedMergeSort(ys), orderedMergesort(zs))
  }
}


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 클래스의 생성자에 전달


+ Recent posts