In the last example we saw our first interesting class declarations. There's a lot more to them than we'll cover in this intro.
SABLE is less restrictive than other languages, allowing methods (of certain kinds) on interfaces, delegates, and enumerations. They can declare static and instance macros, static methods, and staticImpl methods (instance methods which are implemented statically). This allows creating a rich vocabulary for those types, allowing them to be used more clearly and succinctly.
Certain classes can declare various data members. They begin with a pragma message specifying a data member style to apply to the following declarations.
| literals | Introduces named literals. They must have certain built-in types as required by the platform (e.g. numbers, chars, strings) and must be initialized. Arbitrary reference types are allowed, but only with the value Nil. When a literal is used, its value is inserted in the code, not a data member reference. |
| constants | Creates static members whose values are initialized in the class declaration or assigned in a static initializer method. The data member cannot be assigned elsewhere. |
| statics | Refers to modifiable static fields. |
| durables | The declarations are instance fields initialized in the class declaration, assigned in an instance initializer, or set in a constructor. They cannot be assigned elsewhere. |
| fields | Creates modifiable instance fields. |
An enumeration can only declare public literals, so no data member pragma is needed to introduce them.
"Program to interfaces," we're told. Yet interfaces are made second-class citizens, marked with a scarlet letter "I" and limited to public abstract methods. Use them as method argument types, but don't think about using them with constructors.
SABLE elevates interfaces to first-class status. In our naming conventions, they bear the name of a concept to be embodied, and a concrete class which usually implements it gets a modified name. In the collections classes for example, DICTIONARY is the interface, and DICTIONARY_IMPL is its typical concrete implementation. Now, method signatures can use DICTIONARY, but we still need to worry about DICTIONARY_IMPL when creating instances. Right?
This is the declaration of System.Collections.Generic.DICTIONARY[?]:
DICTIONARY[TKey,TValue] {~ interface parent: COLLECTION[KEY_VALUE_PAIR[TKey,TValue]]} {~ implementationClass: DICTIONARY_IMPL[?,?]} {~ cilName: 'System.Collections.Generic.IDictionary`2<TKey,TValue>'} {@ DEFAULT_MEMBER: 'Item'}
Interfaces and abstract classes can specify an >#implementationClass:, a concrete implementation to create when they're used in a constructor context. Our SUBSTITUTION_TESTER uses this, in fact.
test. |mapping| := {DICTIONARY[STRING,STRING]} new at: 'TITLE' put: 'Dr.'; "..." yourself.
Did you notice that we were "constructing an interface"? If not, that's good; SABLE makes coding to interfaces easy and natural. This actually invokes a constructor declared in the implementation class, DICTIONARY_IMPL. However, the type of the constructor expression, and therefore the type inferred for mapping, is the interface type shown. SABLE eliminates redundant repetition in most initializers. Compare these:
// C#Dictionary<string,Employee> emplList1 = new Dictionary<string,Employee>(); IDictionary<string,Employee> emplList2 = new Dictionary<string,Employee>(); "SABLE"|emplList1| := {DICTIONARY_IMPL [STRING, EMPLOYEE]} new. |emplList2| := {DICTIONARY [STRING, EMPLOYEE]} new.
(LIST and LIST_IMPL follow the same pattern. Ideally, you shouldn't need to reference these IMPL classes directly. In practice, you may find that you have to use them because the functionality you need isn't in the interface. In my opinion, this is a defect in library design; the admonition "program to interfaces" implies that the interfaces must be feature-rich enough to be usable. A counterexample is the core library's omission of IList<T>.AddRange().)
Delegate classes are declared in the same way as other classes, where the delegate's Invoke method is abstract. As noted above, the delegate can have additional methods; you might use this to establish default values for some arguments, or to encapsulate common usage patterns.
/-#SystemEVENT_HANDLER {~ delegate} {~ cilName: 'System.EventHandler'} {@ SERIALIZABLE; COM_VISIBLE} =-'invoke'invokeFrom: sender {OBJECT} eventArgs: args {EVENT_ARGS?}. {~ abstract} invokeFrom: sender {OBJECT}. {~ macro} "Invoke the delegate with Nil event args." Me invokeFrom: sender eventArgs: Nil.
The signature of a delegate is that of its abstract method. One way to create a delegate instance is to reference a method with a matching signature. The other is to use a delegate block.
|aHandler| {EVENT_HANDLER}. aHandler := #eventFrom:args:. "Use a static method from the current class" aHandler := form >~> #eventFrom:args:. "Use an instance method on :form" aHandler := #[:sender :args | "..."].
A delegate block will generate an appropriate method depending on whether it accessess any instance fields, any method locals outside of itself, or whether it contains a return from the method where it appears. (The later two cases can have negative implications on performance.)
Now we can invoke an event handler delegate as follows:
aHandler invokeFrom: Me eventArgs: eventArgs. aHandler invokeFrom: Me.
While we're looking at delegate blocks, note that they can even receive messages. This shows how to spawn a thread with a PARAMETERIZED_THREAD_START, without ever mentioning that class:
#[:id | THREAD sleep: 500. CONSOLE writeLine: 'Thread {0} exiting' with: id. ] forkWith: 2585 boxed.