Programming in Scala 18장

여러가지로 바빠서 이제야 번역을 마치게 되었네요. 상태 기반 객체와 그에 대한 예제를 디지털 회로 시뮬레이션을 소개하는데, 잘 와닿지는 않습니다. 실무에서 디지털 회로를 구현할 일이 없으니깐요-_–;; 전체적으로 매끄럽진 않지만 대충 번역하고 19장으로 넘어갑니다.

18 Stateful Objects

이 장에서는 상태 기반 객체가 무엇인지 그리고 Scala에서 이를 표현하기 위한 어떤 문법을 제공하는지를 설명한다. 이 장의 두 번째 부분에서는 상태 기반 객체뿐만 아니라 디지털 회로를 정의하며 내부 DSL(domain specific language)을 만들게 되는 이산 사건 시뮬레이션 소개한다.

18.1 What makes an object stateful?

객체의 구현을 살펴보지 않아도 순수 함수형 객체와 상태 기반 객체의 근본적인 차이점을 볼 수 있다. 순수 함수형 객체의 메소드를 호출하거나 필드를 역참조할 때는 항상 같은 결과를 얻게 된다. 예를 들면, 캐릭터형 리스트에 대해

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

cs.head를 적용하면 항상 ‘a’를 반환한다.

반면에 상태 기반 객체의 경우 메소드 호출이나 필드의 억세스 결과는 그 객체에 대해 무슨 동작이 이전에 실행되었는지에 의존한다.

상태 기반 객체의 좋은 예는 은행 계좌이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
      }
}

BankAccount 클래스는 private 변수 bal과 세 개의 public 메소드를 정의하고 있다. balance는 현재 잔고를 반환하고, deposit은 주어진 amountbal 에 더하고, withdraw는 남은 잔고가 음수가 아닌지 확인하면서 주어진 ‘amount’를 ‘bal’에서 뺀다. withdraw의 반환값은 요청 자금이 성공적으로 인출되었는지 아닌지를 나타내는 Boolean이다.

BankAccount의 내부 동작에 대해 아무 것도 몰라도 BankAccount가 상태 기반 객체인 것은 알 수 있다.

1
2
3
4
val account = new BankAccount
account deposit 100
account withdraw 80 // true
accout withdraw 80 // false

위 두 개의 인출 인터랙션이 다른 결과를 반환하고 있다는 것을 주목하자. 첫 번째 인출 동작은 true를 반환하지만, 두 번째 인출 동작은 false를 반환한다. 따라서 분명하게 은행 계정은 같은 동작이 다른 시간에 다른 결과를 반환할 수 있기 때문에 가변적인 상태를 지니고 있다.

BankAccount의 상태성(statefulness)은 var 정의를 포함하고 있기 때문에 보나마나 분명하다고 생각할 수 있다. 보통 상태와 var는 관련되어 있지만, 일이란게 항상 그리 명확한 것은 아니다. 예를 들면, 클래스는 var를 정의하거나 상속하지 않더라도 상태가 있을 수 있는데, 가변 상태를 지닌 다른 객체의 메소드 호출을 진행하기 때문이다. var를 포함하는 클래스이면서 여전히 순수 함수형(purely funtional)인 역의 경우도 마찬가지로 가능하다. 한 예로는 최적화 목적을 위해 값비싼 연산의 결과를 캐시하는 클래스가 될 것이다. 값비싼 computeKey를 지닌 다음의 최적화되지 않은 Keyed 클래스를 가정해보자.

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

computeKey가 어떤 var 변수도 읽거나 쓰지 않는다고 주어지면, 캐시를 추가함으로써 좀 더 효율적인 Keyed를 만들 수 있다.

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

MemoKeyed를 사용하면 (캐시로 인해) 속도를 얻는 것 외에도 Keyed 클래스와 MemoKeyed 클래스 동작이 정확하게 동일하다. 결과적으로 Keyed 클래스가 순수 함수형이면, 변수를 재할당하는 MemoKeyed도 같은 순수 함수형이다.

18.2 Reassignable variables and properties

재할당 가능한 변수에 대해 두 가지 기본적인 연산을 수행할 수 있다. 그 값을 획득하거나 혹은 새로운 값을 설정하는 것이다. Scala에서 모든 var는 자신의 getter와 setter 메소드를 암묵적으로 정의하는 어떤 객체의 비private 멤버이다. (비 private 멤버, public이거나 protected 라는 뜻인 듯..)

그러나 이러한 getter와 setter는 Java 컨벤션과 다른 이름을 지닌다. var x의 getter는 그냥 “x”의 이름을 가지는데, setter는 “x_=”의 이름을 가진다.

예를 들면, 클래스에 나타나는 var 정의는

1
var hour = 12

“hour” getter를 생성하고, 이외에도 재할당 가능한 변수인 “hour_=” setter를 생성한다. 이 필드는 항상 private[this]로 표기되는데, 이 변수를 소유하는 객체에서만 접근할 수 있다는 것을 의미한다. 반면에 getter와 setter는 원본 var와 같은 가시성을 지닌다. var 정의가 public이면 getter와 setter도 그러하고, protected이면 이들도 protected 이며 나머지 경우도 이와 같다.

예를 들어, 다음의 Time 클래스를 고려해보자, 이 클래스는 hourminute를 지닌 두 개의 public var가 정의되어 있다.

1
2
3
4
class Time {
  var hour = 12
  var minute = 0
}

이 클래스의 구현은 아래에 기술된 클래스 정의와 정확하게 동일하다.

1
2
3
4
5
6
7
8
9
10
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 }
}

지역 필드 hm의 이름은 이미 사용 중인 어떤 이름과도 충돌하지 않기 위해 임의로 선정된다.

var의 이러한 gette와 setter 확장에 대한 흥미로운 점은 var를 정의할 것인지, getter와 setter를 직접 정의할 것인지를 선택할 수 있다는 것이다. 예를 들면, 다음의 Time 클래스는 잘못된 값으로 할당하는 hourminute을 잡아내는 요구사항을 지니고 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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: Int = m
  def minute_=(x: Int){
      require(0 <= x && x < 60)
      m = x
  }
}

변수를 setter와 getter의 쌍으로 해석하는 Scala 컨벤션은 C#의 프로퍼티와 같은 능력을 제공하는 효과가 있다. 프로퍼티는 변수의 getter와 setter에 대한 모든 억세스를 로깅하는데 사용할 수 있다. 또는 변수를 이벤트와 연동할 수 있다. 예를 들면, 변수가 수정될 때마다 어떤 구독 메소드에 통지할 수 있다. (이 예제는 35장에서 볼 수 있다.)

관련된 필드가 없는 getter와 setter를 정의하는 것도 가능하다. 다음의 Thermometer 클래스는 읽고 갱신될 수 있는 온도 변수를 은닉화한다.

1
2
3
4
5
6
7
8
9
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"
}

celsius 변수는 변수의 “값 초기화”인 _로 기술되어 기본값으로 설정된다. 좀 더 정확히는 필드 ”=_“ 초기자(initializer)는 그 필드에 제로값을 할당한다. 제로값은 필드의 타입에 좌우된다. number 타입은 0이고, boolean 타입은 false이고 참조 타입은 null이다.

Scala에서는 “=_” 초기자를 간단히 제거할 수 없다는 것을 주의해야 한다. 만약,

1
var celsius: Float

라고 작성하면, 이는 초기화되지 않은 추상 변수(abstract value)를 선언하게 된다.

celsius 변수 정의 다음에 “fahrenheit” getter와 “fahrenheit_=” setter가 정의되어 있는데, 같은 온도를 억세스하지만 화씨로 계산한다. 화씨의 현재 온도값을 저장하기 위한 별도의 필드가 없다. 대신에 화씨값의 setter와 getter 메소드는 자동으로 섭씨 온도로 바꾼다.

1
2
3
4
5
6
7
8
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

18.3 Case study: Discrete event simulation

이 장의 나머지는 상태기반 객체가 일급 함수와 어떻게 결합될 수 있는지 확장된 예제로 살펴본다. 우리는 디지털 회로의 시뮬레이터의 설계와 구현을 살펴볼 것이다. 첫 번째 디지털 회로를 위한 약간의 언어를 살펴보고 두 번째 간단하지만 이산(discrete) 사건 시뮬레이션을 위한 일반적인 프레임워크를 설명한다. 마지막으로 이산 시뮬레이션 프로그램이 어떻게 구성되고 빌드될 수 있는지를 살펴본다.

이 예제는 고전인 “Structure and Interpretation of Computer Programs (by Abelson and Sussman)”에서 따왔다. 다른 점은 구현 언어가 Scheme이 아니라 Scala이며 예제의 다양한 측면들이 4개의 소프트웨어 레이어로 구조화된다는 것인데, 하나는 시뮬레이션 프레임워크이고, 다른 하나는 기본 회로 시뮬레이션 패키지이고, 세번째는 사용자정의 회로 라이브러리이고, 마지막은 각각의 시뮬레이션되는 회로 자체이다. 각 레이어는 클래스로 표현되며, 좀 더 구체적이 레이어는 보다 일반적인 레이어를 상속한다.

18.4 A language for digital circuits

디지철 회로를 기술하는 작은 언어로 시작하자. 디지털 회로는 배선(wire)과 기능 박스(function box)로 만들어진다. 배선은 기능 박스에 의해 변환되는 신호를 전달한다. 신호는 boolean으로 표현되는데, true는 signal-on 상태이고 false는 signal-off 상태를 나타낸다.

다음은 3가지 기본 기능 박스 (혹은 게이트)이다.

  • inverter: 시그널의 부정을 취한다.
  • and-gate: 입력에 대한 논리곱을 출력으로 설정한다.
  • or-gate: 입력에 대한 논리합을 출력으로 설정한다.

이런 게이트는 모든 다른 기능 박스(function box)를 만드는 데에 충분하다. 게이트에는 “delay”가 있는데, 이 때문에 게이트의 출력은 입력이 바뀌고 지연 시간이 지난 후에야 바뀌게 된다.

다음의 Scala 클래스와 함수 집합에 의해 디지털 회로의 요소들을 기술할 것이다. 먼저, 배선을 위한 Wire 클래스가 있다.

1
2
3
val a = new Wire
val b = new Wire
val c = new Wire

좀 더 간단한 같은 표현은 다음과 같다.

1
val a, b, c = new Wire

두 번째로, 필요한 기본 게이트를 ‘만드는’ 3가지 프로시져들이다.

1
2
3
def inverter(input: Wire, output: Wire)
def andGate(a1: Wire, a2: Wire, output: Wire)
def orGate(o1: Wire, o2: Wire, output: Wire)

함수에 주안점을 둔 Scala에 있어 흔치 않은 것은 이런 프로시져는 결과로써 구성된 게이트를 반환하는 것이 아니라 부작용(side-effect)으로써 게이트를 만든다는 것이다. 예를 들면, inverter(a, b)의 호출은 배선 ab 사이의 인버터를 둔다.

더 복잡한 기능은 기본 게이트로 만들 수 있다. 예를 들면, 다음은 반가산기를 구성한다. halfAdder 메소드는 두 개의 입력 ab를 취해 “s = (a + b) % 2”로 정의된 합계 s를 연산하고, “c = (a + b) /2”라고 정의된 자리올림수 c를 출력한다.

1
2
3
4
5
6
7
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)
}

더 복잡한 회로를 구성하기 위해서는 halfAdder 메소드를 사용할 수 있다. 예를 들면, 다음은 전가산기를 나타낸다. 두 개의 입력 ab, 그리고 자리올림수 cin을 취해 sum = (a + b + cin) % 2로 정의되는 합계를 연산하고 s 그리고 cout = (a + b + cin) / 2로 정의된 자리올림수를 출력한다.

1
2
3
4
5
6
def fullAdder(a: Wire, b: Wire, cin: Wire, sum: Wire, cout: Wire) {
  val s, c1, c2 = new Wire
  halfAddr(a, cin, s, c1)
  halfAddr(b, s, sum, c2)
  orGate(c1, c2 cout)
}

클래스 Wire와 함수 inverter, andGate 그리고 orGate는 사용자가 디지털 회로를 정의할 수 있게 되는 작은 언어를 대표한다. 이는 개별적으로 구현되어지기 보다는 호스트 언어 내에 라이브러리로 정의되는 내부 DSL(domain specific lanauge)의 좋은 예이다.

회로 DSL 구현은 아직 해결되어야할 과제가 있다. DSL로 회로를 정의하는 목적이 회로를 시뮬레이션하는 것이기 때문에 이산 사건 시뮬레이션을 위한 일반적인 API에 기반을 두고 DSL을 구현하는 것이 타당하다. 다음 두 섹션은 먼저 시뮬레이션 API와 다음으로 그 위에 회로 DSL의 구현을 제시할 것이다.

18.5 The Simulation API

시뮬레이션 API는 다음에 나타나 있다. org.stairwarybook.simulation 패키지의 Simulation 클래스로 구성된다. 구체적인 시뮬레이션 라이브러리는 이 클래스를 상속해서 도메인 특정한 기능들을 증대시킬 것이다.

이산 사건 시뮬레이션은 기술된 시간들에 사용자 정의 액션들을 수행한다. 구체적인 시뮬레이션의 하위 클래스에 의해 정의되는 액션들은 모두 공통적인 타입을 공유한다.

1
type Action = () => Unit

이 문장은 빈 매개변수 리스트를 취해 Unit을 반환하는 프로시져 타입의 별칭을 Action으로 정의한다. ActionSimulation 클래스의 타입 멤버이다. 이를 () => Unit 타입의 좀더 읽기 쉬운 이름으로써 생각할 수 있다. 타입 멤버는 20.6에서 자세하게 설명될 것이다.

액션이 수행될 때의 시간은 시뮬레이션되는 시간으로 실제 “wall clock” 시간과는 아무런 관련이 없다. 시뮬레이션되는 시간은 단순히 정수형으로 표시한다. 현재 시뮬레이션되는 시간은 private 변수로 유지한다.

1
private var curtime : Int = 0

이 변수는 현재 시간을 반환하는 public 접근자 메소드를 갖고 있다.

1
def currentTime: Int = curtime

private 변수와 public 접근자의 조합은 Simluation 클래스 밖에서 현재 시간이 수정되지 않는 것을 보장하는데 사용된다. 결국에는 시뮬레이션 시간 여행을 모델링하는 경우를 제외하면, 보통 현재 시간을 관리하는 시뮬레이션 객체를 원하지 않는다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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) time :: ag
      else ag.head :: insert(ag.tail, item)
  }
      
  def afterDelay(delay: Int)(block: => Unit) {
      val item = WorkItem(currentItem + 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.isEmtpy)next()
  }
}

특정 시간에 실행되는 액션을 작업 항목(Work Item)이라고 부른다. 작업 항목은 다음의 클래스로 구현된다.

1
case class WorkItem(item: Int, action: Action)

WorkItem 클래스를 case 클래스로 만들었다. 이는 클래스의 인스턴스를 생성하는 WorkItem이란 factory 메소드를 사용할 수 있고, 생성자 매개변수 timeaction 억세스 하는 접근자를 제공하는 편리한 문법을 가져다준다.

Simulation 클래스는 아직 실행되지 않은 모든 남은 작업 항목의 목록을 지닌다. 작업 항목은 이들이 실행되는 시뮬레이션 시간에 의해 정렬된다.

1
private var agenda: List[WorkItem] = List()

agenda는 이를 갱신하는 insert 메소드에 의해 적당히 정렬된 순서로 유지된다. insert 메소드가 afterDelay에서 호출되는 것을 볼 수 있는데, 이는 agenda에 작업 목록을 추가하는 유일한 방법이다.

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

이름이 함축하는 것처럼, 이 메소드는 (block에 의해 주어진) 액션을 agenda에 추가함으로써 현재 시뮬레이션 시간 이후 delay 시간 단위에 실행하는 스케쥴을 잡게 된다. 예를 들면, 다음 호출은 currentTime + delay 시뮬레이션 시간에 실행되는 새 작업 목록을 생성한다.

1
afterDelay(delay) { count += 1}

실행되는 코드는 메소드의 두번째 인자에 포함되어 있다. 이 인자의 정식 매개변수는 “ => Unit” 타입이다. by-name 매개변수는 함수에 전달될 때 평가되지 않는다. 그래서 위의 호출에서는 count는 시뮬레이션 프레임워크가 작업 목록에 저장된 액션을 호출할 때만 증가된다. afterDelay는 커리 함수(curried function)임을 주목하자. 이는 9.5 섹션에서 소개된 커링(curring)이 메소드 호출을 좀 더 내장된 문법처럼 보이게 하는데 사용될 수 있다는 원리의 좋은 예이다.

생성된 작업 목록은 여전히 agenda에 입력될 필요가 있으며, insert 메소드에 의해 이뤄지는데, 이는 agenda가 시간순으로 정렬되는 불변성(invariant)을 지켜준다.

1
2
3
4
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)
}

Simulation 클래스의 핵심은 run 메소드에 의해 정의된다.

1
2
3
4
5
6
7
def run() {
  afterDelay(0) {
      println("*** simulation started, time = " + currentTime + """)
  }
      
  while(!agenda.isEmtpy) next()
}

이 메소드는 반복적으로 agenda의 첫번째 항목을 가져와, 이를 agenda에서 제거하여 실행되며 agenda에 더 이상의 항목이 없을 때까지 이뤄진다. 각 스텝은 next 메소드 호출에 의해 수행되고, 이는 다음과 같이 정의된다.

1
2
3
4
5
6
7
8
private def next() {
  (agenda: @unchecked) match {
      case item :: rest =>
          agenda = rest
          curtime = item.time
          item.action()
  }
}

next 메소드는 현재 목록을 패턴 매칭으로 앞 항목 item과 작업 항목의 남은 리스트 rest로 분할한다. 현재 목록에서 앞 항목을 제거하고 시뮬레이션 시간 curtime을 작업 항목의 시간으로 설정하여 작업 항목의 액션을 실행한다.

agenda가 비어 있지 않을 때만 next 가 호출될 수 있다는 것을 주목하자. 빈 리스트에 해당하는 case가 없으니 빈 agendanext를 실행하려고 하면 MatchError 예외를 얻게 된다.

Scala 컴파일러는 리스트의 가능한 패턴 중 하나를 놓친 것에 대해 경고한다.

Simulator.scala:19: warning: match is not exhaustive!
missing combination

    agenda match {
    ^

one warning found

이럴 경우 놓친 case는 문제가 되지 않는데, 왜냐하면 next가 오직 비어 있지 않은 agenda에 대해서만 호출될 것을 알고 있기 때문이다. 그러므로, 이 경고를 비활성화시키고 싶을 것이다. 이는 Simulation 코드가 agenda match가 아닌 (agenda: @unchecked) match를 사용하는 이유이다.

18.6 Circuit Simulation

다음 스텝은 섹션 18.4에 그려진 회로를 위한 DSL을 구현하기 위해 시뮬레이션 프레임워크를 사용하는 것이다. 회로 DSL은 배선 클래스와 AND 게이트, OR 게이트, 그리고 반가산기를 생성하는 메소드로 구성된다는 것을 상기하라. 이들은 시뮬레이션 프레임워크를 확장하는 BasicCircuitSimulation 클래스에 모두 포함된다. 다음이 이 클래스이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
package org.stairwaybook.simulation
  
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 addActions(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 addGate(al: Wire, a2: Wire, output: Wire) = {
      def andAction() = {
          val a1Sig = a1.getSignal
          val a2Sig = a2.getSignal
          afterDelay(AngGateDelay) {
              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
  }
}

실제 지연은 이 클래스 레벨에서는 알 수 없다. 왜냐하면 이들은 시뮬레이터되는 회로에 의존하기 때문이다. 그래서 구체적인 정의는 하위 클래스에 위임하도록 지연값들이 추상 BasicCircuitSimulation 클래스 내에 선언되었다.

The Wire class

배선 클래스는 다음의 3가지 기본 액션을 지원한다.

  • getSignal: Boolean 배선의 현재 신호를 반환한다.
  • setSignal(sig: Boolean): 배선의 신호를 sig에 설정한다.
  • addAction(p: Action): 주어진 프로시져 p를 배션의 액션에 첨부한다. 아이디어는 어떤 배선에 첨부된 모든 액션의 프로시저가 배선의 신호가 바뀌는 시간마다 실행되도록 하는 것이다. 즉, 첨부된 액션은 배선에 추가된 시간에 한번, 그 이후에는 배선의 신호가 변경될 때마다 실행된다.

두 개의 private 변수는 배선의 상태를 형성한다. sigVal 변수는 현재 신호와 actions 변수는 현재 배선에 첨부되는 액션의 프로시저를 나타낸다.

흥미로운 메소드의 구현은 setSignal이다. 배선의 신호가 변경되었을 때 새 값을 변수 sigVal에 저장하고 나아가 배선에 첨부된 모든 액션들이 실행된다. 이에 대한 단축 문법은 action foreach (_ ())으로 함수 _ ()action 리스트의 각 요소에 적용한다. 섹션 8.5에서 설명된 것처럼 함수 _ ()f => f()의 단축 표현으로 함수를 취해서 이를 빈 매개변수 리스트에 적용한다.

The inverter method

인버터를 생성한 결과는 액션이 자신의 입력 배선에 설치되는 것뿐이다. 이 액션은 설치되는 시점에 한번 그후에는 입력 신호가 변경될 때마다 매번 호출된다. 액션의 영향으로 인버터 입력값의 반전시킨 값이 출력값으로 설정된다. 인버터 게이트가 지연값을 가지기 때문에 이 변화값은 입력값이 바뀌어 액션이 실행돤 후 시뮬레이션 시간의 InverterDelay 만큼 영향을 받아야 한다.

1
2
3
4
5
6
7
8
9
def inverter(input: Wire, output: Wire) = {
  def invertAction() {
      val inputSig = input.getSignal
      afterDelay(InverterDelay) {
          output setSignal !inputSig
      }
  }
  input addAction invertAction
}

inverter 메소드의 영향으로 input 배선에 invertAction이 추가된다. 이 액션은 호출될 때, 입력 신호를 얻어와 시뮬레잉션 아젠다에 output 신호를 반전하는 다른 액션을 설치한다. 이 다른 액션은 시뮬레이션 시간의 InverterDay 만큼 후에 실행되는 것이다.

The andGate and orGate methods

AND 게이트 구현은 인버터 구현과 유사하다. AND 게이트 목적은 입력 신호의 합(conjunction) 을 출력하는 것이다. 이는 두 입력 중 하나의 변화 이후 시뮬레이트된 AndGateDelay 시간 단위에서 발생해야 한다. 따라서 다음과 같이 구현된다.

1
2
3
4
5
6
7
8
9
10
11
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
  a1 addAction andAction
}

andGate 메소드의 영향으로 addAction을 입력 배선 a1a2 모두에 추가된다. 이 액션은 호출될 때, 입력 시그널 모두를 얻어와 입력 시그널의 합에 대한 output 신호를 설정하는 다른 액션을 설치한다. 이 다른 액션은 시뮬레이트 시간 AndGateDelay 단위 이후에 실행된다. 입력 배선이 달라지면 출력이 재계산되어야 한다는 것을 주의하라. 그래서 두 입력 배선 a1a2 각각에 대해 andAction이 설치된다. orGate 메소드는 논리합 대신 논리곱을 수행하는 것을 제외하면 비슷하게 구현된다.

Simulation output

시뮬레이터를 실행하려면 배선의 신호 변화를 검사하는 방법이 필요하다. 이를 해내기 위해서 배선에 조사자(probe)를 두는 액션을 시뮬에이션할 수 있다.

1
2
3
4
5
6
7
def probe(name: String, wire: Wrie){
  def probeAction() {
      println(name + " " + currentTime + " new-value = " + wire.getSignal)
  }
      
  wire addAction probeAction
}

probe 프로시저의 영향으로 주어진 배선에 probeAction이 설치된다. 늘 하던대로, 설치된 액션은 배선 신호가 변화될 때마다 실행된다. 이 경우는 단순히 배선의 이름과 마찬가지로 현재 시뮬레이션 시간 그리고 배선의 새로운 값을 출력한다.

Running the simulator

시뮬레이터를 구동해볼 시간이다. 구체적인 시뮬레이션을 정의하기 위해 시뮬레이션 프레임워크를 상속할 필요가 있다. 관심있는 것을 보기 위해, BasicCircuitSimulation을 상속하는 추상 시뮬레이션 클래스를 생성하고 이번 장과 18.6장, 18.7장에서 살펴본 것처럼 반가산기와 전가산기를 위한 메소드 정의를 포함할 것인데, 이를 CircuitSimulation이라 부르며 이상 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package org.stairwaybook.simulation
  
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)
  }
}

구체적인 회로 시뮬레이션은 CircuitSimulation 클래스로부터 상속된 객체일 것이다. 그 객체는 시뮬레이트되는 회로 구현 기술에 따라 게이트 지연을 수정할 필요가 있다. 마지막으로, 시뮬레이션할 구체적인 회로를 정의할 필요가 있다. 이런 단계는 Scala 인터프리터로 대화식으로 행할 수 있다.

1
scala> import org.stairwaybook.simulation._

먼저, 게이트 지연이다. MySimulation이라 부르는 객체를 정의하고 어떤 숫자를 입력하자.

1
2
3
4
5
scala> object MySimulation extends CircuitSimulation {
  def InverterDelay = 1
  def AndGateDelay = 3
  def OrGateDelay = 5
}

MySimulation 객체 멤버를 반복적으로 억세스할 것이기 때문에, 객체의 import로 이후의 코드를 줄여쓴다.

scala> import MySimulation._

다음은 회로이다. 4개의 배선을 정의하고 그들 중 두 개에 조사자(probe)를 둔다.

scala> val input1, input2, sum, carry = new Wire
scala> probe("sum", sum)
sum 0 new-value = false
scala> probe("carry", carry)
carry 0 new-value = false

조사자(probe)가 즉시 출력을 찍었다는 것을 주목하라. 이는 배선에 설치된 모든 액션은 처음 액션을 설치했을 때 실행된다는 사실의 결과이다.

이제 배선을 연결하는 반가산기를 정의하자.

scala> halfAdder(input1, input2, sum, carry)

마지막으로, 하나씩, 두 입력 배선에 true 설정하고, 시뮬레이션을 실행하자.

scala> input1 setSignal true

scala> run()
*** simulation started, time = 0 ***
sum 8 new-value = true

scala> input2 setSignal true
scala> run()
*** simulation started, time = 8 ***
carry 11 new-value = true
sum 15 new-value = false

18.7 Conclusion

이 장에서는 가변 상태(mutable state)와 고차함수(higher-order function) 이질적으로 보이는 두 개의 기술을 함께 다뤘다. 가변 상태는 물리적 엔티티의 상태가 시간이 지남에 따라 변경되는 것을 시뮬레이션하는데 사용되었고 고차함수는 시뮬레이션 시간에 특정한 점에서 액션을 실행하는 시뮬레이션 프레임워크에서 사용되었다. (이하 생략)

Copyright © 2014 - Patrick Yoon. Powered by Octopress