서론 (시간 없으면 skip 하세요)
적어도 옵저버, 제네릭은 알아야 읽을 수 있습니다.
3년전 코딩을 처음 시작했을 때 C로 입력은 두값의 값을 + - * /
사칙연산 하는 프로그램을 만들고
계산기는 정말 쉬운 예제구나 생각했습니다. 그러나 친구중 한 명이 어셈블리 계산기를 만들었는데
진짜 윈도위의 계산기 비스무리한 걸 만들었다며 계산기도 제대로 만들면 어려운 거라고 했던게 기억납니다.
콜백으로 입력 받은 값을 그 자리에서 계산 해주고 식의 길이의 상관없이 만들려면 그 옛날에 했던 예제 보다는 어렵습니다.
요점은 뭔든지 깊게 파면 복잡하다.
입니다.
이번 스칼라 강의는 그 이상입니다. 소스는 짧지만 옵저버 패턴과 모나딕
의 정수가 담겨있는데 아마도 마틴 오더스키 교수는 우리에게
스칼라 언어 자체의 설계 그 철학을 알려주려고 이런 무리한? 예제를 만드신 것 같습니다.
뭐하는 과제였는가
과제는 생둥맞은 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()
}
}
}
후기
매우 간단한 글이고 이 과제를 푸는 사람이 아니라면 별 도움이 안 될겁니다. 그냥 저런 과제가 있네 봐주세요