Add handling of lists to SL1.scala. Support lists of the form x :: y :: z :: Nil and list functions head, tail, and isEmpty. We will call them as functions, not methods. For example, evaluating head(1 :: Nil) yields 1.
Define a case class
case class Cons(a: Expr, b: Expr) extends Expr
Parsing :: isn't hard—add a right-associative operator that binds more strongly than * or / and yields a Cons.
Evaluating Cons is a little icky. We want eval(Cons(a, b), symbols) to be the Scala list eval(a, symbols) :: eval(b, symbols). But eval has return type Any. Apply a cast like this:
eval(a, symbols) :: eval(b, symbols).asInstanceOf[List[Any]]
Add four entries to the initial symbol table. Bind "Nil" to an empty Scala list and head, tail, isEmpty to instances of
case class ListOp(f: List[Any] => Any)
For example, isEmpty should be bound to ListOp(l => if (l.isEmpty) 1 else 0)
Then define what it means to evaluate a ListOp. First figure out where it is evaluated. (Hint: head(x) looks like a function call fac(n), except when looking up head in the symbol table, you get a ListOp, whereas a function like fac is a Closure.) Once you have found the right spot, you just need to add a case ListOp(f) => ..., evaluating the function on the first argument. You will again need a cast to List[Any].
Do not worry about handling incorrect programs. If someone calls head(x, y) or head(1) or head(), let it crash. This won't be tested.
To show that your list operations work, provide a file revlist.sl1 in which you define a function reverse that reverses a list, like in lecture 3, followed by a call reverse(1 :: 2 :: 3 :: 4 :: 5 :: Nil). (It doesn't have to be tail recursive, and you don't have to use a Y combinator. But you can. If you do, remember to curry append or reverseHelper—the Y combinator produces functions with one argument.)