-
Notifications
You must be signed in to change notification settings - Fork 21
lang 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, or can be annotated by the 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;const
classes are value types. -
enum
- aconst
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. While services in Ecstasy are often used to replace threads in lower level languages like Java or C++, 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#.
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, likeTrue
andFalse
for theBoolean
enumeration, are 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 a class is an object. But this is simpler than it sounds: In Ecstasy, anything that exists and you can do something with is an object. And since classes exist, and since you can do something with them, they must be 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 constantPI
that is defined within the classFPNumber
. - 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 name "HelloWorld
" identifies the singleton instance of the module) - The values of an enumeration:
Boolean value = True;
(the name "True
" identifies the singleton instance of the enum value)
- A module class:
- The name of a class can be used to indicate the corresponding type. In the example immediately above,
Boolean value = True;
, the class nameBoolean
is used to specify the type of the variable. We'll cover the concept of types in the next chapter.
This guide is meant as an introduction to the Ecstasy language, and not a definitive syntax reference. In other words, we want to convey 90% of the useful information that a definitive syntax reference would give you, while somehow using 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, and most of the time the assumptions should work. 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 -- modifierspublic
,private
,protected
, andstatic
. - 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 in Ecstasy it would be a bit strange and confusing to arbitrarily assign some weird naming convention for either one of them.
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
" type, or any other "primitive" types; all Ecstasy values are objects, including things as simple as bits, bytes, booleans, characters, and integers. And as we discussed earlier, those objects must all have classes from which they are instantiated. And since they're just normal classes, the names are written out in the same manner as other classes, capitalized in the same manner, and so on.
Moving on to mixins, here is an example declaration of a mixin, ListFreezer, whose job it is to add the ability to freeze (i.e. "make something immutable") to any List
:
mixin ListFreezer<Element extends Shareable>
into List<Element> + CopyableCollection<Element>
implements Freezable
A small clarification is needed: This mixin does not actually apply to just any List
. Specifically, it only applies to a List
class that has an Element
type that is Shareable
, and the into
clause specifies that the mixin can only be applied to a List
class that is also a CopyableCollection
. This is how a mixin defines its constraints on what classes 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
mixin 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 lazily 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 will then automatically redirects all of the calls coming into the ChickenOrEggMapping
, and it will send them to the newly hatched chicken
. In other words, the delegates
clause implements an entire interface by routing all of the interface methods to another object -- and it does all that in one simple and obvious 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. And by delegating to a @Lazy
property, the first such delegating call to come in, will automatically cause the chicken to be hatched from that egg!
Enumerations in Ecstasy are fairly straight-forward. The syntax is largely based on Java's enum
syntax, and is easy to read for common use cases; for example:
enum Event {Created, Modified, Deleted}
In this example, the Event
class is compiled as an @Abstract const
class, and the class object for the enumeration implements the Enumeration
interface, allowing easy programmatic access to the values of the enumeration, such as iterating through all of the enumeration's values:
for (Event event : Event.values) {
console.print($"event={event}");
}
Each enumeration value is a singleton const
class that extends the enumeration class and implements the Enum
interface; in the example, Created
is a singleton const
that extends Event
. This allows for simple and obvious usage of enumerations, such as:
Event event = Created;
And:
if (event != Deleted) {
process(event);
}
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 ofExample
. And yes, the nameExample.addOne
references an actual function object (of typeFunction<<Int>, <Int>>
). - The constant
Example.One
has a value unrelated to any instance ofExample
. - The typedef
Example.Text
is always aString
, and is not based on or related to any instance of theExample
class. - The const class
Example.Point
can be constructed without having an instance ofExample
, and it does not capture (i.e. it does not have) a reference to any instance of theExample
class.
Each of these items can be referenced directly without having an instance of Example
. This is an Example
-- no pun intended! -- of a class being used as a means of organization ("somewhere to put stuff"), and is not some highfalutin Object Oriented concept. In some ways, Example
is being used just like a package
is often used: It is an organizational container of other stuff. As we explained in a previous section, classes within a package can be pasted inside the body of the package code itself, just like the Point
above is found inside of Example
. In fact, it would be perfectly legal in Ecstasy to simply change class
to package
in the Example
code above!
You can see from this Example
that you can place these kinds of items within a class, and access them either from code within the class, or by specifying the class name. But these items are not part of the object instances of the class. So with this simple part of the topic out of the way, let's move on to the more interesting topic of the things that are related to actual instances of the class.
What we mean by instance-specific is simply that these contents can vary from instance to instance of a class, and that an instance of the class is required in order to access these contents. Let's start with a simple example:
const Point(Int x, Int y);
Breaking this down:
-
The form of the class is a
const
, which defines a class whose objects are immutable by the time that they complete construction. -
The name of the class is
Point
, likely referring to a Cartesian coordinate system. -
No explicit building blocks are used to define the class. However, all classes automatically implement Object, and all
const
classes automatically implement the Const interface, which is itself composed of Orderable, Hashable, Comparable, and Stringable. -
The declaration ends with
(Int x, Int y)
, which is short-hand notation for declaring two properties and defining an initializing constructor; the long-hand equivalent may be more familiar:const Point { Int x; Int y; construct(Int x, Int y) { this.x = x; this.y = y; } }
We'll cover properties in greater detail in a later chapter, but the important thing to keep in mind at this point is that each property on an object is itself an object that holds (or at least provides) a value; it is not a field. Let's add another property that measures the area of the region from the origin (0, 0)
to the point:
const Point(Int x, Int y) {
Int areaFromOrigin {
@Override
Int get() {
return x.abs() * y.abs();
}
}
}
This new property whose type is Int
and whose name is areaFromOrigin
looks a lot like a class inside of the Point
class -- and it is! Every read-only property automatically extends a class that implements the Ref
interface, and every read/write property automatically extends a class that implements the Var
interface, which itself extends the Ref
interface. While the class that implements read-only and read/write properties is unknowable, it must exist -- because properties exist, and they are objects, and objects come from classes. As with the Module
implementation code in the previous chapter, the linker is responsible for automatically providing an implementation of the Ref
and Var
interfaces for us.
The above example code is a bit too wordy, though. Fortunately, there is a short-hand syntax for defining a single method (in this case, get
) on a property:
const Point(Int x, Int y) {
Int areaFromOrigin.get() = x.abs() * y.abs();
}
For purposes of making an example, and without involving any cats or dogs (which seems to be a requirement for books focused on Object Oriented programming), let's introduce a sub-class that works in three-dimensional space. Before discussing the example, we do need to apologize for abusing sub-classing in this manner; if we didn't need an easy-to-explain example, we would never have done this:
const Point3D(Int x, Int y, Int z)
extends Point(x, y) {
@Override
Int areaFromOrigin.get() {
// technically this is volume, not area
return (x * y * z).abs();
}
}
There are a few important reasons for showing this example at this point:
-
To illustrate the use of the
@Override
annotation, which is required when overriding an instance member (e.g. method or property) of a class; -
To show how one constructor can delegate to another using the short-hand notation; and
-
To have an excuse to show the long-hand form of the constructor:
construct(Int x, Int y, Int z) { this.z = z; construct Point(x, y); }
There are various rules about what you can and cannot do from a constructor, and these rules are stricter in Ecstasy than in most languages. In an Ecstasy constructor, the object being instantiated does not yet exist; the this
that the constructor has access to is not a Point
object, but is a structure that will be used to instantiate the Point
object after the constructors have all finished their execution. The structure contains only the fields (the raw memory storage) for the Point, so the code this.z = z
in the constructor is not setting a property, but is simply storing the value in the structure's field in memory. Similarly, the constructor cannot call any of Point's methods on this
, because the Point
object doesn't yet exist during the constructor. We'll get into more details about constructors later on, but if you're playing around with code examples, these details will hopefully save you some frustration.
Methods in Ecstasy follow a syntax that is based closely on Java and C#. Continuing the Point
example from above, let's add two methods:
void print() {
@Inject Console console;
console.print($"point={this}");
}
Point double() = new Point(x*2, y*2);
If you're paying attention, we have now created a huge mess, because Point3D
will return a Point
from the double()
method. Or does it? There's a subtle detail here that needs to be explained: When the class Point
has a method that says it takes or returns a Point
, the compiler assumes that the method does not actually take or return an object of the exact class Point
, but rather the method takes or returns an object of the current class. So on Point3D, we need simply to add this implementation:
@Override
Point3D double() = new Point3D(x*2, y*2, z*2);
This behavior is called auto-narrowing: The Point
type when used in a declaration on the Point
class will be assumed to automatically narrow on sub-classes. If that assumed behavior is wrong, then the syntax to tell the compiler that we meant the exact Point
class (and not the auto-narrowing class) is to use the type Point!
, as in, "I really mean Point
!" And remember that the @Override
annotation on the method is required here; to avoid common programming errors, overriding a method in Ecstasy is a conscious and explicit decision. We'll cover these topics in more depth when we get to the virtual behavior and the virtual child class topics.
We've already seen some examples of type parameters, and even how they can affect the shape of the resulting class due to conditional incorporation. Type parameters in Ecstasy are similar to type parameters in C# and template parameters in C++, and the syntax is also very similar to type parameters in Java, although Java type parameters are erased (the type information only exists at compile time). The List
and Map
examples earlier in this chapter provide a sufficient overview of the similarities with other languages, so let's just look at some specific design considerations with Ecstasy type parameters:
- Each type parameter automatically creates a property of the same name. For example,
class Bag<Element extends Replicable>
means thatBag
will have anElement
property of typeType<Replicable>
that will contain the actual type provided to the type parameter. - Type parameters can be used both as "left hand side" and "right hand side" types. For example, continuing with the
class Bag<Element extends Replicable>
example above, it would be legal to write:Element e = new Element();
, even though the actual type ofElement
is not known by the compiler. (The terms "left hand side" and "right hand side" do not have a single definition, but they are often used to specify the use of a type or a variable either on the left hand or the right hand side of an assignment operation; the concepts are also related to the terms "l-value" and "r-value", referring to something that can be assigned to, and something that can be assigned from, respectively.) - Type parameters make new classes. As already explained,
List<Element>
is a class, and whileList<String>
is still aList
class, it actually defines a different class thanList<Element>
. The term "specializes" is sometimes used in languages with type parameters when the compiler automatically cut-and-pastes all of theList
code for you for every single differentElement
type; for example, "List<String>
is a specialization of theList
class." But that is not what we mean; it's not that the class is cut-and-pasted; it's just thatList<String>
is a separate, different, unique class. - Type parameters are shared by a class with its virtual child classes. Consider the interface Map<Key, Value> and its virtual child interface Entry: The
Entry
interface does not have its ownKey
andValue
type parameters, because its class exists contextually within aMap
class. This is a subtle but important difference: There is a classMap<Int, String>.Entry
, but there is no such class asMap.Entry<Int, String>
.
We just introduced the concept of a virtual child class with the simple and well-known Map.Entry
example, but the capability goes much deeper than the sharing of type parameters. While we won't go into a great amount of technical details at this point, virtual (i.e. non-static
) child classes are virtual in much the same manner that virtual methods are virtual.
Instead of going into depth on this topic here, we'll just give one example to whet your appetite:
module VirtualChildExample {
@Inject Console console;
void run() {
new SuperClass().doSomething();
new SubClass().doSomething();
}
class SuperClass {
void doSomething() {
new Child().printSomething();
}
class Child(String name="superclass") {
void printSomething() {
console.print($"{name} child");
}
}
}
class SubClass
extends SuperClass {
@Override
class Child(String name="subclass");
}
}
Look carefully at the code: Nowhere does it ever say new SubClass.Child()
. Yet when it's run, the output indicates otherwise:
superclass child
subclass child
The line of code that says "new Child()" instantiates the child class, virtually.
At this point, we've only covered a small fraction of the topic of how classes are defined, but it should be enough to provide context for the subsequent chapters. Now we're ready to introduce the Ecstasy type system.
Prev: Your first "Hello World!" | Next: Understanding types |
---|