Thursday, 16 February 2012

Basic Libretto Constructs (1/5). Classes

A class is a template that determines the structure and behavior of objects of a certain type, which have common features. Such objects are called the instances of this class. The Libretto type system is based on multiple inheritance: a subclass can inherit from several superclasses.


Class Definition

A class definition contains

  • the class name 
  • the calls of superclass constructors, from which this class directly inherits. 
  • the class constructor definition, which introduces 
    • the fields of the class 
    • the methods of the class 

 A class definition in Libretto can be accompanied with factory methods – special functions used for creating instances in various situations and contexts (see section Constructors and Factory Methods).

This is a class definition example:
class Colored(var color: String)

class Circle(fix radius: Real) {
  def square = 3.14 * radius * radius
}

class ColoredCircle(c, r) extends Colored(c), Circle(r) {
  def repaint(newColor) {color = newColor}
}
The class of colored circles is built via multiple inheritance from superclasses Colored and Circle. The parameters are passed to superclass constructors via local variables c and r. The class ColoredCircle inherits two fields (mutable color and immutable radius) and the method square, which calculates the square of the circle. The ColoredCircle constructor also contains the definition of a method for changing the circle color.

In this class definition, the following keywords are used:

  • class is the class declaration keyword 
  • extends is the superclass declaration keyword 
  • def is the method/function definition keyword 
  • fix and var are the field/variable declaration keywords 

All components of a class definition can be omitted except the keyword class and the class name. This is an example of a minimal definition: class Person

Naming

The naming system of Libretto has the following structure. The first group of names of Libretto entities (classes, objects, fields, functions and packages) consists of words, which start with a letter (any character declared as a letter by Unicode) and contains letters, digits and underscore symbols “_”. Here are examples:
HelloWorld  дом115  γεια_κόσμος
Although not forbidden, it is strongly recommended not to include underscores in user-defined names, because they are used to name some predefined Libretto constructs, e.g. special functions.

The naming convention of Libretto recommends to use PascalCasing for class names and camelCasing for function, field and variable names. There is no convention for object names.

The second group of names consists of sequences of
-   :  =  .  >  <  ~  +  *  @  $  #  ^  |  & 
and other characters including mathematical ones, e.g.
->   --   >=  ###  +=  :-  ≥   ≈©  ≠   ¬  ø  ¢  ∞
The keywords of Libretto are:
as        asc       break    class     continue
def       desc      else     extends   fix
if        index     import   object    override
package   private   return   this      var
The back quotes are applied, when we need to use keywords (e.g. class) or arbitrary symbol sequences as names, e.g. `class` or `this is a name`.

Class Constructors and Fields

Fields associated with a class describe the properties of its instances:
class Person(fix name: String, var residence: String)
The expression following the keyword class is called a constructor. The instances of the class Person will contain two fields: immutable name and mutable residence. Fields can be of two types: immutable (‘fixed’) and mutable (variable). The value of an immutable field (marked by fix) is determined at initialization and can not change. The value of a mutable field (marked by var) can change at any time. The type of a field can be optionally declared separated by the semicolon.

There are two ways of class instance creation: declaratively
object JOHN extends Person(“John”, “New York”)
and dynamically
{
  var person = Person(“Manfred”, “Kiel”)
}
Libretto objects and their handling are considered in more detail in section Fields.

The last example is enclosed in curly brackets, because person is a local variable. Local variables can be defined only in local environments like blocks or function bodies. To emphasize this all the following examples with occurrences of local variables will be shown as blocks.

Fields (class variables) and local variables are distinguished by the place they occur in the code (similarly to Java). Global variables (on the level of packages) are not allowed in Libretto, because they are the notorious source of vulnerabilities and errors. Instead of them Libretto offers much safer constructs working with global data, such as external fields (see section External Fields).

The dot operator performs an access to a field value (see section Sequences and the Dot Operator). Let us define, for instance,
class Cls(fix a: Int, var b)
object obj extends Cls(1, 1)
Now:
obj.b = 2
obj.b + 2  //  4
obj.a = 2  //  ERROR: Attempt to reassign a fixed field.
In this and following examples the computation results are shown in comments starting with // (see section Code Comments).

A class declaration can include a block in curly brackets containing the field declarations and method definitions. The constructor
class Cls(fix a) {
  var b = 115
  def sum = a + b 
}
contains in its block the declaration of the field b with the default value 115 and the definition of the function sum:
object obj extends Cls(1)
obj.a  //  1
obj.b  //  115
obj.sum  //  116
In this example, since the types of the fields a and b are not defined explicitly, by default they are determined as Any. Both fields and methods can be declared as private, and then they are inaccessible from outside:
class Cls(fix a) {
  private def add115 = a + 115
  var b = add115
  private fix c = “Hello”
}
object obj extends Cls(1)
obj.b  //  116
obj.add115  //  ERROR: Method add115 is private in class Cls
obj.c  //  ERROR: Field c is private in class Cls
The programmer can specify default values in constructor headers:
class C(fix a, var b = 115)
This feature allows omitting arguments with default values:
{
  var c = C(1, 2) 
  c.b  //  2
  c = C(1)
  c.b  //  115
}
Local variables also can be the formal parameters of constructors used for passing auxiliary values:
class C(i: Int) {
  fix a = i
  var b = i * i
}

C(2).b  //  4
The variable i is local and its life is determined by the time of the instance creation (see section Local Variables). Local parameters are distinguished from fields by the lack of the keyword var/fix.

Both fields and local parameters can be typed or untyped. For instance, in the definition
class C(var x: Int, i) {
  fix y = i
}
the field x is typed as integer, and the field y and the variable i are not typed – by default they are Any.

Field typing is not necessary, but it allows incorporating flexible polymorphic tools. It also improves the conditions for code maintenance (e.g. debugging and documentation).

If a field type descriptor contains the asterisk *, it means that this field can contain a sequence of more than one element (see section Sequences and the Dot Operator)
class C(n: Int, m: Int*)
C(1, (2,3,4)).m  //  2 3 4
C(1, ()).m  //  ()
Sequences in Libretto are established in parentheses. If the asterisk * is absent, the field can be assigned not more than one value (zero or one):
C((), (2,3,4)).n  //  ()
C(1, (2,3,4)).n  //  1
C((1,2,3), (2,3,4)).n  //  ERROR: too many values in field n
The basic feature of sequences in Libretto is that a value (say, 5) and a singleton sequence containing this value (5) are not distinguished. Thus, the following definitions are equivalent:
C(1,5).m  //  5
C(1,(5)).m  //  5
If a field is rightmost in a constructor header and marked by * (like m in the example above), a syntactic sugar allows us to omit parentheses in the sequence:
C(1, (2,3,4))   is equivalent to   C(1, 2, 3, 4)
C(1)  is equivalent to  C(1, ())
In the definition
class C(m*)
m* is the shorthand for m:Any*. A field marked by the asterisk is called a collection field.

Multiple Inheritance

Libretto is based on the multiple inheritance paradigm. For instance, using classes for circles and colored objects
class Colored(fix color: String)
class Circle(fix radius: Real)
and the keyword extends, we can define the class of colored circles
class ColoredCircle(c, r) extends Colored(c), Circle(r)
In this example, the constructor for ColoredCircle depends on the constructors for Colored and Circle. The local parameters c and r pass the values to the superclass constructors. Local parameters, which are not marked by var and fix, are always handled as immutable (fix) variables. Now,
{
  var cc = ColoredCircle(“green”, 5.0)
  cc.color  //  “green”
}
The class Any is the root of the Libretto class hierarchy, from which all other classes inherit. A number of most general methods are associated with Any, e.g. toString (see section Class Any).

If the constructor of a superclass does not have arguments, then the parentheses can be omitted. For instance, the following declarations are equivalent:
class D extends C()
class D extends C

Multiple Inheritance Collision Resolving

The multiple inheritance has potential for producing collisions, when different entities have the same name. The basic principle of handling collisions in Libretto is that the system does not make decisions itself but offers several options to the programmer.

There is a general solution based on entity renaming at import, for instance,
import Package/name1 as name2
This declaration substitutes name1 by name2.

In addition to renaming, the programmer has the following options.

Field Name Collision

Consider the example:
class A(n: Int)
class B(n: Int)
class C(n1, n2) extends A(n1), B(n2)
Here two fields have the same name n associated with the classes A and B. The attempt to access n directly raises an error:
C(1, 2).n
  //  ERROR: Ambiguous field name n
The solution is to use the compound name explicitly indicating, from which class the field is inherited:
C(1, 2).A/n  //  1
C(5, 6).B/n  //  6
Here A/n is the compound (global) name of the field n associated with A. The slash / is used for forming compound names.

Method Name Collision

Consider the example
class A {
   def f = “from A”
}
class B {
  def f = “from B”
}
class C extends A, B
Now, applying f to an instance of the class C raises an error:
C().f
  //  ERROR: Ambiguous function name f
Solution 1. We can explicitly indicate the class name:
C().A/f  //  “from A”
Solution 2. We can redefine f in class C:
class C extends A, B {
  override def f = B/f
}

C().f  //  “from B”
In this solution, the method f of the class C is defined as f of the class B. The keyword override is necessary because f is inherited. To call f associated with A, we can use the compound name A/f.

Note that the definition def f = B/f in the last example does not work with functions as first-class objects (“f is equal to B/f”), but declares that for each object x, the application of f to x (that is, x.f) is equal to x.B/f.

Abstract Methods and Underdetermined Classes

Libretto allows the programmer to define abstract methods, which have the header (signature) but do not have the body. On the other hand, Libretto does not support the concept of an abstract class: each class in Libretto can produce instances. Instead of this, underdetermined classes can be defined, which contain incomplete abstract methods. The lack of preciseness is not a barrier for the creation of instances of such classes. But an attempt to call an abstract method raises an error. Abstract methods can be reified either in subclasses, or directly in the instances of the underdetermined class.

As an example, let us define the class of geometric figures containing the declaration of an abstract method square:
class GeomFigure {
  def square: Real
}
Basing on this class we can define rectangles and resizable circles:
class Rectangle(fix width: Real, fix height: Real) extends GeomFigure {
  def square = width * height
}

class ResizableCircle(var radius: Real) extends GeomFigure {
  def square = 3.14 * radius * radius
  def resize(r) = {radius = r}
}
Now, we can apply the concrete methods:
{
  var rc = ResizableCircle(2.0)
  rc.square  //  12.56
  rc.resize(3.0)
  rc.square  //  28.26
}
The definitions of both heirs of GeomFigure reify the abstract method square. Since the method square in the class GeomFigure is abstract, the keyword override is not needed, when we define its concrete counterparts.

The abstractness of square declared in GeomFigure does not prevent from creating class instances:
{
  fix gf = GeomFigure()
}
This object can be handled normally (e.g. used as a field value or a function parameter etc.) But an attempt to invoke the abstract method raises the error:
gf.square  //  ERROR: Method square is abstract
Adding the method definition, while creating the object, solves the problem:
{
  fix gf = GeomFigure() # {def square = 15}
  gf.square  //  15
}
The operator # combines the components of a dynamic object constructor (see section Dynamic Object Creation).

Constructors and Factory Methods

A class in Libretto can have only one constructor, e.g.
class C(var n: Int)
But this constructor can be accompanied with an arbitrary number of special functions called factory methods. For instance, factory methods for the class C above can be defined:
def C(s: String) = C(s.length)

def C(s: String, nn: Int) {
  fix c = C(“abc”)
  c.n = c.n + nn
  c
}

C(“abc”).n  //  3
C(“abc”, 4).n  //  7
Each of them creates a new instance of the class C by invoking its constructor. The first factory method does this directly, while the second one operates via the first method. The most appropriate constructor or factory method is selected by dynamic dispatch (see section Dynamic Dispatch).

If the class constructor and a factory method have the same signature (the same number and types of parameters) then the factory method has priority to be invoked over the constructor. In such situations to invoke the constructor, a predefined method new must be applied (see section Metaprogramming). For example,
class C(fix s: String)

def C(s: String) = %C.new(s+s)  // overshadows the constructor

def C(s1: String, s2: String) = %C.new(s1+s2)
def C(s1: Int) = C(s1.toString + “!”)

C(“abc”).s  //  “abcabc”
C(“a”, “bc”).s  //  “abc”
C(15).s  //  “15!15!”
Here the constructor and the first factory method have the same signatures. Thus to call the constructor, the method new is applied. %C is the object of the class C (see section Class Object). Note that the last factory method uses not the constructor but the first factory method. The keyword private restricts constructor accessibility to the package, in which the class is defined:
class C private(fix s: String)
Factory methods have the following features:

  • The name of a factory method coincides with the class name. 
  • A factory method has access to all fields and methods associated with the class – including private ones. 
  • A factory method can be defined only in the package, in which the class is defined. External method definitions are not allowed. 
  •  The fields and methods of a class can be introduced only in its constructor. Only local variables are allowed to be the formal parameters of factory methods. 
  • An access to the constructor of a class can be performed by the method new, which defined on the class object. 
  • The set of definitions comprising the constructor together with all factory methods forms the polymorphic definition of the class. 
  • Factory methods do not necessarily create brand-new instances of the class, they can return existing objects. 
  • In other aspects, a factory method is an ordinary method, for whose behavior the programmer is solely responsible. 

Factory methods can provide flexible control over the set of existing class instances. For instance, it is not necessary that any factory method creates a new instance. Instead of this it can return existing objects, store them in a cache, and do other advanced operations.

In the following example the first factory method creates a new instance by applying the constructor, whereas the second factory method never creates new objects but returns the same object with updated contents instead. First, define the class MyString:
class MyString(fix str: String = “”) {
  private var extra = “”
  override def toString = str + extra
}
Now, define the first factory method of the class MyString. This method calls the constructor to create a new object and then sets the value of the private field extra:
def MyString(base: String, extras: String) {
  fix s = %MyString.new(base)
  s.extra = extras
  s
}

MyString(“hello, ”, “ world”).toString  //  “hello, world”
The second factory method does not create new objects, but always uses the same object cacheString:
object cacheString extends MyString {extra = “!”}
def MyString(base: String) = cacheString {str = base}
Note that in the body of the first factory method the constructor is called via the method new, instead of direct MyString(base). It is necessary, because dynamic dispatch selects the appropriate constructor or method dynamically – at the moment of a call. This means that as soon as the second factory method has been introduced with the signature coinciding with that of the constructor, MyString(base) in the first factory method would be interpreted as a second factory method call.

Now
{
  fix gb = MyString(“goodbye”)
  gb.toString  //  “goodbye!”
  MyString("hello")  //  “hello!”
  gb.toString  //  “hello!”
}
The introduction of a class overshadows existing entities having the same name. For instance, if a function with the same name exists, it becomes inaccessible after the class introduction:
def C(s: String) = s + s
C(“abc”)  //  “abcabc”

class C(n: Int) 
C(“abc”) 
  // ERROR: neither constructor nor factory-method found for this input

Class Object

By using the operator % (see section Operator %) we can obtain a special object, which represents the class. This object allows handling the class as a first-class entity. Let us define C:
class C(fix s: String)
The class C is represented by the object %C. %C is an instance of the predefined class Class. This object is created automatically at the moment when the class is introduced. A number of metaprogramming methods are based on this object (see section Metaprogramming). In particular, the method new, which explicitly invokes the class constructor, is defined on class objects.

No comments:

Post a Comment