Implementing Jedi Values and the ALU

Here is a detailed design of a segment of the Value hierarchy:

Addables & Numerics

I've introduced two new value traits: Addable (things that can be combined using +) and Numeric (things that can be combined using +, *, -, / and can be negated).

-> 3 * 2.5
7.5
-> -67
-67
-> 5 / 3
1
-> 5.0 / 3
1.6666666666666667
-> "bat" + "man"
batman
-> "2 + 2 = " + 4
2 + 2 = 4
-> true + false
Inputs to + must be addable

Orderables

All of the concrete classes (Boole, Chars, Exact, and Inexact) are value classes in the sense that they need overrides of equals, toString, and hashCode.

Instances of Chars, Exact, and Inexact can be compared to other values, throwing exceptions if the other values don't make sense:

-> "abc" < "def"
true
-> "abc" == "abc"
true
-> 23 < 3.14
false
-> 2.9 < 100
true
-> 0 < true
Arguments must be comparable

Adding Strings to Things

Jedi strings are called Chars. (I decided not to have a Char type in Jedi.) An instance of Chars encapsulates the Scala string it represents. Adding Chars means appending or concatenation:

-> "abc" + "def"
abcdef
-> "abc" + 100
abc100
-> "abc" + 3.14
abc3.14
-> "abc" + false
abcfalse

Numerics

Exact and Inexact are Jedi's version of integers and floating-point numbers, respectively. Exacts encapsulate the Scala Int they represent, and Inexacts encapsulate the Scala Double they represent. They are numeric and ordered (comparable). Notice that exacts automatically coerce (i.e. convert) themselves into inexacts when they appear in a context where an inexact was expected:

-> 3 + 5
8
-> 3 + 5.0
8.0
-> 3.0 + 5
8.0
-> 3 / 5
0
-> 3.0 / 5
0.6
-> 3.0 + 5.0
8.0

Exact Numbers

Let's look at the Exact class in detail: Exact.scala

Notes:

1.     Not shown in the diagram above, is that all of the concrete classes extend the Literal trait from the expression package.

2.     The implementations of +, -, *, /, and compare are similar in that they take an arbitrary value as input, then throw a type exception if the input isn't numeric. Putting error checking in the value classes greatly simplifies error checking in the ALU.

3.     In case the input is inexact, then the implicit exact is automatically coerced into an inexact:

case x: Inexact => Inexact(this.value.toDouble + x.value)

4.     Division throws a divide-by-zero exception and yields integer division if its argument is exact, but otherwise yields floating point division.

Exceptions

Add JediExceptions.scala to the context package. Four types of programmer errors are caught by Jedi:

-> x
Undefined identifier: x   // undefined exception
-> 2 + true
Numeric operand required  // type exception
-> 3 / 0                 
Divide by 0!              // illegal value exception
-> 3 +                    // syntax exception
context.SyntaxException: Syntax error
end of input expected
line # = 1
column # = 3
token = +
->

(I've commented out syntax exceptions for now as they require the parser library to compile.)

The ALU

Every programming language needs a few built-in functions to get programmers started. In machine language these operations are implemented in hardware by a processor component called the arithmetic and logic unit or ALU. Accordingly, we need to provide an ALU for Jedi. Copy alu.scala into your context package and complete the code.

The alu is a singleton. Its execute method is a dispatcher method. It examines the opcode, then dispatches control to the appropriate private helper method. (Think of a taxi dispatcher that receives a call for a ride, then dispatches the closest free cab.) Notice that the other input to the execute method is a list of arguments of type Value; the return type is also Value. Also notice that the helper methods expect their argument lists to have different sizes. Some want one (unary), some want two (binary) and some want at least two (n-ary) arguments.

For example, the function call:

-> add(2, 3, 4.0)
9.0

triggers a call to:

alu.execute(Identifier("add"), List(Exact(2), Exact(3), Inexact(4.0)))

which in turn calls the following sequence:

alu.add(List(Exact(2), Exact(3), Inexact(4.0))
(Exact(2) + Exact(3)) + InExact(4.0)
Exact((2 + 3)) + InExact(4.0)
Inexact(5.0 + 4.0)
Inexact(9.0).

The provided code shows two approaches: righteous and simple. The add method uses a match-case expression to determine if the first argument, arg(0), is of type Addable. If so, it calls a tail-recursive helper method that left-reduces the argument list using whatever + method values of type Addable must have. Here we see the advantages of working with abstract traits like Addable. Instead of an explicit dispatch like alu.execute, we are relying on an implicit dispatch provided by polymorphism. This style is sometimes referred to as Data-Driven Programming: let the data decide what code to execute instead of the programmer having to write explicit dispatch methods that need to be updated each time new types are added to the system. We will need to modify alu execute each time new functions are added to Jedi, but we won't need to update alu.add. For now, chars, exacts, and inexacts can be added using their respective + methods. Because the + methods throw exceptions if any of their arguments are not of type Addable, the alu.add method doesn't need to do this check.

The less helper method is called when values are compared using <, for example:

-> "abc" < "def"
true
-> 3 < 2
false
-> 3 < 2.0
false

Executing the last expression triggers the call:

alu.execute(Identifier("less"), List(Exact(3), InExact(2.0)))

Which triggers the calls:

alu.less(List(Exact(3), InExact(2.0)))

Exact(3) < Inexact(2.0)

Exact(3).compare(Inexact(2.0))

Inexact(3.0).compare(Inexact(2.0))

3.0 < 2.0  // in Scala now

Boole(false)

This method expects exactly two arguments, and the first one must be of type Ordered[Value]. This is determined using the isInstanceOf method instead of match-case. In general, something can have type T, but not necessarily be an instance of T. Using isInstanceOf might limit the effectiveness of polymorphism, which is why using math-case is better but also a bit more complicated. Also, making Exact extend Ordered[Value] rather than Ordered[Exact] or Ordered[Numeric] means that the input to compare can be any Value, which means checking for un-ordered or non-comparable inputs can be done by compare rather than alu.less. Finally, notice how the computation above gradually reduces 3 < 2.0 in Jedi, our object-language, to 3.0 < 2.0 in Scala, our meta-language. Of course this returns the Scala Boolean false which then needs to be converted to the Jedi Boole Boole(false).

Testing

Here's a test harness for your value classes: testValues.scala