CS470/570: A Brief Sketch of Object-Oriented Programming in Scala

Let's start from the beginning.

An object is, conceptually, a bundle of two things: some data and a descriptor. However, it can only be accessed in certain ways, as specified by the descriptor.bundle

An object's descriptor is a realization of its class definition as internal data structures. (All elements of the class share a pointer to exactly the same descriptor.) The class definition specifies the object's data and their types, how the data are initialized (constructed), how the data are accessed, various methods for doing things with the object, and what other classes this one inherits from. The class definition is basically a template for every object of the class, i.e., with that descriptor. The fields, methods, etc. are called members of the class. If none of this rings a bell, you've probably just forgotten the object-oriented features of your favorite language, because all languages have them, even C, if you think of C++ as the object-oriented extension of C. Find a book and review it!

Scala is unusual in a few ways:

  1. Objects can be defined on their own. The resulting definitions look just like class definitions, except the "class" in question has just one element.element
  2. Everything in a class definition is part of the description of objects of that class. If there are entities associated with the class in some other way, their declarations and definitions should be in the class's companion object declaration, which has the same name as the class. (If there are no such entities, there needn't be any companion object.) For example, if you have a class Fribble whose elements are sometimes built by reading data from a file, it makes no sense to put the method that does this file reading in the class, because it could only be called if you already had an element, and the element built by x.buildFromFile(…) would bear no relation to x. But buildFromFile goes nicely into the companion object, where it can be referred to as Fribble.buildFromFile.
  3. A Scala trait is a like a class except that it (a) has no val or var members; and (b) may have method declarations with no definition, which is like an ordinary method definition except everything starting with the "=" sign is missing. A trait with method declarations is a promissory note: any class the extends it must provide a definition of the method.
  4. Classes and traits can extend as many traits as they please, but at most one class. This architecture keeps the slots as tidy as those of Java classes, but allows the same method to be defined in multiple supertraits. We won't be exploiting this feature. But you do have to define all the methods declared but not defined in a supertrait.
  5. Scala has case classes, which are useful for defining algebraic datatypes. These are datatypes defined by (typically) recursive specs, such as If A and B are WFFs, then so is AB (and a bunch of other similar statements). So you might have a WFF such as P ∨ (Q ∧ R). If this is represented as an object, then Q ∧ R is a member of it, and you can obviously build more deeply nested structures easily. This kind of nesting has nothing to do with any class hierarchy. Here we have an "∧" occurring as a member of an "∨" object, but that doesn't mean there's an andWFF class that's a subclass of an orWFF class, or anything of the sort. Instead you have several classes, all on the same level:

      /* Warning: This is plausible Java, but not the Scala way */
      class WFF ...
      class primWFF extends WFF ...
      class andWFF extends WFF ...
      class orWFF extends WFF ...
      class notWFF extends WFF ...
    

    There are other, possibly better ways, of organizing these classes; but we don't care, because our target is the way you would typically handle this situation is Scala:

      trait WFF ...
      case class primWFF(name: String) extends WFF ...
      case class andWFF(lft: WFF, rgt: WFF) extends WFF ...
      case class orWFF(lft: WFF, rgt: WFF)  extends WFF ...
      case class notWFF(arg: WFF) extends WFF ...
    

    That's it: a shallow hierarchy, just one trait with a few extending subclasses. A trait is a collection with members of many types and roles, but the main thing here is that it (may) contain some methods that use the following device. But such methods can be placed anywhere convenient.

    The device is to decode a value of type WFF by using a match expression:

       w match {
         case primWFF(n) => ...
         case andWFF(a, b) => ...
         case orWFF(a, b) => ...
         case notWFF(a) => ...
       }
    

    A case class is a transparent record. Its constructor takes the arguments shown, and each one becomes a publicly accessible (but not settable) slot in the record. The class can be constructed just by writing ClassName(args); you don't have to write new in front of this expression.no-new Not surprisingly, a case class can be used in a case expression, several of which occur inside the entity that forms the right argument of match.case It's not a good idea to try to give a case class any extra slots or methods. It's not a good idea to have a class hierarchy with more than one level below a trait like WFF.

    This is basically grafting algebraic data types onto Java-style classes. If that's what you (a language designer) gotta do, then Scala's approach is as good a way of doing it as any.


Endnotes

Note bundle
C and C++ hackers: You can think of an object as a pointer to a data/descriptor bundle, but you can't get at the bundle, only the pointer, no matter how hard you try. There are no tricks to convert an object reference to a value of type void*. By the way, the compiler is free to optimize this conceptual picture considerably, although it's constrained by the fact that the target machine is the Java Virtual Machine. On the other hand, Ints in Java are primitive data, and one hopes that at least in some cases Scala succeeds in viewing an Int as a simple 32-bit quantity. [Back]

Note element
An element of a class is an object described by the class. A member of a class is a val, var, method, or some other entity defined at the top level of the class. There's an ambiguity here. Sometimes members are thought of as "compile-time" entities such as var declarations or method definitions. Sometimes they are thought of as "run-time" entities such as the value or storage location of a slot, or (perhaps) the functional entity (including free-variable bindings) corresponding to a method. The "compile-time" sense should go by some other label, such as "facet," or perhaps "meta-element," to emphasize that members are pieces of the description of a class's elements. In the "run-time" sense, one object is (or can be) a member of another object, but it's an element of its class, the abstract entity the object's descriptor is derived from. [Back]

Note no-new
The reason you don't need new is that the companion object ClassName has an apply method. Any object with an apply method can be used as function. Any object at all. Play around with that idea; what other classes might make good use of an apply method? [Back]

Note case
Once again, the machinery for making case work is nice and transparent. All you have to do to be able to write "case f(…) => …" is provide an unapply method for f. Just as apply takes some arguments and returns an object, the unapply method for f takes an object and returns the arguments that, when f is applied to them, yield the object. This method is defined automatically in a case class, but you can define it "manually" for other objects. (Because f is not, in general, invertible, it doesn't return the arguments, but an Option(T1, … TN) object, where Ti is the type of the i'th parameter, so it can return None when there are no arguments that produce the required value.) [Back]