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 hierarchical organization that is 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 a set of properties and methods; as such, it is a reusable building block for building other classes. The term "interface" here has much the same meaning as when it is used in the term application programming interface (API). The use of this keyword in Ecstasy is consistent with its use 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 use of this keyword in Ecstasy is consistent with its use 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 use of this keyword in Ecstasy is consistent with its use 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 Person exists, then it probably exists because some code that contains the expression "new Person()" was executed. There are other ways to create objects, but for now, let's keep it simple, and assume 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 ..."

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. On the other hand, the actual values of the enumeration, like True and False for the Boolean enumeration, concrete classes; specifically, enumeration values are singleton const classes, each of which is a sub-class of the enumeration class.
  • 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 code is being compiled that actually attempts 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 themselves are used:

  • Classes are used to instantiate objects, such as in the code: return new User(id);.
  • 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 constant PI that is defined within the class FPNumber.
  • Classes are actual objects, such as the name Boolean in the code: Class boolclass = Boolean;.
  • A singleton class makes its singleton instance available via the name of the class. Here are two obvious examples that we have already discussed:
    • A module class: Module m = HelloWorld;
    • 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 the 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 90% of the useful information that a definitive syntax reference would give you, while somehow spewing 90% fewer words and unveiling 90% 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 of course, there are some syntactic differences.

So how is a class defined?

  • The class' form 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 class' name is a simple (one part) name; the exception to this rule is the name for a module, which may be qualified like an Internet domain name, such as MyApp.mycompany.com.
  • The class' building blocks may include annotations, a super class, a list of implemented and/or delegated interfaces, and incorporated mixins.
  • The class' contents 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 use natural names like "Key", instead of a first letter thereof, and they do not have an extra "T" glued to the start of their name. There's a very important point hidden here, so let's make it obvious: There is no conceptual difference in Ecstasy between the type of Key and the type of Int. That's why they are written out in the same manner, capitalized in the same manner, and so on. They are both named types, and to pretend otherwise would be strange and confusing.

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 well-known List interface, for example:

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. To communicate this to the reader of the code, we write out and capitalize Int and Boolean, just as we did with Key.

Here is an example declaration of a mixin, ListFreezer, whose job it is to add the ability to freeze (i.e. "make it immutable") 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 incorporated into. As you can see, though, the mixin is itself a form of class, specifying that it implements the Freezable interface, for example. So when the mixin is incorporated into a List, the resulting List class will automatically pick up those additional building blocks, 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, which is used to add a fast-lookup index to a ListMap. Look at its into clause and its type parameter declaration: 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! So let's examine that incorporates line closely:

incorporates conditional ListMapIndex<Key extends immutable Hashable, Value>

Note the addition of 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 my class." Pause for a moment to think about what is happening here: It is as if the class is being automatically 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. By delegating all of the Mapping calls to the chicken, the ChickenOrEggMapping acts just like a chicken, even though it was constructed with only an egg!

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, which is the simple part of this topic:

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 items is unrelated to its containing class, other than happening to exist syntactically within it:

  • The function addOne operates only on the value passed to it, and it can be called without having an instance of Example. And in case you are wondering: Yes, the name addOne references an actual function object.
  • The constant One has a value unrelated to any instance of Example.
  • The typedef Text is always a String, and is not based on or related to any instance of the Example class.
  • The const class Point can be constructed without having an instance of Example, and it does not have a reference to any instance of the Example class.

Each of these items can be referenced directly without having an instance of Example. These are examples of the Example class being used as a means of organization ("somewhere to put stuff"), and not some highfalutin Object Oriented concept. In some ways, Example is being used just like a package is often used: It is just an organizational container of other stuff. As we covered previously, classes within a package can be pasted inside the body of the package code itself, just like Point is located inside of Example, and the relationship is very similar. In fact, it would be perfectly legal in Ecstasy to simply change class to package in the Example code above.

TODO

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

Clone this wiki locally