Skip to content
Cameron Purdy edited this page Jun 28, 2022 · 31 revisions

Understanding Classes

We've already seen a few classes; for example, the HelloWorld module is a class. The first thing to understand about classes is this: Classes exist to help organize things, just like files and directories help to organize a hard drive. So, before we even attempt to define what a class is, it would be helpful to imagine classes in your mind in much the same way you imagine files and directories on your hard drive. Sometimes, the hard drive organization makes sense: Good file names, obvious file types, and easily examinable file contents. (Of course, for most people, there are big parts of the hard drive that look like a huge and unintelligible mess!)

Nonetheless, all of your information is stored in files, and those files are organized into directories -- because that is how operating systems store things. Similarly, in an class-based Object Oriented language like Ecstasy, everything is part of a class because that is how a class-based Object Oriented language organizes things. Classes are not mystical magical beings; they exist first and foremost as simple organizational tools.

Continuing the analogy, a module is like the root directory on a hard drive, and a package is like a sub-directory. Both modules and packages are classes, but they also provide the high-level organization that is conceptually similar to the directory structure on a hard drive. In addition to module and package, there are six other keywords that Ecstasy uses to define classes:

  • interface - a form of an abstract class that describes an application programming interface (API); as such, it is a reusable building block for other classes. The meaning of this keyword in Ecstasy is largely consistent with its meaning in Java and C#.
  • mixin - a class that can be mixed into another class; a class can incorporate a mixin. A mixin is a re-usable building block that can be added to other classes -- even classes that weren't designed to have such functionality added to them.
  • const - a specialized form of a class that is immutable by the time it completes construction.
  • enum - a const class that defines a list of enumerated values. The meaning of this keyword in Ecstasy is largely consistent with its meaning in Java and C#.
  • service - a class that defines a (i) new domain of mutability, which is (ii) asynchronous, (iii) concurrent, and (iv) has an explicitly manageable lifecycle. The closest analog for a service is a Web Worker in JavaScript, or a Process in Erlang.
  • class - the catch-all used to define any class that isn't covered by the previous keywords. The meaning of this keyword in Ecstasy is largely consistent with its meaning in C++, Java, and C#.

Objects versus Classes

Classes are the blueprints for objects.

If an object exists, it exists because there was a blueprint that was used to create it. As in Java and C#, if an object of some class C exists, then it probably exists because some code that contains the expression "new C()" was executed. There are other ways to create objects, but for now, let's keep it simple, and just pretend that they were all created using new.

A blueprint for an object is called the object's class. And new-ing an object is called instantiation, as in:

  • "... the object was instantiated from the class"
  • "instantiating the object ..."
  • "... a new instance of the class"
  • "creating an instance of the class ..."
  • etc.

A concrete class is a set of blueprints that is complete enough to be used to instantiate an object. An abstract class is a set of blueprints that lacks sufficient information on its own to instantiate an object. But don't think of an abstract class as a failure, like not finishing your homework! Instead, think about an abstract class as some useful piece of design that you can use in a more complete design; in other words, abstract classes are reusable building blocks.

There are a few different reasons why a class may be abstract:

  • A class that is declared with the interface keyword is always abstract.
  • The class representing an entire enumeration, such as Boolean, is always abstract. (The enumeration values, like True and False, are singleton const concrete sub-classes of the enumeration.)
  • A mixin is abstract, because it needs to be mixed into another class in order to form an instantiable class.
  • A class can be annotated with the @Abstract annotation in order to make it explicitly abstract.
  • Instead of producing a compilation error, the compiler may mark a class as abstract if the class is missing some necessary design information, but no one is attempting to instantiate it.

There are two truisms related to objects and classes:

  • All objects are instantiated from concrete classes; if an object exists, it is an instance of a concrete class.
  • Everything is an object -- even classes! But don't let this confuse you: In Ecstasy, anything that exists and you can do something with is an object. Since classes exist, and you can do something with them, they are objects.

And finally, there are only a few ways that classes are used:

  • Classes are used for organization. That means that the name of a class can be used to tell the compiler where to look for another name. For example, the compiler understands that FPNumber.PI refers to the named value PI that is defined within the class FPNumber.
  • Classes are used to instantiate objects, such as in the code: return new User(id);.
  • Classes are actual objects, such as the name Boolean in the code: Class boolclass = Boolean;.
  • A class with a singleton value makes that value available via the name of the class. Two obvious examples that we have already discussed are a module class, Module m = HelloWorld;, and the values of an enumeration: Boolean value = True;.
  • The name of a class can be used to indicate the corresponding type. In the example immediately above, Boolean value = True;, the class name Boolean is used to specify the type of a variable. We'll cover the concept of types in the next chapter.

Composing Classes: How to build a blueprint

This guide is meant as an introduction to the Ecstasy language, and not a definitive syntax reference. In other words, we want to somehow convey 95% of the useful information that a definitive syntax reference would give you, while somehow spewing 95% fewer words and unveiling 95% less complexity. To help us get there, we've already explained that many of the syntax choices in Ecstasy were purposefully based on Java and C#; we hope that developers can rely on their existing Java and C# knowledge to assume the correct Ecstasy syntax, when in doubt. But there are certainly some syntactic differences.

A class is defined by (i) its form, (ii) its name, (iii) its building blocks, and (iv) its contents. The form of the class includes the keyword (class, const, enum, etc.) used to define it, as well as the optional -- and rarely-used -- modifiers public, private, protected, and static. The name of the class is a simple (one part) name; the only exception is the name of a module, which may be qualified like an Internet domain name, such as MyApp.mycompany.com. The building blocks may include annotations, a super class, a list of implemented and/or delegated interfaces, and incorporated mixins. The contents of a class may include: type parameters, constructors, methods, functions, properties, constants, typedefs, and classes.

Let's take a look at how building blocks are used. We'll start with the well-known class, HashMap, from the Ecstasy core library:

class HashMap<Key extends Hashable, Value>
        extends HasherMap<Key, Value>
        implements Replicable
        incorporates CopyableMap.ReplicableCopier<Key, Value>

Everything here would seem quite at home in either Java (HashMap<K,V>) or C# (Dictionary<TKey,TValue>), except for the incorporates clause. While designing mixins is not a beginner-level topic, you can think of the incorporates clause as being similar to the extends clause in some ways: extends specifies a named super-class that this class inherits from, while incorporates specifies a mixin that has functionality that we want to assimilate in a borg-like manner.

Stylistically, CamelCase is used for class and type names, and type parameters are named using their natural names (instead of a first letter thereof), and without an extra "T" glued to the front.

Interfaces are defined similarly to Java's syntax, including the use of the extends keyword instead of implements (or C#'s :) when listing super-interfaces; here is the List interface:

interface List<Element>
        extends Collection<Element>
        extends UniformIndexed<Int, Element>
        extends Sliceable<Int>

In addition to the items already covered, one thing immediately stands out compared to Java and C#: In Ecstasy, Int is a class, and as such, it is capitalized with CamelCase just like every other type and class name. Ecstasy does not have an "int" or any other "primitive" types; all Ecstasy values are objects, including things as simple as bits, bytes, booleans, characters, and integers, and those objects all have classes from which they are instantiated.

Here is an example declaration of a mixin, ListFreezer, whose job it is to add the ability to freeze to any List:

mixin ListFreezer<Element extends Shareable>
        into List<Element> + CopyableCollection<Element>
        implements Freezable

Correction: Not any List. The Element type of the List must be Shareable, and the into clause specifies that the mixin can only be applied to a List that is also a CopyableCollection; this is how a mixin defines its constraints on what it can be plugged into. As you can see, though, the mixin is itself a form of class, specifying that it implements the Freezable interface, for example. When the mixin is incorporated into a List, that List will then automatically pick up those additional traits, such as implementing the Freezable interface. For now, don't focus on how this happens; we'll discuss those details in a subsequent chapter.

Here's another mixin, ListMapIndex. Again, it specifies that it can only be mixed into a ListMap, and more specifically, into one whose Key type is Hashable:

mixin ListMapIndex<Key extends Hashable, Value>
        into ListMap<Key, Value>

Let's look at where it is used, by the class ListMap:

class ListMap<Key, Value>
        implements Map<Key, Value>
        implements Replicable
        incorporates CopyableMap.ReplicableCopier<Key, Value>
        incorporates conditional ListMapIndex<Key extends immutable Hashable, Value>
        incorporates conditional MapFreezer<Key extends immutable Object, Value extends Shareable>

This is, by far, the most complicated example that we have examined. Note that ListMap does not require that its Key be Hashable, but yet it incorporates ListMapIndex, which as we just noted requires its Key to be Hashable! Let's examine that incorporates line closely:

incorporates conditional ListMapIndex<Key extends immutable Hashable, Value>

Note the conditional keyword: ListMap is defining a blueprint that says: "If my Key type is an immutable Hashable type, then (and only then) automatically incorporate the ListMapIndex into the class." Pause for a moment to think about what is happening here: It is as if the class is altered based on the type of Key that the ListMap is instantiated with!

There is one more way to specify a building block, which is the delegates clause, seen here in the const ChickenOrEggMapping class, shown here in full:

const ChickenOrEggMapping<Serializable>(function Mapping<Serializable>() egg)
        delegates Mapping<Serializable>(chicken)
    {
    private function Mapping<Serializable>() egg;

    private @Lazy Mapping<Serializable> chicken.calc()
        {
        return egg();
        }
    }

The "egg" function is provided as part of the constructor, and then the "chicken" is created the first time that it is needed, by calling the "egg" function and caching the resulting "chicken". What the delegates clause does, though, is incredible: It then automatically redirects all of the calls coming to the ChickenOrEggMapping, and sends them to the newly hatched chicken. The delegates clause implements an interface by routing all of the interface methods to another object -- and it does all that in one simple-to-read and -understand line of code.

Class Contents

As stated above, the contents of a class are: type parameters, constructors, methods, functions, properties, constants, typedefs, and classes. Let's split these into two groups:

class-level
(static, no this required)
object instance-specific
(this required)
type parameters
constructors
functions methods
constants properties
typedefs* typedefs*
static child classes virtual child classes

There's a lot to explain here, and there's no simple way to explain it. Let's start by illustrating the "no this required" column:

class Example
    {
    static Int addOne(Int n)
        {
        return n+One;
        }
        
    static Int One = 1;
    
    typedef String as Text;
    
    static const Point(Int x, Int y);
    }

Notice that each one of these is unrelated to its containing class:

  • The function addOne operates only on the value passed to it, and it can be called without having an instance of Example.
  • The constant One has a value unrelated to any instance of Example, and it can be obtained without having an instance of Example.
  • The typedef Text is always a String, and is not based on an instance of Example.
  • The const class Point can be constructed without having an instance of Example.

TODO

Prev: Your first "Hello World!" Next: Understanding types

Clone this wiki locally