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