* fp 는 수학적인 함수식을 원한다.

18.1 무엇이 객체를 변경 가능하게 하는가?

: 순수 함수형 객체와 변경 가능한 객체 간의 차이는 객체의 필드에 접근하거나 메소드를 호출하면 항상 동일한 결과가 나오냐에 있다.

val cs = List('a', 'b', 'c')

cs.head                       // cs.head는 cs를 정의한 시점부터 무조건 'a'

res0: Char = a 

 class BankAccount {

  private var bal: Int = 0

  def balance: Int = bal

  def deposit(amount: Int) {
    require(amount > 0)
    bal += amount
  }

  def withdraw(amount: Int): Boolean = 
    if (amount > bal) false
    else {
      bal -= amount
      true
    }

 


scala> val account = new BankAccount
account: BankAccount = BankAccount@bf5bb7

scala> account deposit 100

scala> account withdraw 80
res1: Boolean = true

scala>  account withdraw 80      
res2: Boolean = false

: 변경 가능한 객체는 이전에 어떤 연산자를 실행했는가에 따라 결과가 달라진다.

 

: 필드에 var이 있다고 변경 가능한 객체는 아니다.

Ex. 어떤 클래스가 변경 가능한 상태를 가진 다른 객체에게 메소드 호출을 위임하기 때문에 var를 상속하거나 정의하지 않고도 변경 가능한 상태를 가질 수 있다.

 

: var을 포함하더라도 순수 함수일 수 도 있다.

Ex. 최적화를 위해 결과를 필드에 캐시하는 클래스

class Keyed {
  def computeKey: Int = ... // this will take some time
  ...
}

 

class MemoKeyed extends Keyed {
  private var keyCache: Option[Int] = None
  override def computeKey: Int = {
    if (!keyCache.isDefined) keyCache = Some(super.computeKey)
    keyCache.get
  }
}

 

18.2 재할당 가능한 변수와 프로퍼티

: 보통 재할당 가능한 변수에 대해 게터와 세터 메소드를 명시적으로 정의해 추가한다. 하지만, 스칼라는 비공개가 아닌 모든 var 멤버에 게터와 세터를 자동으로 정의해준다.

: var x의 게터는 x이고, 세터는 x_= 이다.

 

Ex. var hour = 12 가 있으면 'hour' 게터와 'hour_=' 세터가 생긴다. 필드에는 항상 private[this]가 붙는다. 

 

" 두 클래스 정의는 동일하다 " 

class Time {
  var hour = 12
  var minute = 0
}

class Time {
  private[this] var h = 12
  private[this] var m = 0

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

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

: 게터와 세터를 직접 정의해 변수 할당과 변수 접근 연산을 원하는 대로 변화시킬 수 있다.

=> require을 이용해 허용해서는 안 되는 값을 변수에 할당하지 못 하게 

=> 어떤 변수에 대한 모든 접근을 로그로 남기기 위해

=> 변수에 이벤트를 접목해 어떤 변수를 변경할 때마다 구독을 요청한 다른 객체들에게 통지하게 

class Time {

  private[this] var h = 12
  private[this] var m = 0

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

  def minute = 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"
}

 

scala> val t = new Thermometer
t: Thermometer = 32.0F/0.0C

scala> t.celsius = 100

scala> t
res3: Thermometer = 212.0F/100.0C

scala> t.fahrenheit = -40

scala> t
res4: Thermometer = -40.0F/-40.0C

* 필드 초기화에 '=_'을 사용하면 필드에 제로를 할당(수 타입은 0, 불리언 타입은 false, 참조 타입은 null)

* 스칼라는 '=_' 초기화를 생략 불가 => "var celsius:Float" 는 초기화가 아닌 추상 변수

18.3 사례 연구 : 이산 이벤트 시뮬레이션

: 디지털 회로 시뮬레이터 예제 - 시뮬레이션 동안 실행한 동작 기록

 

< 디지털 회로 >

- Wire 클래스 : 디지털 회로를 구성하는 선 

- def inverter(input: Wire, output: Wire)  :  신호를 반전시킨다.

- def andGate(a1: Wire, a2: Wire, output: Wire)  :  출력을 입력의 교집합으로 설정한다.

- def orGate(a1: Wire, a2: Wire, output: Wire)   :  출력을 입력의 합집합으로 설정한다.

- 각 게이트는 시간 지연 존재 => 게이트 입력이 변한 뒤에 일정한 시간이 지나야 게이트의 출력이 변한다.

 

" 반가산기 구현 "

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)
}

 

< 시뮬레이션 API >

: 사용자가 정의한 액션을 구체적으로 수행한다.

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],        // agenda에 작업 항목 삽입, 실행 시간에 따라 정렬해야 하는 조건 유지
      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)    // delay 시간 뒤에 작업할 액션을 만들고 insert 수행

        (block: => Unit) {       // 이름에 의한 파라미터
    val item = WorkItem(currentTime + delay, () => block)    
    agenda = insert(agenda, item)
  }

  private def next() {          // agenda의 첫 번째 작업 액션 실행
    (agenda: @unchecked) match {
      case item :: rest => 
        agenda = rest 
        curtime = item.time
        item.action()
    }
  }

  def run() {            // agenda의 모든 작업 수행(next 메소드를 통해 하나씩 순차적으로 작업 수행)
    afterDelay(0) {
      println("*** simulation started, time = "+
          currentTime +" ***")
    }
    while (!agenda.isEmpty) next()
  }
}

 

< 회로 시뮬레이션 >

abstract class BasicCircuitSimulation extends Simulation {
  
  def InverterDelay: Int      // Inverter, andGate, orGate 지연 시간 추상 메소드
  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 (_ ())             //  ' f => f() " 축약형
      }
  
    def addAction(a: Action) = {       // 선에 액션 추가, 액션 한 번 실행
      actions = a :: actions
      a()
    }
  }
  
  def inverter(input: Wire, output: Wire) = {   // 입력으로 받은 선에 InterverDelay 후 출력 신호를 반전시키는 액션 설치
    def invertAction() {
      val inputSig = input.getSignal
      afterDelay(InverterDelay) {
        output setSignal !inputSig 
      }
    }
    input addAction invertAction
  }
  
  def andGate          // 두 입력 신호의 교집한한 결과를 AndGateDelay 후 output에 설정하는 액션 설치

       (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            // 입력 신호 중 하나의 신호만 바껴도 and 재계산
    a2 addAction andAction
  }

  def orGate                    // 두 입력 신호의 합집합한 결과를 OrGateDelay 후 output에 설정하는 액션 설치

       (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           // 입력 신호 중 하나의 신호만 바껴도 or 재계산
    o2 addAction orAction
  }
  
  def probe(name: String, wire: Wire) {    // 선의 신호가 변할 때마다 실행해 신호 변화 감지 액션 설치
    def probeAction() {
      println(name +" "+ currentTime +
          " new-value = "+ wire.getSignal)
      }
      wire addAction probeAction
  }
}

 

 

< 최종 실행 >

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)
  }
}

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

object main extends App{
  val input1, input2, sum, carry = new MySimulation.Wire
  MySimulation.probe("sum", sum)
  MySimulation.probe("carry", carry)
  MySimulation.halfAdder(input1, input2, sum, carry)
  
  input1 setSignal true
  MySimulation.run()
  
  input2 setSignal true
  MySimulation.run()
}


-------------------------------------------------

 

sum 0 new-value = false
carry 0 new-value = false
*** simulation started, time = 0 ***
sum 8 new-value = true
*** simulation started, time = 8 ***
carry 11 new-value = true
sum 15 new-value = false

17.1 시퀀스

: 시퀀스 타입은 순서가 정해진 데이터 그룹을 가지고 작업할 수 있게 해준다.

 

< 리스트 >

: 변경 불가능한 연결 리스트로 원소를 앞 부분에 빠르게 추가/삭제할 수 있지만 리스트를 순차적으로 따라가야해 임의의 위치에 접근할 때 빠르지 않다. 

: 첫 번째 원소를 빠르게 추가/삭제할 수 있는 건 패턴 매치를 잘할 수 있다는 뜻

: 불변성은 리스트를 복사하지 않아도 되기 때문에 효율적이면서 올바른 알고리즘 개발에 도움

 

< 배열 >

: 임의의 위치에 있는 원소에 효율적으로 접근

: 배열은 '[]'가 아니라 '()'에 인덱스를 넣어서 원소 접근

 

< 리스트 버퍼 >

: 변경 가능한 객체로 상수 시간에 원소를 앞/뒤로 추가 가능 

    * 리스트로 끝부분에 원소를 추가하려면 리스트의 앞에 원소를 차례로 추가해 뒤집힌 리스트를 만들고 reverse 호출

- append , "+=" : 원소를 뒤에 추가

- prepend, "+=:"  : 연산자를 통해 원소를 앞에 추가

- toList : ListBuffer => List

- 잠재적인 스택 오버플로를 피하기 위해 List 대신 ListBuffer 사용(재귀, for, while에서)

scala> import scala.collection.mutable.ListBuffer
import scala.collection.mutable.ListBuffer

scala> val buf = new ListBuffer[Int]             
buf: scala.collection.mutable.ListBuffer[Int] = ListBuffer()

scala> buf += 1                                  
res4: buf.type = ListBuffer(1)

scala> buf += 2                                  
res5: buf.type = ListBuffer(1, 2)

scala> buf     
res6: scala.collection.mutable.ListBuffer[Int]
  = ListBuffer(1, 2)

scala> 3 +=: buf                                  
res7: buf.type = ListBuffer(3, 1, 2)

scala> buf.toList
res8: List[Int] = List(3, 1, 2)

 

< 배열 버퍼 >

: 끝 부분과 시작 부분에 원소를 추가/삭제할 수 있다는 점만 제외하면 배열과 같다.

: 새 원소 추가/삭제하는 데 평균적으로 상수시간이 걸리나, 때때로 버퍼의 내용을 저장하기 위해 새로운 배열을 할당해야 하기 때문에 종종 선형 시간이 걸린다.

scala> import scala.collection.mutable.ArrayBuffer
import scala.collection.mutable.ArrayBuffer


scala> val buf = new ArrayBuffer[Int]()             // 생성할 때 타입 인자 지정, 크기는 필요에 따라 자동 조절
buf: scala.collection.mutable.ArrayBuffer[Int] = 
  ArrayBuffer()


scala> buf += 12
res9: buf.type = ArrayBuffer(12)

scala> buf += 15
res10: buf.type = ArrayBuffer(12, 15)

 

< 문자열(StringOps를 통해서) >

: Predef 에 String을 StringOps로 바꾸는 암시적 변환이 있어 문자열을 시퀀스처럼 다룰 수 있다.

scala> def hasUpperCase(s: String) = s.exists(_.isUpper)
hasUpperCase: (s: String)Boolean

scala> hasUpperCase("Robert Frost")
res14: Boolean = true

 

17.2 집합과 맵

: 스칼라의 Set과 Map은 변경 불가능한 객체와 변경 가능한 객체 존재

: 모든 스칼라의 소스 파일에 자동으로 임포트하는 Predef 객체에 변경 불가능한 맵, 집합이 정의되어 있음 

object Predef {
  type Map[A, +B] = collection.immutable.Map[A, B]             //  Map 타입
  type Set[A] = collection.immutable.Set[A]
  val Map = collection.immutable.Map                         // Map 싱글톤 객체
  val Set = collection.immutable.Set
  // ...
}

 

< 집합의 사용 >

: 집합은 특정 객체를 최대 하나만 들어가도록 보장한다

 

< Map 의 사용 >

: 어떤 값과 집합의 각 원소 사이에 연관 관계를 만든다.

 

< 디폴트 집합과 맵 >

: scala.collection.mutable.Set() 팩토리 메소드는 내부적으로 해시 테이블을 사용하는 scala.collection.mutable.HashSet을 반환한다. scala.collection.mutable.Map() 팩토리 메소드도 ~.HashMap을 반환한다.

: 변경 불가능한 집합과 맵은 팩토리에 얼마나 많은 원소를 전달하느냐에 따라 달라진다.(원소가 5개 보다 적은 집합에 대해서는 성능을 극대화하기 위해 특정 크기의 집합만 담당하는 특별한 클래스를 사용한다.)

 

< 정렬된 집합과 맵 >

- 정해진 순서대로 원소를 반환하는 이터레이터를 제공하는 맵이나 집합을 사용할 때 SortedSet, SortedMap 트레이트 사용

- SortedSet과 SortedMap 구현이 TreeSet, TreeMap

- 순서를 커스텀하게 정할 때, 정렬된 집합이나 정렬된 맵의 키에 Ordered 트레이트를 혼합하거나 암시적으로 Ordered 트레이트로 변환 가능해야 한다. 

scala> import scala.collection.immutable.TreeSet
import scala.collection.immutable.TreeSet

scala> val ts = TreeSet(9, 3, 1, 8, 0, 2, 7, 4, 6, 5)
ts: scala.collection.immutable.TreeSet[Int] = TreeSet(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)

 

scala> import scala.collection.immutable.TreeMap
import scala.collection.immutable.TreeMap

scala> var tm = TreeMap(3 -> 'x', 1 -> 'x', 4 -> 'x')
tm: scala.collection.immutable.TreeMap[Int,Char]
  = Map(1 -> x, 3 -> x, 4 -> x)

scala> tm += (2 -> 'x')

scala> tm
res30: scala.collection.immutable.TreeMap[Int,Char] = Map(1 -> x, 2 -> x, 3 -> x, 4 -> x)

 

17.3 변경 가능 컬렉션과 변경 불가능 컬렉션

: mutable, immutable 중 어떤 것을 선택해야 할지 모르겠다면, immutable로 시작하고 나중에 필요에 따라 바꾸는 게 좋다. ( immutable이 mutable 보다 프로그램 추론이 쉽다. )

 

: mutable을 먼저 사용한다면 코드가 복잡하고 추론하기 어려울 때 immutable 고려( 변경 가능 컬렉션의 복사본을 언제 만들어야 할지 고민되거나, 누가 갖고 있는지나 누구에게 포함되어 있는지 많이 생각해야 한다면 )

 

: 컬렉션에 저장할 원소의 수가 적을 경우 immutable이 mutable보다 더 작게 메모리를 사용 

Ex. 변경 가능한 빈 HashMap은 80바이트이고 원소를 하나 추가할 때마다 16바이트가 더 든다. 변경 불가능한 빈 맵은 싱글톤 객체 하나를 모든 참조가 공유하기 때문에 포인터 필드 하나만큼의 메모리만 필요하다.

 

: 변경 불가능한 맵/집합은 크기가 4일 때까지 단일 객체(Set1~Set4, Map1~Map4)를 사용한다. 단일 객체 크기는 16~40 바이트 정도로 변경 가능한 맵/집합보다 공간이 훨씬 작다. => 많은 컬렉션이 작은 크기라면 컬렉션을 변경 불가능하게 만드는 것이 공간을 절약하고 성능을 향상하는 중요한 선택

 

: 스칼라에서는 맵/집합에 데이터를 조금 저장하는 것을 추구, immutable은 값을 매번 복사 => 메모리를 많이 잡아먹음 => 별도 객체

 

" 변경 불가능한 맵 "

var capital 

    = Map("US" -> "Washington", "France" -> "Paris")
capital += ("Japan" -> "Tokyo")
println(capital("France")) 

" 변경 가능한 맵 "

import scala.collection.mutable.Map // only change needed!
var capital = Map("US" -> "Washington", "France" -> "Paris")
capital += ("Japan" -> "Tokyo")
println(capital("France")) 

 

17.4 컬렉션 초기화

: 일반적인 초기화 방법은 초기 원소를 컬렉션 동반 객체의 팩토리 메소드에 넘기는 것(apply 메소드 호출로 변환)

: 동반 객체의 팩토리 메소드에 타입 명시 가능

scala> import scala.collection.mutable
import scala.collection.mutable

scala> val stuff = mutable.Set(42)
stuff: scala.collection.mutable.Set[Int] = Set(42)

scala> stuff += "abracadabra"
:15: error: type mismatch;
   found   : java.lang.String("abracadabra")
   required: Int
         stuff += "abracadabra"
                  ^

scala> val stuff = mutable.Set[Any](42)             // 타입 명시
stuff: scala.collection.mutable.Set[Any] = Set(42)

: 어떤 컬렉션을 다른 컬렉션으로 초기화하는 경우

scala> val colors = List("blue", "yellow", "red", "green")
colors: List[java.lang.String]
  = List(blue, yellow, red, green)


scala> import scala.collection.immutable.TreeSet
import scala.collection.immutable.TreeSet

scala> val treeSet = TreeSet(colors)                 
:15: error: could not find implicit value for 
parameter ord: Ordering[List[java.lang.String]]
      val treeSet = TreeSet(colors)                 
                              ^

scala> val treeSet = TreeSet[String]() ++ colors
treeSet: scala.collection.immutable.TreeSet[String]
     = TreeSet(blue, green, red, yellow)

 

< 배열이나 리스트로 바꾸기 >

: 배열이나 리스트로 바꿀 때 toList, toArray를 호출하면 된다.

: toList, toArray를 호출했을 때 각 원소의 순서는 호출하는 컬렉션이 생성한 이터레이터가 돌려주는 원소의 순서와 같다.

scala> treeSet.toList
res50: List[String] = List(blue, green, red, yellow)

 

< 변경 가능한 집합(맵)과 변경 불가능 집합(맵) 사이의 변환 >

: empty 메소드를 사용해 새로운 타입의 컬렉션 생성 => "++" , "++=" 연산자를 사용해 원소 추가

scala> import scala.collection.mutable
import scala.collection.mutable

scala> treeSet
res52: scala.collection.immutable.TreeSet[String] = 
  TreeSet(blue, green, red, yellow)

scala> val mutaSet = mutable.Set.empty ++= treeSet
mutaSet: scala.collection.mutable.Set[String] = 
  Set(yellow, blue, red, green)

scala> val immutaSet = Set.empty ++ mutaSet
immutaSet: scala.collection.immutable.Set[String] = 
  Set(yellow, blue, red, green)

 

17.5 튜플

: 배열, 리스트와 달리 여러 타입의 원소를 넣을 수 있다. => 데이터만 저장하는 단순한 클래스를 정의해야 하는 번거로움을 덜 수 있다.

: 각기 다른 타입의 객체를 결합할 수 있기 때문에 Traversable을 상속 X

: 튜플 원소에 접근할 대는 _n 메소드 사용(인덱스가 1부터 시작)

scala> longest = ("quick", 1)

scala> longest._1      // 첫 번째 원소
res53: String = quick

scala> longest._2
res54: Int = 1

 

scala> val (word, idx) = longest      // 패턴 매치
word: String = quick
idx: Int = 1

scala> word
res55: String = quick


scala> val word, idx = longest    // '()'를 생략하면 다른 결과, 한 표현식에서 여러 개의 정의 제공
word: (String, Int) = (quick,1)
idx: (String, Int) = (quick,1)

: 튜플은 'A 하나와 B 하나' 수준을 넘지 않는 데이터를 한데 묶을 때 아주 유용 

: 결합에 어떤 의미가 있거나 결합에 어떤 메소드를 추가하기 원한다면 클래스를 생성하는 게 좋다.(Ex. 연,월,일 => Date 클래스)  => 코드 이해가 쉽고, 컴파일러가 제공하는 기능을 통해 오류를 쉽게 찾을 수 있다.

+ Recent posts