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:
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
.
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 extend
s it must
provide a definition of the method.
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 A∧B (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.
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, Int
s 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]