trait Expr[A] {
// reminiscent of factory methods
def +(other: Expr[A]): Expr[A] = Plus(this, other)
def *(other: Expr[A]): Expr[A] = Mult(this, other)
}
case class Plus[A](left: Expr[A], right: Expr[A]) extends Expr[A]
case class Mult[A](left: Expr[A], right: Expr[A]) extends Expr[A]
case class Atom[A](v: A) extends Expr[A]
case class Var[A](s: String) extends Expr[A]
/*
we need to implement a new type objects, which tells us how we should
interpret + and *, depending on who A is.
*/
case class Strategy[A](atom:A => A, plus: (A,A) => A, mult: (A,A) => A)
object Today extends App{
/* More polymorphism
*
* a little piece of code which might be related to interpreters or
* compilers.
*
* We would like to define expressions such as:
* Plus(1,Mult(2,3)) vs x + (2 * 3)
* and we would like to evaluate such expressions
* input: "1 + (2 * 3)", we would like to skip parsing
* e.leftSide
* e.rightSide
* */
/*
1 + 2 * 3
+
/ \
1 *
/ \
2 3
*
/ \
+ 3
/ \
1 2
*/
val e = Plus(Atom(1), Mult(Atom(2), Atom(3)))
type Store[A] = Map[String, A]
def eval[A](store: Store[A])(str: Strategy[A])(e: Expr[A]): Option[A] = {
def aux(e: Expr[A]): Option[A] =
e match {
case Plus(e1, e2) =>
(aux(e1), aux(e2)) match {
case (Some(x), Some(y)) => Some(str.plus(x, y))
case _ => None
}
case Mult(e1, e2) =>
(aux(e1), aux(e2)) match {
case (Some(x), Some(y)) => Some(str.mult(x, y))
case _ => None
}
case Atom(v) => Some(str.atom(v))
case Var(s) => store.get(s)
// something important happens here! (error handling)
// What do we do, if the store does not contain s?
// We do not know -> we need to "pass on" an error
}
aux(e)
}
/* Without modifying functionality, what could we improve (in terms
of usability) with this code?
1. We can improve the legibility of expression constructors.
We want to define "+" and "*" and use them INFIX
e1 + e2 translates to e1.+(e2)
*/
val e1 = Var("x") + Atom(2) * Atom(3)
//1 + 2 * 3 // Scala integer expression
/*
2.
What would be a natural extension for expressions?
What are expressions missing? (Div is an option)
We would like to include variables!
3. If we include variables, we need to include error-handling. Done
4. What if atoms could be ANYTHING?
- now, atoms are integers
- atoms could be: booleans, lists, functions
* */
val strInt = Strategy[Int](x => x, _ + _, _ * _)
val strBool = Strategy[Boolean](x => x, _ || _, _ && _)
println(eval(Map("x" -> 1))(strInt)(e1))
//val strList =
}