서론 (시간 없으면 skip 하세요)

적어도 옵저버, 제네릭은 알아야 읽을 수 있습니다.

3년전 코딩을 처음 시작했을 때 C로 입력은 두값의 값을 + - * / 사칙연산 하는 프로그램을 만들고 계산기는 정말 쉬운 예제구나 생각했습니다. 그러나 친구중 한 명이 어셈블리 계산기를 만들었는데 진짜 윈도위의 계산기 비스무리한 걸 만들었다며 계산기도 제대로 만들면 어려운 거라고 했던게 기억납니다.

콜백으로 입력 받은 값을 그 자리에서 계산 해주고 식의 길이의 상관없이 만들려면 그 옛날에 했던 예제 보다는 어렵습니다.

요점은 뭔든지 깊게 파면 복잡하다. 입니다.

이번 스칼라 강의는 그 이상입니다. 소스는 짧지만 옵저버 패턴과 모나딕의 정수가 담겨있는데 아마도 마틴 오더스키 교수는 우리에게 스칼라 언어 자체의 설계 그 철학을 알려주려고 이런 무리한? 예제를 만드신 것 같습니다.

뭐하는 과제였는가

과제는 생둥맞은 3가지 문제를 푸는게 목적입니다.

  1. 트위터 글자수 확인
  2. 다항식
  3. 계산기

말만 들으면 거창해 보이지만 정말 별거 없이 Signal[T]의 모나딕한 클래스가 하나 있고 그 안에는 옵져버 패턴을 연상시키는 옵저버 객체가 내용이 있습니다.

class Signal[T](expr: => T) {
  import Signal._
  private var myExpr: () => T = _
  private var myValue: T = _
  private var observers: Set[Signal[_]] = Set()
  private var observed: List[Signal[_]] = Nil
  update(expr)
  protected def computeValue(): Unit = {
    for (sig <- observed)
      sig.observers -= this
    observed = Nil
    val newValue = caller.withValue(this)(myExpr())
    /* Disable the following "optimization" for the assignment, because we
     * want to be able to track the actual dependency graph in the tests.
     */
    //if (myValue != newValue) {
      myValue = newValue
      val obs = observers
      observers = Set()
      obs.foreach(_.computeValue())
    //}
  }

  protected def update(expr: => T): Unit = {
    myExpr = () => expr
    computeValue()
  }

  def apply() = {
    observers += caller.value
    assert(!caller.value.observers.contains(this), "cyclic signal definition")
    caller.value.observed ::= this
    myValue
  }
}

과제의 핵심 (풀이)

기본적으로 사칙연산에 관해선 이미 구현되어있습니다.

    sealed abstract class Expr
    final case class Literal(v: Double) extends Expr
    final case class Ref(name: String) extends Expr
    final case class Plus(a: Expr, b: Expr) extends Expr
    final case class Minus(a: Expr, b: Expr) extends Expr
    final case class Times(a: Expr, b: Expr) extends Expr
    final case class Divide(a: Expr, b: Expr) extends Expr

하스켈에서 타입클래스라고 부르는 방식이라고 기억합니다 다음 글을 참조하면 쉽게 코딩할 수 있습니다.

스칼라 강의 (47) ADT (Algebraic Data Types) 이란? (feat. 타입클래스) 출처: http://hamait.tistory.com/899?category=79134 [HAMA 블로그]

우리가 구현 해야되는 부분은 2가지입니다.

object Calculator {

        // 구현해야됨
      def computeValues(
          namedExpressions: Map[String, Signal[Expr]]): Map[String, Signal[Double]] = ???
    
        // 구현해야됨
      def eval(expr: Expr, references: Map[String, Signal[Expr]]): Double = ???
    
      /** Get the Expr for a referenced variables.
       *  If the variable is not known, returns a literal NaN.
       */
      private def getReferenceExpr(name: String,
          references: Map[String, Signal[Expr]]) = {
        references.get(name).fold[Expr] {
          Literal(Double.NaN)
        } { exprSignal =>
          exprSignal()
        }
      }
    }

eval은 evaluation 평가라는 뜻인데 스칼라 코세라 강의를 보면 주로 컴파일되면서 구문이 어떻게 해석된다는 뉘앙스로 많이 사용합니다. 실제계산을 해주는 부분입니다.

computeValus는 우리가 계산결과를 나타냅니다. computeValues는

      def computeValues(
          namedExpressions: Map[String, Signal[Expr]]): Map[String, Signal[Double]] =

Signal안에 들어있는 값을 꺼내서 순회 해야합니다. 그런 문법으로는 for과 flatmap있습니다. 어떤 문법을 쓰든간 제네레이터가 순회하면서 값을 꺼내 쓰는 건 똑같지만 for문이 더 보기 좋기에 예제에서는 주로 for 문을 사용하는 모습을 보실 수 있습니다.

//variable은 String
//expression은 Signal[Expr]
// eval 메소드는 Double을 돌려줌

def computeValues(
              namedExpressions: Map[String, Signal[Expr]]): Map[String, Signal[Double]] =
namedExpressions.flatMap((variable, expression) => {
    (variable -> Signal(eval(expression(), namedExpressions)))
})

이제 재귀함수를 사용하여 eval을 구현하면 됩니다. 깃허브의 다른 사람들의 소스를 찾아보면 다음과 같은 모습으로 패턴매칭을 한게 보이는 데 둘 다 상관은 없습니다.

// 다른 사람들이 올린 소스의 일부  case Plus(x) =>

//eval
  def eval(expr: Expr, references: Map[String, Signal[Expr]]): Double = {
    expr match {
      case x: Literal => x.v
      case Ref(name) => {
        val ref = getReferenceExpr(name, references)
        eval(ref, references - name)
      }
      case exp: Plus => eval(exp.a, references) + eval(exp.b, references)
      case exp: Minus => eval(exp.a, references) - eval(exp.b, references)
      case exp: Times => eval(exp.a, references) * eval(exp.b, references)
      case exp: Divide => eval(exp.a, references) / eval(exp.b, references)
    }
  }

소스

object Calculator {
  def computeValues(
      namedExpressions: Map[String, Signal[Expr]]): Map[String, Signal[Double]] = {
    for {
      (variable, expression) <- namedExpressions
    } yield { (variable -> Signal(eval(expression(), namedExpressions))) }
  }

  def eval(expr: Expr, references: Map[String, Signal[Expr]]): Double = {
    expr match {
      case x: Literal => x.v
      case Ref(name) => {
        val ref = getReferenceExpr(name, references)
        eval(ref, references - name)
      }
      case exp: Plus => eval(exp.a, references) + eval(exp.b, references)
      case exp: Minus => eval(exp.a, references) - eval(exp.b, references)
      case exp: Times => eval(exp.a, references) * eval(exp.b, references)
      case exp: Divide => eval(exp.a, references) / eval(exp.b, references)
    }
  }

  /** Get the Expr for a referenced variables.
   *  If the variable is not known, returns a literal NaN.
   */
  private def getReferenceExpr(name: String,
      references: Map[String, Signal[Expr]]) = {
    references.get(name).fold[Expr] {
      Literal(Double.NaN)
    } { exprSignal =>
      exprSignal()
    }
  }
}

후기

매우 간단한 글이고 이 과제를 푸는 사람이 아니라면 별 도움이 안 될겁니다. 그냥 저런 과제가 있네 봐주세요