Introducing Java’s Enumerated Type

J. Mohr University of Alberta, Augustana Campus, 4901 46 Ave., Camrose, Alberta, Canada T4V 2R3 [email protected]

Abstract— The enumerated types that were introduced in associativity or of operators with other than binary. Java 5.0 provide a way to treat operators as enu- Of the textbooks cited above, only Weiss [10] and Kruse meration constants with an method that is customized and Ryba [11] discuss right associativity (the right-to-left for each operator, allowing us to take an object-oriented evaluation of operators like exponentiation), and only the approach to applying an operator to its arguments, using latter textbook deals with both unary and binary operators. dynamic dispatch instead of case , when implementing a Kruse and Ryba are also unique in their use or an arithmetic expression evaluator. When used of an enumeration for token types, including enum in conjunction with variable arity methods (or varargs, constants such as , unaryop, binaryop, also introduced in Java 5.0) or by passing an operand rightunaryop, leftparen and rightparen.1 stack as the single argument to an operator’s eval method, The introduction of enumerated types in Java 5.0 (release an enumeration for operators can be extended to handle 1.5) provided the opportunity to treat arithmetic operators operators of differing using a single abstract eval as enumeration constants with an apply or eval method method in the enumerated type. Such an example gives that is customized for each operator. This allows us to an opportunity to expose our students to Java’s version of take an object-oriented approach to applying an operator enumerated types by using an Operator enumeration in to its arguments, using dynamic dispatch instead of case an assignment on infix expression parsing and evaluation. logic through constant-specific method implementations [12, p. 152]. This also provides a good opportunity to expose our Keywords: Enumerated types, enum, expression evaluation, Java, students to Java’s version of enumerated types in conjunction variable arity parameters with the discussion of arithmetic expression parsing and evaluation. 1. Introduction Arithmetic expression evaluation is a topic of study in 2. Enumerated Types many data structures or algorithms courses and is covered in several data structures and a few algorithms textbooks. In Niklaus Wirth introduced the enumerated type in Pascal some cases, the evaluation of postfix expressions is intro- (1970) as a user-defined ordinal type that “specifies an duced as part of the discussion of stacks [1]–[5]; in others, ordered set of values by enumerating the constant identifiers the evaluation of expression trees is discussed when binary which denote the values” [13, Ch. 5]. It associated the trees are introduced [1]–[3], [6]–[8]. Some textbooks present first constant listed with the value 0; the second, with 1; the subject of infix-to-postfix conversion in conjunction with and so on. It provided a high-level, application-oriented expression evaluation [3], [6], [9], [10]. Kruse and Ryba binding construct [14] that improved program readability and [11] dedicate a chapter (Ch. 13, pp. 594–645) to a case reduced errors by replacing ad hoc constructs such as study on , including the evaluation of prefix const and postfix expressions and the translation from infix form Mon = 0; to reverse Polish (postfix) form. Smith [5] shows how to Tue = 1; combine infix-to-postfix translation with postfix expression ... evaluation to evaluate infix expressions using two stacks; Sun = 6; Koffman and Wolfgang [6] present the same algorithm in an end-of-chapter programming project. with Most textbooks that treat the topic of arithmetic expression type Day = (Mon, Tue, Wed, Thu, Fri, evaluation confine their coverage to the evaluation of binary Sat, Sun); operators, which is especially appropriate if the topic is presented as part of the discussion of binary trees. Those that 1Lambert and Osborne [3] give an example of a parser that uses a discuss the conversion of infix expressions to their postfix switch statement with case labels such as PLUS, MINUS, MUL, DIV and L_PAR, but these are implemented as defined constants rather than equivalents usually include the handling of parentheses and enum constants, an ad hoc approach that will be discussed below as the operator priorities, but there is typically no discussion of int enum pattern [12]. The relational operators can be used to compare values the static methods valueOf(String) and values() of Pascal enumerated types, and the predefined functions are implicitly declared; the former returns the enumeration ord(X), pred(X), and succ(X) return the ordinal num- constant corresponding to a name, and the latter returns all ber, predecessor, and successor of X, respectively. A for the constants of the enumerated type [24, §8.9]. statement can iterate over the values of an enumeration (or Java enumerated types can have constructors, instance data a subrange of them), and enumeration constants can be used fields, and methods. The combination of a constructor, an as selectors in a case statement and to index arrays. instance field and an accessor can be used to associate values An important feature of Pascal’s enumerated types is that other than the default ordinals with enumeration constants, they are typesafe: each enumerated type is different from as illustrated in this example from the Java Language and incompatible with all other types, including integers and Specification, 3e [24, §8.9]: other enumerated types. The compiler can enforce strong public enum Coin { typing by ensuring that only the enumerated values of an PENNY(1), NICKEL(5), DIME(10), enumerated type can be assigned to a variable of that type QUARTER(25); or passed as a parameter to a function or procedure that Coin(int value) {this.value = value;} is defined to accept an argument of that enumerated type. private final int value; Pascal’s enumerated types are also secure: given the decla- public int value() {return value;} ration of type Day above, succ(Sun) and pred(Mon) } are compile-time errors. An enum type may not be declared abstract, but Subsequent languages introduced variations of the enu- it may include an abstract method if all the enumeration merated type. Several languages, including C++ and Ada, constants have class bodies that provide concrete imple- allow the ordinal values of enumeration constants to be mentations of the method. This feature can be used to specified, as in implement an enumeration of arithmetic operations [24, enum coins {penny = 1, nickel = 5, §8.9] [12, p. 152], as shown in Figure 1. This simple example dime = 10, quarter = 25}; deals only with binary operators, but Java’s enumerated type facility can easily be used to implement nullary, unary, Ada and C# allow enumerations to be overloaded (i.e., the and ternary operators as well as binary operators, and to same literal can be used in the declaration of more than one distinguish between left- and right-associative operators and enumeration in the same scope); C# requires that overloaded between prefix and postfix operators. names be fully qualified, but Ada allows the type prefix to be omitted if the compiler can infer it from the context [15]. 3. An Enumerated Type for Arithmetic Java did not include support for an enumeration type until release 1.5 (Java 5.0, 2004). Prior to its introduction, many Operators of Various Arities Java programmers used the ad hoc solution that is referred 3.1 A Simple Enumeration for Arities int enum pattern to as the [12]: The first step in supporting operators of various arities is public static final int MON = 0; to add an Arity enumerated type as a nested type within public static final int TUE = 1; an Operator enumeration: ... public enum Arity { public static final int SUN = 6; NULLARY, UNARY, BINARY, TERNARY, Several writers suggested ways of implementing typesafe GROUPING enumerations using Java classes [16]–[20]. In its “Taxonomy } of Problems in Teaching Java” [21], the ACM Java Task Implementing an Operator enumeration is like design- Force identified the lack of enumerated types as a “weakness ing a little language [25]: we can decide what operations in the Java type system.” can be included in arithmetic expressions that use our The enum type introduced in Java 5.0 shifted the concept Operator class to process them. The Arity enumeration of an enumerated type away from a set of integer constants above allows us to support unary operators such as factorial toward the concept of classes that is central to object- (‘!’) and percent (‘%’) and single-argument functions (such oriented programming [22, p. 112]. A Java enum type is as abs, sqrt, sqr, trunc, round, log, lg, ln, sin, a class of which “the possible values of the enumeration cos, and exp or pow); binary operators such as exponenti- are the only possible instances of the class” [23, p. 257]. ation (‘ˆ’) and modulus (mod) in to the usual ‘+’, All enum types are implicitly subclasses of the predefined ‘-’, ‘*’, and ‘/’; and ternary operators such as C and Java’s class Enum, from which they inherit toString, equals, conditional operator ‘?:’ and variations on that operator compareTo, hashCode, ordinal (which returns the such as pos, neg, and zero. zero-based position of the constant in its enum declaration), The NULLARY arity is provided for pseudo-operators such and a few other methods. In addition, for each enum type, as constants (e and pi) and no-argument functions such public enum { PLUS { double apply(double x, double y) {return x + y;} }, MINUS { double apply(double x, double y) {return x - y;} }, TIMES { double apply(double x, double y) {return x * y;} }, DIVIDE { double apply(double x, double y) {return x / y;} };

abstract double apply(double x, double y); } Fig. 1: An enumeration with an abstract method. as rand. Strictly speaking, constants such as e and pi are The body of each eval function would include a switch , not operators, but in the context of an expression on the value of the enumeration constant, such as: evaluator, they could be considered as no-argument functions switch (this) { that return a result, so I include them as “operators” for the case PERCENT: convenience it provides for writing a parser for arithmetic return arg / 100.0; expressions: all non-numeric tokens can be processed by case ABSOLUTE_VALUE: calling the eval function on the Operator represented return Math.abs(arg); by that token. ... The GROUPING arity constant was added to designate } ( ) symbols such as parentheses ‘ ’ and ‘ ’ and the ternary Each eval function should begin with a test of the arity : operator separation symbol ‘ ’. It is an error to attempt of the enumeration constant on which the eval function to evaluate these symbols, and providing a separate arity was called, to ensure that the proper number of arguments category for them facilitates our handling of that error. have been passed, and throwing an exception if not (perhaps 3.2 A Constructor for Operator Enumera- a customized OperatorException). tions While this approach is workable, it is contrary to one of the fundamental ideas of object-oriented programming: Operators have other attributes in addition to arity, such as the replacement of case logic by polymorphism. Instead of priority, associativity, and location (prefix, postfix, or infix). using conditional logic based on the type of an object, we use Associativity and location can be supported with addi- subclasses (or, as in the case of our Operator enumeration, tional nested enumerated types: different enumeration constants) to create objects whose public enum Associativity { behavior varies based on their type. LEFT, RIGHT, NA A common way to implement polymorphic behavior is } to define an abstract method in the base class of a type public enum Location { hierarchy (or in the enumeration type) so that each subclass PREFIX, INFIX, POSTFIX, (or each enumeration constant) has to provide a concrete im- PRE_OR_POSTFIX, NA plementation of that method. In the case of our Operator } enumeration that includes operators of various arities, the Neither associativity nor location are applicable to nullary problem is that we don’t want to have to declare four 2 pseudo-operators or to grouping constructs . Some operators different abstract eval methods, one for each possible arity, (such as ‘++’ and ‘--’) can appear in either prefix or postfix because then each enumeration constant will have to provide locations. concrete implementations of all four (three of which will With the definitions of these enumerations and some throw an exception to indicate that an incorrect number of attributes, the constructor for the Operator enumerated arguments was provided, given the arity of the operator). type could be implemented as shown in Figure 2. How do we declare a single abstract method for the top- 3.3 Eliminating Case Logic level enumerated type when different enumeration constants require different numbers of arguments? One way to handle operators of differing arities would be Two solutions to this problem come to mind. The first uses to define multiple eval functions, each accepting a different another feature that was added to Java in release 1.5: support number of arguments, as illustrated in Figure 3. for variable arity methods, or varargs [12, p. 197–200]. 2Although the ‘:’ of the ternary operators is located between the second By combining this feature with an abstract eval method in and third operands, and is thus strictly speaking an infix symbol, it is not a Java enumeration, it is quite easy to support the evaluation a true operator, but a separator provided for the convenience of the parser, so the concept of location is not applicable in the same sense as it is for of operators of various arities in an object-oriented way. operators that can be evaluated. The second solution is to pass a reference to a collection private final String symbol; private final int precedence; private final Arity arity; private final Associativity associativity; private final Location location;

private Operator(String sym, int prec, Arity ar, Associativity assoc, Location loc) { symbol = sym; precedence = prec; arity = ar; associativity = assoc; location = loc; } Fig. 2: The constructor for an Operator enumeration.

public double eval() { ... } public double eval(double arg) { ... } public double eval(double lhs, double rhs) { ... } public double eval(double test, double ifTrue, double ifFalse) { ... } Fig. 3: Multiple eval methods for operators of differing arities. that contains the arguments to which the operator is to We could make implementation of the eval method be applied, and to make it a responsibility of the eval in each enumeration constant more concise by delegating method to retrieve the correct number of arguments from to the Arity enumerated type the task of checking that the collection (typically a stack). the number of arguments passed matches the arity of the operator. To do this, we add the following attributes and 3.3.1 An eval function using varargs methods to the body of the Arity enumeration: Using Java’s varargs facility, the case logic of the private final int numArgsExpected; example above can be avoided by providing a constant- private Arity(int numArgs) { specific implementation of the eval function in the body numArgsExpected = numArgs; of each enumeration constant. } abstract void checkArgs(int numArgs, First, an abstract eval method is specified in the body String sym) of the enum Operator: throws OperatorException; abstract double eval(double... args) Each Arity constant then implements the abstract throws OperatorException; method checkArgs. The test that the number of ar- Because the method accepts a variable number of arguments, guments supplied matches the arity of the constant is a single abstract eval method suffices for operators of the same for each arity, but the message passed to the differing arities. OperatorException constructor is customized for each Then each enumeration constant must implement the enumeration constant. For example, Figure 5 shows the eval function, checking that it has been passed the correct implementation of the checkArgs method for the UNARY number of arguments. A nullary Operator enumeration constant of the Arity enumeration. constant might be implemented as shown in Figure 4. The implementation of the eval function in each enu- The implementation of a unary operator, such as SQUARE meration constant can then be simplified by replacing the (represented by the symbol sqr), would be similar, but if statement that compares args.length to the expected would check that args.length is 1 and would return a number of arguments with the following method call: result such as args[0] * args[0]. A binary operator arity().checkArgs(args.length, such as MULTIPLY would return args[0] * args[1], toString()); and a ternary operator such as NEGATIVE (represented by the symbol neg and the separator ‘:’) would return the 3.3.2 Passing an Operand Stack to eval value of A postfix expression evaluator or an infix expression (args[0] < 0.0) ? args[1] : args[2]. parser typically uses a stack to keep track of the operands RANDOM("rand", 7, Arity.NULLARY, Associativity.NA, Location.NA) { @Override double eval(double... args) throws OperatorException { if (args.length != 0) { throw new OperatorException("Pseudo-operator '" + symbol + "' accepts no arguments."); } return Math.random(); } } Fig. 4: Implementation of a nullary Operator enumeration constant.

UNARY(1) { @Override void checkArgs(int numArgsProvided, String symbol) throws OperatorException { if (numArgsProvided != arityValue()) { throw new OperatorException("Operator '" + symbol + "' requires a single operand."); } } } Fig. 5: Implementation of the checkArgs method in an Arity constant. that have been seen but not yet passed to an operator or argument popped is the last one that was pushed. If we have that have been produced by evaluation of a subexpression. more than a few binary or ternary Operator constants, we Instead of passing individual operands as parameters of the could factor out the repeated code by defining an abstract eval method, we could pass the operand stack3 as the sole getArgs method in the Arity enumeration: argument of eval. To implement this method of passing operands to an abstract double[] getArgs Operator enumeration constant, we declare eval as an (Deque valStack); abstract method of the Operator enumeration, accepting a stack4 argument: We then have to marshall the correct number of arguments only once for each arity. Figure 7 shows a simple method abstract double eval of doing so, using the implementation of getArgs in (Deque valStack) the BINARY enumeration constant as an example. This throws OperatorException; implementation puts the arguments in the correct order in A concrete implementation of the eval method must an array of doubles, the array being the same size as the then be provided in the constructor of each constant of the arity. Operator enumeration. Figure 6 shows how eval might Figure 8 shows how the use of Arity.getArgs() be implemented for the unary round operator, represented makes the code for the eval method of a binary Operator by the ROUND enumeration constant. quite concise; the implementation of eval for a ternary While the implementation of eval for a unary operator operator is equally simple. as shown in Figure 6 is quite simple, it is a little more complicated to pop the required number of arguments off However, this simplicity is somewhat deceptive, because the stack for a binary or a ternary operator because we also the implementation shown does not check that the stack have to be careful to use them in the correct order: the first actually contains at least as many operands as required by the operator, given its arity. If the user of the Operator 3 A postfix expression evaluator typically uses a single stack for both class did not provide enough arguments for the evaluation of operands and operators, while an infix expression evaluator typically uses separate stacks for operands and operators. a specified operator, the Deque.pop() method will throw 4The method parameter is declared using the Deque interface because a NoSuchElementException; the client class should the Java Stack class has been deprecated; instead, use of the Deque meth- catch this exception and take the appropriate action, such ods that conform to the stack protocol for accessing a deque (push, pop, peek) is recommended. The concrete class ArrayDeque implements a as informing a user that the infix expression they entered deque using a circular array. contained a syntax error (namely, insufficient arguments). ROUND("round", 6, Arity.UNARY, Associativity.RIGHT, Location.PREFIX) { @Override double eval(Deque valStack) throws OperatorException { return Math.rint(valStack.pop()); } } Fig. 6: Implementation of eval for a unary operator.

BINARY(2) { @Override double[] getArgs(Deque valStack) { double[] result = new double[2]; result[1] = valStack.pop(); result[0] = valStack.pop(); return result; } } Fig. 7: Implementation of getArgs for the BINARY arity.

DIVIDE("/", 5, Arity.BINARY, Associativity.LEFT, Location.INFIX) { @Override double eval(Deque valStack) throws OperatorException { double[] args = arity().getArgs(valStack); return args[0] / args[1]; } } Fig. 8: Implementation of eval for a binary operator.

4. Teaching Arithmetic Expression Eval- expression evaluator because it helps them to understand uation how expressions that they write in their programs will be evaluated. For example, when asked about the order in which It would probably be unreasonable to expect a first-year an expression will be evaluated, students will commonly computing science student to implement an Operator enu- say that parenthesized subexpressions will be evaluated merated type as described above. However, I have success- first. Writing or studying an infix expression evaluator will fully used an Operator enumeration in an assignment on quickly show that this is a misconception. For example, infix expression evaluation in an introductory data structures given the expression 2∗3+(4−5), a typical infix expression course by providing the source code for enum Operator evaluator will evaluate 2 ∗ 3 first, pushing the result to an to the students and showing them how to use it to apply operand stack. operators to operands. Their assignment was to implement an This assignment also acquainted the students with infix expression parser and evaluator that correctly handles enumerated types in general and with the enhanced operators of various arities and associativities. I provided the features of the Java enumerated type facility through students with pseudocode for an algorithm to evaluate infix studying the enum code that was provided and learn- expressions that use only left-associative binary operators; ing to call the public methods of the enum such the challenge of the assignment was to implement the as isUnary(), isLeftParend(), precedence(), algorithm in Java and to revise it to correctly handle right- isRightAssociative(), and eval(). associative operators such as exponentiation (‘ˆ’) and unary Such an example would also be useful in a programming and ternary operators. Such revisions are fairly easily made languages course to illustrate the difference between tradi- when some thought is given to the problem, and students tional enum facilities in languages such as Pascal, Ada, and usually have to think through the problem because they may C++ and the more object-oriented approach taken by the have difficulty finding sample code on the internet for arith- designers of the enumerated type facility introduced in Java metic expression evaluators that handle right associativity 5.0. This also provides a good opportunity to contrast the and multiple arities. use of case logic with dynamic dispatch and polymorphism. It is valuable for students to write (or revise) an infix Several variations on this assignment might also provide good learning opportunities. For example, an infix expres- focus primarily on the algorithms for parsing and evaluating sion evaluator could also be designed as the combination arithmetic expressions using a stack. Such an example could of two other applications: an infix-to-postfix converter and also be used in an algorithms course to introduce infix- a postfix expression evaluator. This is the approach taken to-postfix conversion or postfix expression evaluation, or in Kruse and Ryba’s [11] and Koffman and Wolfgang’s [6] in a programming languages course to contrast the object- case studies. oriented nature of Java’s enumerated type with the enum Goodrich and Tamassia [8, Exercise C-7.15] suggest that facilities of other languages or to illustrate the generation of infix-to-postfix conversion can be done by converting an code for expression evaluation. infix expression to its equivalent binary tree representa- tion using an algorithm that they provide to construct References an expression tree from a fully parenthesized arithmetic [1] D. A. Buell, Data Structures Using Java™. Burlington, MA: Jones expression. The equivalent postfix expression can then be & Bartlett Learning, 2013. [2] B. R. Preiss, Data Structures and Algorithms with Object-Oriented generated by a postorder traversal of the expression tree. Design Patterns in Java™. New York: Wiley, 2000. Lambert and Osborne [3] present this method in greater [3] K. A. Lambert and M. Osborne, Java™: A Framework for Program detail. Preiss [2] takes the opposite approach, constructing Design and Data Structures. Pacific Grove, CA: Brooks/Cole, 2000. [4] N. Dale, D. T. Joyce, and C. Weems, Object-Oriented Data Structures an expression tree from a postfix expression, then emitting Using Java, 2nd ed. Sudbury, MA: Jones & Bartlett, 2006. the corresponding infix expression. Drozdek [7] provides [5] P. Smith, Applied Data Structures with C++. Sudbury, MA: Jones pseudocode for generating assembly code or intermediate & Bartlett, 2004. [6] E. B. Koffman and P. A. Wolfgang, Objects, Abstraction, Data code in a compiler from an expression tree; such an exercise Structures and Design Using Java™. New York: Wiley, 2005. might be relevant in a programming languages course as well [7] A. Drozdek, Data Structures and Algorithms in Java™. Pacific as a data structures course. Expression trees are typically Grove, CA: Brooks/Cole, 2001. [8] M. T. Goodrich and R. Tamassia, Data Structures and Algorithms in discussed in connection with binary trees, so the arithmetic Java™, 5th ed. New York: Wiley, 2005. expressions featured in such examples are restricted to using [9] R. Sedgewick and M. Schidlowsky, Algorithms in Java, Parts 1-4: binary operators. Fundamentals, Data Structures, Sorting, Searching, 3rd ed. Boston: Addison-Wesley, 2003. Students could also be asked to add operators to the [10] M. A. Weiss, Data Structures and Problem Solving Using C++, Operator enumeration; this would require them to think 2nd ed. Reading, MA: Addison-Wesley, 1999. about the relative priority and the associativity of the opera- [11] R. L. Kruse and A. J. Ryba, Data Structures and Program Design in C++. Upper Saddle River, NJ: Prentice Hall, 1999. tors to be implemented, in order to ensure that the order [12] J. Bloch, Effective Java™, 2nd ed. Upper Saddle River, NJ: Addison- of execution of expressions including the new operators Wesley, 2008. corresponds to the expected or specified behavior of the [13] K. Jensen and N. Wirth, “Enumerated and subrange types,” in Pascal User Manual and Report. New York: Springer, 1991, pp. 50–54. operators. [Online]. Available: http://dx.doi.org/10.1007/978-1-4612-4450-9_6 Kruse and Ryba [11] also discuss the running-sum condi- [14] B. J. MacLennan, Principles of Programming Languages, 3rd ed. tion for properly formed postfix expressions; students could New York: Oxford University Press, 1999. [15] M. L. Scott, Pragmatics, 3rd ed. Burlington, be asked to implement this check when parsing postfix MA: Morgan Kaufmann, 2009. expressions. [16] J. Bloch, Effective Java™Programming Language Guide. Boston: Addison-Wesley, 2001. [17] P. A. Cairns, “Enumerated types in Java,” Software—Practice and 5. Conclusions Experience, vol. 29, no. 3, pp. 291–297, 1999. Java enumerated types can be used to implement a fully [18] E. Armstrong, “Create enumerated constants in Java,” JavaWorld, Jul. 1997. [Online]. Available: http://www.javaworld.com/article/2076970 featured object-oriented Operator class that facilitates the [19] P. Bishop, “Java tip 27: Typesafe constants in C++ and Java,” use of operators of varying arities, precedences, associativity, JavaWorld, Mar. 1997. [Online]. Available: http://www.javaworld. and locations of use. com/article/2077524 [20] T. E. Davis, “Use constant types for safer and cleaner code,” It is possible to specify a single abstract eval method JavaWorld, Sep. 1999. [Online]. Available: http://www.javaworld. in such an enumeration that is concretely implemented in com/article/2076481 enumeration constants that represent operators of differing [21] ACM Java Task Force, “Taxonomy of problems in teaching Java,” Feb. 2004, unpublished draft. [Online]. Available: http://www-cs- arities, rather than using case logic, either by using the faculty.stanford.edu/~eroberts/java/java-problem-taxonomy varargs facility or by passing an operand stack as an [22] A. B. Tucker and R. E. Noonan, Programming Languages: Principles argument to the eval function and having each operator and Paradigms, 2nd ed. New York: McGraw-Hill, 2007. [23] R. W. Sebesta, Concepts of Programming Languages, 10th ed. (or the Arity member of each Operator enumeration Boston: Addison-Wesley, 2012. constant) pop the appropriate number of arguments off the [24] The Java Language Specification, 3rd ed., Sun Microsystems, Inc., stack. 2005. [Online]. Available: http://docs.oracle.com/javase/specs/jls/se5. 0/html/j3TOC.html The use of such a class in a data structures course exposes [25] J. Bentley, “Little languages,” Communications of the ACM, vol. 29, the students to enumerated types while allowing them to no. 8, pp. 711–21, Aug. 1986.