Saturday 18 February 2012

Basic Libretto Constructs (2 of 5). Functions

In this post we consider and discuss functions in Libretto.


Function Definitions

A named function definition starts with the keyword def. Then the function’s header and body follow separated by the equality sign:
def fact(n) = if (n == 0) 1 else n * fact(n - 1)

fact(5)  //  120
The arguments and the result of a function can be optionally typed:
def fact(n: Int): Int = if (n == 0) 1 else n * fact(n - 1)
Argument typing is the basis for polymorphic function definitions (see section Parameter Polymorphism)

A function can return not only a single value but also ordered collections, for instance,
def double(n: Int) = {
  fix n2 = n * n;
  (n2, n2 * n)
}
The function double returns the square and the cube of a number:
double(5)  //  25  125
In Libretto such ordered collections are called sequences. Parentheses are used to represent sequences (see section Sequences and the Dot Operator). The body of double contains a block in curly brackets. The value of a block is always the value of its rightmost expression (see section Block Operator).

The expressions in a block are separated by a semicolon, but due to the rules for its omitting (see section Omitting Semicolons), the semicolon have to be used quite rarely. In particular, the definition of double can be rearranged as follows:
def double(n: Int) = {
  fix n2 = n * n
  (n2, n2 * n)
}
When the body of a function has the form of a block in curly brackets, the equality after its header can be also omitted:
def double(n: Int) {
  fix n2 = n * n
  (n2, n2 * n)
}
The return expression return n evaluates n and then exits the function returning n’s value as the result:
def oddEven(n: Int) {
  if (n mod 2 != 0) return n
  println(“hehe”)
  n * n
}
If oddEven is applied to an odd integer, it returns the argument itself. If its argument is even, then the square of the argument is returned, but before this “hehe” is printed:
oddEven(115)  //  115
oddEven(100)
  //  “hehe” 
  //  10000
The return expression without the argument is equivalent to return (), where () is the empty sequence.

Pervasive Context

A pervasive context is the key concept of Libretto. The computation of any expression in Libretto is performed within some context. A context can be regarded as the current state of a computation or its implicit parameter. Let us define
def plus1() = this + 1
This nullary function plus1 depends only on the context. Nullary functions can be defined without parentheses (see a special convention in section Nullary Function Definition):
def plus1 = this + 1
The dot operator is responsible for passing the current context to a function:
115.plus1  //  116
“115”.plus1  //  “1151”
In the first expression, plus1 gets as its context integer 115, and in the second – string “115”. The keyword this is used for accessing the current context value.

We can redefine plus1 and make it independent of the context value:
def plus2(x) = x + 2
plus2(115)  //  117
234.plus2(115)  //  117
plus2(“115”)  //  “1152”
A table in section Dot Operator Generalization shows the expressions evaluation rules dependently on their context. In particular, constants and objects do not depend on the context and always equal themselves:
115.“abc”  //  “abc”
The context is pervasive in Libretto. For instance,
def helloWorld = hello + world 
def hello = "Hello, "
def world = "World" + this

"!".helloWorld  //  "Hello, World!"
Although the context value "!" explicitly occurs only at a call of helloWorld, it penetrates through to the body of the function world. This happens because the context is passed in Libretto as an implicit argument to each subexpression. This means that the context "!" is available for the expression hello + world, and for its subexpressions hello and world. So, helloWorld behaves as if it were defined as

def helloWorld = this.(this.hello + this.world)
Thus,
The context of a function call has impact on its body. The context of the whole expression has impact on each subexpression.

A function in Libretto can depend on the context and arguments simultaneously:
def pol(x,y) = this * x + y
2.pol(3,4)  //  10
The context allows ‘pipeline’ computations within paths (see section Sequences and Paths). For instance, def plus1 = this + 1
“a”.println().plus1.println().plus1.println()  //  “a11”  
  “a”
  “a1”
  “a11”
In the path above (a path is a chain of expressions separated by the dot operator, see section Paths and Steps) each next call of plus1 takes as its context the result of evaluation of the preceding call. Method println() prints the context value without changing it (passes to the next step its own context). Now let us consider another example of a ‘pipeline’ computation:
class Stack {
  private stack*

  def push(x) {stack .= x}
  def pop() {stack(0) --; this}
  def top = stack(0)
  def sum() = push(pop() + pop())
  def mult() = push(pop() * pop())
}
The sequence of stack elements is stored in the private field stack.
Stack().push(1).push(2).sum().push(3).mult().top  //  9
This path example is based on the idea that each next step receives as its context the same object as the preceding one (the stack object). The definition of the class Stack uses several assignment operators (see section Assignment Operators). The assignment operator .= is transparent for its context (input equals output), but the deletion operator -- returns deleted elements, so ‘this’ must be used in the body of the function pop.

Function arguments also depend on the context:
def plus(x) = this + x

1.plus(plus(2).plus(3))  //  7
The outer call of plus and plus(2) in the argument are evaluated in the context of 1. The expression plus(3) is evaluated in the context of the value of plus(2).

Similarly the context works in function bodies. In a function body, the context of each expression at the outer level is equal to the context of the function call (that is, ‘this’). This means that ‘this’ can be omitted in many cases. In the next example, each expression in the function body is evaluated in the context of the function call:
def plus1 = this + 1
def f {var x = plus1; plus1 + x}
2.f  //  6
The above definition of f is equivalent to the following:
def f {var x = this.plus1; this.plus1 + this.x}
We remind that the value of a block is equal to the value of its rightmost expression.

The semantics of paths is heavily based on the context (see section Paths and Steps). A special symbol $ plays in paths the role of ‘this’. It denotes the context value of the current step of the path:
1.($ + 2)  //  3
The evaluation of step ($ + 2) is performed in the context of 1. The value of $ changes from step to step: the result of evaluation of the preceding step becomes the context of the next one.

On the outer level of function bodies the values of ‘this’ and $ coincide:
def f = if (this == $) “a”.($+this) else “no”
“bc”.f  //  “abc”
Here the first occurrence of $ is equal to the context of the function call (that is, “bc”), and the second occurrence of $ is equal to “a”. The value of ‘this’ does not change.

Thus, using $ we could define ‘this’ explicitly (if ‘this’ were not a keyword):
def f {
  fix this = $
  ...
}
A context can contain a sequence of several objects. For instance:
def plus1 = this + 1
(1,2,3).plus1  //  2 3 4
The function plus1 is evaluated in the context of three integers. This function is sequentially applied to each sequence element. In Libretto there are two types of functions:

  • Element functions. These functions handle each element of the context sequence separately, gradually forming the output sequence. Element functions are called as many times as there are elements in the sequence. In particular, there are no calls of an element function in the context of the empty sequence.
  • Collection functions (see section Collection Functions) handle the context sequence as a whole. A collection function is called exactly once, independently of the size of the context collection (including the empty collection).

The function plus1 above and the function fact are element functions:
def fact = if (this == 0) 1 else this * (this - 1).fact

5.fact  //  120
(1,2,3,4,5).fact  //  1 2 6 24 120
and the function sum is a collection function:
def * sum {
  var x = 0
  this. (x = x + $)
  x
}
(1,2,3,4,5).sum  //  15
The asterisk in the context type declaration indicates that the function is a collection function. In collection functions ‘this’ takes the value of the whole context sequence.

The context governs the computation in paths. In particular, an appearance of the empty context in a path means failure and the necessity to backtrack:
().1  //  ()
The value of this path is equal to (), although 1 does not depend on the context (as a constant). But the computation does not reach 1 because the preceding step produces the empty sequence.

Thus, the empty sequence is the important element of the path evaluation strategy (see section Empty Sequences in Paths). Only collection functions can handle the empty sequence as a normal value. The empty sequence can be also caught and handled by local traps (see section The Empty Context as the Weakest Exception).

Nullary Function Definition

A nullary function can be defined either with parentheses or without them:
def plus1() = this + 1
def plus1 = this + 1
In Libretto the following syntactic rule applies: a nullary function is used in the same form as it has been defined. For instance, if the factorial is defined as
def fact = if (this == 0) 1 else this * (this - 1).fact
the call fact() is not allowed. Similarly, if a nullary function has been defined with parentheses, a parenthesis-free call raises the error. A good programming style is to use the parenthesis-free form if the function does not have side effects, and with parentheses, otherwise. For instance, since the following function has a side effect (printing) the parentheses should be used:
def myPrint() = println(this)

(1,2).myPrint()
  //  1
  //  2
Parenthesis-free nullary functions are convenient for the representation of ‘dynamic’ fields. Compare the behavior of two classes:
class Person1(var name: String, var surname: String) {
  var fullname = name + “ ” + surname
}
class Person2(var name: String, var surname: String) {
  def fullname = name + “ ” + surname
}
Now:
object p1 extends Person1(“Tom”, “Smith”)
object p2 extends Person2(“Tom”, “Smith”)
p1.fullname  //  “Tom Smith”
p2.fullname  //  “Tom Smith”
Let’s change the Smith’s name:
p1.name = “Paul”
p2.name = “Paul”
p1.fullname  //  “Tom Smith”
p2.fullname  //  “Paul Smith”
In the second expression the value of fullname is still valid, whereas in the first case it becomes incorrect.

Default Parameters

In Libretto, the default values of function parameters can be specified. If a parameter has a default value, it can be omitted in a function call:
def f(x: Int, y: Int = 2, z: Int = 3) = x + y + z
f(1)  //  6
f(1, 10) // 14
f(1, 10, 100)  // 111
To keep this scheme correct, the default values must be set from the rightmost position to the left:
def f(x: Int, y: Int = 2, z: Int) = x + y + z
  //  ERROR: Parameter z does not have default value

Explicit Parameter Passing

In some cases (e.g. in domain specific language development) it is convenient to work with parameter names explicitly. This is also allowed in Libretto:
def f(x, y, z = 100) = x + y + z

f(z = 5, y = 2, x = 3)  //  10
f(y = 10, x = 1)  //  111

Context, Parameter and Field Typing

The function context also can be typed. For instance, we can specify the context type of fact as Int:
def Int fact = if (this == 0) 1 else this * (this - 1).fact
Libretto is a dynamic language, and typing does not play in it such a crucial role as in static languages. Libretto allows code development in a script-like type-free style. But typing significantly enriches Libretto. For instance, context and parameter typing provides such expressive tools as polymorphism and dynamic dispatch. Besides, context typing allows us to use functions as methods. A number of Libretto constructs like external methods and external fields are also based on context typing (see sections External Methods and External Fields).

A type in Libretto is defined as a pair class/cardinality (cardinality is a maximum allowed number of elements). In the following example
class C {
  fix x: Int
  fix y: Int*
} 
the fields x and y can store integers. But the field x is allowed to store at most one value (this is determined by default). In the declaration of y the asterisk means that the field can store an arbitrary sequence of integers. Concrete positive numbers also can specify the maximally allowed number of elements. In the following example
class C {
  fix x: Int(5)
}
the field x can contain at most five values. The attempt to add extra values raises an error:
C() {x = (1,2,3,4,5,6,7,8,9,0)}  //  ERROR: Too many values for x
For the sake of efficiency, dynamic dispatch in Libretto does not distinguish parameters of the same type but with different cardinalities. For instance,
def f(x: Int) = 1
def f(x: Int*) = 2

f((1,2,3,4,5))  //  ERROR: ambiguous definitions for function f
While selecting the most appropriate function, the dynamic dispatch algorithm takes into account the parameter type and ignores the parameter cardinality. Therefore it does not distinguish the signatures of the function definitions above. This weakening does not affect the expressive capabilities, preventing at the same time from costly checks, while applying functions and class constructors.

On the other hand, cardinality plays a key role in context typing. Collection functions (whose contexts are typed with the asterisk), have the semantics different from that of element functions (without asterisk). For instance,
def  f = size
def* g = size

(1,2,3,4,5).f  //  1 1 1 1 1
(1,2,3,4,5).g  //  5
The element function f selects each element of the context separately (it is called once for each context element). The collection function g first collects all context elements in a sequence, and only then is applied (only once). Here size is the predefined collection function counting the number of elements in the context sequence. Collection functions are discussed in more detail is section Collection Functions. The signature of a function definition
def C0 f(x1:C1,..., xN:CN) = ...
is a tuple [C0,C1,...,CN] of class names, which specify the types of the context and the parameters of the definition.

Unlike fields or parameters, local variables defined in blocks and paths are always type-free (see section Local Variables). By default, immutable fix-variables have the type of their initializing value, and mutable var-variables have the type Any. But the cardinality of a local variable can be specified by the asterisk:
{
  var x = 1
  var y* = (1,2,3)
}
The variable x can have at most one value simultaneously, whereas y can be assigned an arbitrary sequence of elements.

Functions as Methods

Thanks to context typing, functions can play the role of methods. Let us consider geometric figures:
class Rectangle(fix width: Real, fix height: Real) {
  def square {width * width}
}

class Circle(fix radius: Real) {
  def square {3.14 * radius * radius}
}
Each class above has its own definition of the function square. The context type of each definition is specified automatically as the class, in which the definition occurs.

In Libretto, a method of a class C is a function, whose context is typed as C.

When square is applied, its most appropriate definition is selected:
object r extends Rectangle(5, 10)
object c extends Circle(2)

r.square  //  50
c.square  //  12.56
1.square  //  ERROR: Field or method square not found for object 1
Methods are inherited. The class of colored circles is the heir of Circle:
class ColoredCircle(r, var color: String) extends Circle(r) {
  def recolor(color) { @color = color }
}
The recolor method is defined in this class, whereas square is inherited from Circle. Both methods are defined on the objects of the class ColoredCircle. The operation @ allows the use of the same names for fields and variables (see section Operator @ and Parametric Fields).
Circle(3.0, “blue”).square  //  28.26
Circle(5.2, “green”).recolor(“red”).color  //  “red”
The keyword this is used in function/method bodies for access to the current context value:
class C(fix n) {
  def getN2 {this.n + this.n}
}

C(5).getN2  //  10
Since all expressions in Libretto (in particular, function bodies) are always computed in the context, ‘this’ can be omitted:
class C(fix n: Int) {
  def getN2 {n + n}
}

C(5).getN2  //  10
The keyword override is used to redefine a method defined in a superclass:
class Person(name: String, age: Int) {
  override def toString = name + “ aged ” + age
}
The method toString defined in the class Any is redefined in the class Person (each class in Libretto is a heir of the root class Any). This means that if toString is called in the context of a Person’s instance, then the above definition is selected. For instance,
Person(“John”, 35).toString  //  “John aged 35”
Functions in Libretto can be defined beyond the class definitions. Such functions are called external methods. The context type of external methods is specified explicitly (see details in section External Methods):
def Int f = this + 1
(1,2,3).f  //  2 3 4

Polymorphic Methods

Polymorphic definitions of functions are based on context and argument typing. This is an example of a polymorphic function based on its context type:
def Int f = this + 1
def String f = this * 2

1.f  //  2
“abc”.f  //  “abcabc”
Let’s define the underdetermined class of geometric figures:
class GeomFigure {
  def square
}
This class definition contains an abstract function square. Now, define rectangles and circles as subclasses of GeomFigure:
class Rectangle(xx: Real, yy: Real) extends GeomFigure {
  def square = xx * yy
}

class Circle(rr: Real) extends GeomFigure {
  def square = 3.14 * rr * rr
}
Since in the definition of GeomFigure the method square is abstract, the keyword override should not be used. Let us add the definition of square, which handles inappropriate objects:
def Any square = error("Only geometric figures have square")
This is an external method, which works on the level of the package. Now,
object r extends Rectangle(2.0, 3.0)
object c extends Circle(2.0)

r.square  //  6.0
c.square  //  12.56
We can check how square works on erroneous data:
GeomFigure().square  //  ERROR: Method square is abstract

5.square  //  ERROR: Only geometric figures have square
In this example square has a polymorphic definition, which can work in various contexts. The selection of the most appropriate method is based on the class hierarchy. For instance, the most appropriate method for a rectangle has the context type Rectangle, whereas the best method for integer 5 has the context type Any. Two of the three square methods in the example are part of class definitions, while the third one is defined externally (on the package level).

Erroneous data in the context can be also handled by local traps (see section Local Exception Handling):
object r extends Rectangle(2.0, 3.0)
object c extends Circle(2.0)

(r, 5, c).square ?{case e if String => e}. println()
  //  6.0 
  //  “Only geometric figures have square” 
  //  12.56 
For each context value the most appropriate definition of a polymorphic method is selected individually.

Parameter Polymorphism

Libretto allows polymorphic definitions based on the parameter types:
def is(i: Int) = “integer”
def is(s: String) = “string”

is(5) // “integer”
is(“abc”) // “string”
A more complicated example:
class Cls1 {
  def f = “this is Cls1”
  def g(i: Int) = “Cls has number ” + i 
  def g(s: String) = “Cls1 is a ” + s
}
class Cls2 {
  def f = 115
  def g(x) = x + “!”
}
object c1 extends Cls1
object c2 extends Cls2
The function f defined on both classes Cls1 and Cls2 is polymorphic by context:
c1.f  //  “This is Cls1”
c2.f  //  115
The function g is polymorphic by both context and arguments:
c1.g(1)  //  “Cls has number 1”
c1.g(“first class”)  //  “Cls is a first class”
c2.g(“first class”)  //  “first class!”
(c1,c2).g(“first class”)  //  “Cls is a first class”  “first class!”
In Libretto, a dynamic dispatch algorithm is used for selecting the most appropriate definition of a polymorphic method.

Dynamic Dispatch

Dynamic dispatch selects for a call of a polymorphic function the most appropriate definition basing on the context and argument types of the call. Let there be two definitions of a function having the signatures [C0,...,CN] and [D0,..., DN], respectively.

  • We say that [C0,...,CN] is less than [D0,..., DN] if for each pair Ci and Di either Ci equals Di, or Ci inherits from Di.
  • We say that a function definition is appropriate to a call, if the signature of the call actual parameters is less than the signature of the function definition.
  • We say that a function definition can be applied to a function call, if among all definitions, which are appropriate to the call, this definition has the least signature (that is, its signature is comparable with the other signatures and less them all).

So, the most appropriate definition has the least signature among all definitions applicable to the call. For instance,
class C1
class C2 extends C1
class D1
class D2 extends D1

def f(x: C1, y: D1) = 1
def f(x: C2, y: D1) = 2

f(C2(), D2())  //  2
The second definition is applied here, because [C2,D2]<[C2,D1]<[C1,D1]. In case of ambiguity an error occurs. Let us define
class C1
class C2 extends C1
class D1
class D2 extends D1

def f(x: C1, y: D2) = 1
def f(x: C2, y: D1) = 2
When the most appropriate definition can be found, the computation is successful:
f(C1(), D2())  //  1
f(C2(), D1())  //  2
But
f(C2(), D2())
  //  ERROR: ambiguous definitions for function f
The error occurs because the two definitions of f are incomparable and can be equally applied to the call (the most appropriate definition can not be selected).

Polymorphic Definitions vs Typeless Definitions in Libretto

Polymorphism in Libretto is based on the following principles:

  • Typing and polymorphic tools are mainly used in Libretto for better expressiveness; they also contribute in secure programming, and make easier program maintenance and documentation.
  • The use of polymorphic tools is not obligatory: Libretto allows program development in a type-free style.

For instance, Libretto allows the following type-free definition:
def f = 
  if (String) this + this 
  else if (Int) this * 10
  else error(“wrong!”)

“abc”.f  //  “abcabc”
Its polymorphic counterpart is:
def String f = this + this
def Int f = this * 10
def f = error(“wrong!”)

“abc”.f  //  “abcabc”
Each variant works correctly, and the choice is largely the matter of programming style and discipline.

The same situation is with class constructors (see section Constructors and Factory Methods). We can define a type-free constructor
class C(n) {
  fix field = n. if (String) length else $
}
as well as a constructor with a polymorphic factory method:
class C(fix field)
def C(n: String) = C(n.length)

Duck Typing

For many tasks (and programmers) inheritance and polymorphism are unnecessarily heavy. Therefore Libretto supports a dynamic style of typing, e.g. duck typing. Duck typing is dynamic typing, in which the semantics of an object is determined by the set of methods and fields, rather than by inheritance from classes. Duck typing is characterized by the following well known maxim: when I see a bird that walks like a duck and swims like a duck and quacks like a duck, I call that bird a duck.

In the following example of duck typing the classes of people and ducks have methods with the same names, but these methods can not be integrated within a mutual superclass due to their semantic incompatibility:
class Duck {
  def quack() = print("Quaaaaaaak!")
  def feathers() = print("The duck has white and gray feathers.")
}

class Person {
  def quack() = print("The person imitates a duck.")
  def feathers() = print("The person shows a duck’s feather.")
}

def inTheForest() {
  quack()
  feathers()
}
 
def game() {
  fix donald = Duck()
  fix john = Person()
  donald.inTheForest()
  john.inTheForest()
}
 
game()
In this example the donald duck and the person john have the methods for quacking and demonstrating feathers, but these methods have completely different meaning for the duck and for the man.

Consider a more serious example. The problem of closing resources (e.g. files or streams) after their reading or writing is well known. Usually a resource is closed by some close() method. The problem arises when we do not have a most general class of all resources, in which close() could be defined. Duck typing allows us to easily cope with this problem. Let us define the ‘duck-typing’ function
def with(resource, block) = 
        try {resource.block()} finally {resource.close()}
which gets a resource, does something with it by applying the block block, and then closes it (try-expressions are discussed in section Operator try and Global Exception Handling). The only assumption concerning the resource is that the method close() is defined on it. Duck typing is irreplaceable here, because we do not know, from which class the resource arrives, so close() can not be inherited from anywhere.

For instance, a resource can be an instance of the class File
class File(fix name) {
  def open()
  def close()
  def readline 
  ...
}
and then an ordinary query like
with(File("hehe.txt")) {open().readline}
can be performed. Duck typing makes the definition of with very cosmopolitan and insensitive to the application domain. For instance, we can handle a jam pot in the same way as a file:
class Pot(fix stuff, fix from) {
  def open() = {
    “Take a {stuff} pot from {from} and remove its lid”!.println() 
    this
  }
  def eat = “Eat 2 spoons of {stuff}”!.println()
  def close() = “Put the lid on”.println()
}
Here “Eat 2 spoons of {stuff}”! is a parametric string (see section Parametric Strings). Now we can call with:
with(Pot("jam", "the shelf")) {open().eat}
  //  Take a jam pot from the shelf and remove its lid
  //  Eat 2 spoons of jam
  //  Put the lid on
Inheritance and duck typing are easily combined. For instance, we can define the main data about an object via classes and inheritance, but data about its interface representation – via duck typing:
Person(fix name: String, fix age: Int)
object JOHN extends Person(“John”, 31) {
  def interfaceColor = “green”
}

JOHN.age  //  31
JOHN.interfaceColor  //  “green”
Here the method interfaceColor is defined in the duck typing style, whereas the fields name and age are inherited.

Duck typing is closely related to the EAFP principle (“it is Easier to Ask for Forgiveness than for Permission”). The EAFP recommends us not to check beforehand if a method can be applied to an object (e.g. in the style of instanceof), but to try to apply it immediately, and handle the exception in case of failure.

A version with a preliminary check (‘ask for permission’) corresponds to the following Libretto code:
str. if (String) inverse else error(“String not found”)
Here str is first checked for being a string and, if yes, inverted. Otherwise the error occurs.

The next Libretto query follows the EAFP principle. To handle the problem it uses a local trap (see section Local Exception Handling):
str.inverse ?{case _ => error(“String not found”)}
Local traps are expressive tools capable of solving problems on the spot in a readable and compact manner – exactly what the EADS principle needs.

External Methods

Functions in Libretto can be defined beyond classes, that is, on the package level. Such functions are called external methods. Using external methods we can define static methods, which do not depend on the context. Besides, external methods are useful for adding functionality to classes without affecting class definitions, e.g. in case of imported classes. For instance,
def Int plus5 = this + 5
Now:
1.plus5  //  6
The external method plus5 expands our capabilities in handling integers. But the class Int is not affected by this definition.

An external method definition is based on the context typing declaration (Int in the above example). If the typing declaration is omitted, then by default it is specified as Any (note that methods introduced within class definitions are always implicitly typed by their host class).

The polymorphic definitions of external methods are also possible:
class ShowIt(fix n: Int)
class DoNotShowIt

def ShowIt show = “I like to show ” + n
def DoNotShowIt show = “Never show it!”

ShowIt(115).show  //  “I like to show 115”
DoNotShowIt().show  // “Never show it!”
The combinations of in-class and external definitions are also possible:
class Circle(fix radius: Real) {
  def square = 3.14 * radius * radius
}
def Any square = “Only circles have squares”

Circle(1).square  //  3.14
1.square  //  “Only circles have squares”
In this example square has two definitions – for circles and non-circles – which are handled by dynamic dispatch (see section Dynamic Dispatch). The second method is external.

The basic characteristic of external methods is that their behavior is not distinguishable from that of in-class methods:
def String f(n: Int) = repeat(n, “+”)
def String f(s: String) = this + s
def Int f(n: Int) = this * n
def Int f(s: String) = s.repeat(this, “-”)

“abc”.f(3)  //  “abc+abc+abc”
(“abc”, “-=+”).f(“def”)  //  “abcdef”  “-=+def”
(2, 3).f(4)  //  8 12
3.f(“abc”)  //  “abc-abc-abc”
(2, “abc).f(3)  //  6  “abc+abc+abc”  
The function f is defined separately for each combination of integer and string data. The predefined function repeat produces several copies of a string separated by the second argument.

If two methods, in-class and external, are equally applicable, then the priority is granted to the external method:
class A {
  def f(b: B) = “inner”  // internal method
}

class B
class C extends A, B

def B f(a: A) = “outer” // external method

C().f(C())  //  “outer”
Here both definitions of f are equally applicable and their signatures are incomparable. But since the second method is external, it is selected for the application (see section Dynamic Dispatch).

To sum up: an external method expands handling capabilities for instances of a class – without modifying the class itself. An external method can be exported as well as defined in a package different from that, where the class is introduced.

External Methods on Objects

External methods can be defined not only in the context of classes, but also in the context of declarative objects. For instance,
class Lang {
  def helloWorld = “Hello, world!”
}
object English extends Lang
object French extends Lang

def French helloWorld = “Bonjour tout le monde!”

English.helloWorld  //  “Hello, world!”
French.helloWorld  //  “Bonjour tout le monde!”
On the other hand, external methods can not be defined in the context of dynamic objects, because their context type should be specified by the object name, but dynamic objects are anonymous.

The Import of External Methods

An external method behaves as if it were defined in a class/object definition – with one difference. In-class methods are integral part of their host classes or objects, whereas external methods are independent constructs. This difference is significant for importing. Let us define a package p1:
package p1
class Lang {
  def helloWorld = “Hello, world!”
}
def Lang helloWorld = “Hi, world!”
Lang().helloWorld.print()  //  “Hi, world!”
In the call of helloWorld, its external definition is applied, which has priority over the in-class method. Define a package p2:
package p2
import p1/Lang
Lang().helloWorld.print()  //  “Hello, world!”
Although Lang has been imported in p2, together with its own method helloWorld, the external definition of helloWorld is still inaccessible. It must be imported explicitly:
package p2
import p1/{Lang, helloWorld}
Lang().helloWorld.print()  //  “Hi, world!”
The asterisk can be used to import all public entities in the namespace of p1:
package p2
import p1/*
Lang().helloWorld.print()  //  “Hi, world!”

Collection Functions

Most of the functions we defined above handle context sequences in an element-wise manner. For instance,
def Int plus1 = this + 1
(1,2,3).plus1  //  2 3 4
Here for each call of plus1this’ is assigned the next value of the context sequence. The other functions are those, which handle the context sequence as a whole. Such functions are called collection functions. For example, the function sum, which sums up the sequence of integers
(1,2,3,4,5).sum  //  15
uses as input the whole sequence simultaneously.

To declare a function as a collection function, its context type must be marked by the asterisk. When a collection function is called

  • this’ is assigned the whole context sequence,
  • the function is called exactly once – independently on the type and size of its context sequence.

This is the definition of sum:
def Int* sum {
  var x = 0
  this. (x = x + $)
  x
}

(1, 2, 3, 4, 5).sum  //  15
Since sum is a collection function (its context type declaration contains *), ‘this’ is equal to the whole sequence (1,2,3,4,5). The dot expression this.(x = x + $) iterates over the elements of the context sequence as a ‘foreach’ operator.

The predefined function size could be defined in the same way:
def* size {
  var x = 0
  this. (x = x + 1)
  x
}

(1, 2, 3, 4, 5).size  //  5
Here def* is a shorthand for def Any*.

The behavior of element functions and collection functions is dramatically different on the empty context:
def f = size
def* g = size

(1,2,3).f  //  1 1 1
(1,2,3).g  //  3

().f  //  ()
().g  //  0
The element function f is called once for each element, that is, zero times for the empty context. Thus, its result is also the empty sequence. A collection function first collects all context elements (zero elements in case of the empty context) and then is called exactly once.

The actual arguments of a call of a collection function are also evaluated in the context of the whole context sequence, for instance,
def Int* sumN(n: Int) {
  var x = 0
  this. (x = x + $)
  x + n
}
The function sumN sums up the elements of its context sequence and then adds its argument n to the sum. In the query
(1,2,3).sumN(size)  //  9
the argument is the number of elements of the context sequence counted by the function size.

In Libretto the following rule is applied, which determines the semantics of paths (see section Paths and Steps):

If a path step evaluation returns the empty sequence, then the computation of the path continues only if its next step is a call of a collection function.

That is, only collection function calls are allowed to be evaluated in the empty context:
().sum  //  0
().1  //  ()
A generalized type of a sequence (the list upper bound of its element types) is always calculated. This is useful for polymorphic definitions:
def Int* aggregate = sum
def String* aggregate = join(“, ”)

(1,2,3,4,5).aggregate  //  15
(“a”, “b”, “c”, “d”).aggregate  //  “a, b, c, d”
(1,”a”).aggregate  //  ERROR: Method aggregate not found for class Any
Here join(“, ”) is a predefined function, which concatenates the context strings separated by comma. Note that both definitions of aggregate implicitly use the context, as if they were defined like:
def String* aggregate = this.join(“, ”)
Inaccurate use of typed context sequences can seriously affect the efficiency of computation.

Now consider another example – a so-called zip function, which uses as input two sequences of the same length and returns the sequence of pairs:
class Pair(fix fst, fix snd)

def * zip(s*) {
  this index i. Pair($, s(i))  
}
Let’s apply it to a couple of sequences:
(“a”, “b”, “c”). zip(1,2,3). “{fst}:{snd}”!
  //  “a:1”  “b:2”  “c:3”
The input sequences can be restored:
(“a”, “b”, “c”). zip(1,2,3). snd  //  1 2 3
In the next example, the ordinary set operations are defined:
def* union(snd) = (this, snd).distinct
def* intersection(snd) = this ?[$ in snd].distinct
def* subtraction(snd) = this. if (not $ in snd) $
def* difference(snd) = union(snd).subtraction(snd)
def* complement(snd) = snd.subtraction(this)
Sequences are always flat (linear), so ((1,2,3),(4,5,6)) is equivalent to (1,2,3,4,5,6). This allows us to define union as presented above. The expression ?[$ in snd] is a predicate. It lets through only those context elements, which belong to the sequence snd. The predefined function distinct removes repeating elements (because sequences play the role of sets).

These definitions of the set operations are not very efficient, but correct:
(1,2,3,4).union(3,4,5,6)  //  1 2 3 4 5 6
(1,2,3,4).intersection(3,4,5,6)  //  3 4
For collection and element definitions of functions the following rule is applied:

A function can not have element and collection definitions simultaneously.

For instance,
def  f = this
def* f = this

1.f  //  ERROR: collection-wise and element-wise definitions of f 

Functions as Objects

Classes, functions, fields and packages are defined in Libretto as objects with additional functionality. Using the operator % we can handle these entities as objects. Let us define
def fact = if (this == 0) 1 else this * (this-1).fact
Now %fact denotes the object of the function fact. Any object can turn into a function if the method do is defined on it (see section Methods do and undo). That is, the definition of the factorial above is equivalent to the following one:
object fact extends Function {
  def do = if (this == 0) 1 else this * (this-1).fact
}
The expression 5.fact is actually a shorthand for 5.%fact.do. The predefined class Function is used mostly in metaprogramming. Note that inheritance from Function is not obligatory:
object fact {
  def do = if (this == 0) 1 else {this * (this-1).fact}
}
5.fact  //  120
Thus, functions in Libretto can be defined in a duck typing style and recognized by the presence of the method do. Inheritance from Function is necessary only when metaprogramming is involved (see section Metaprogramming), and we need to deploy special methods.

The name fact can be interpreted as either an object name or a function name. To avoid ambiguity the following rule is applied:

If an object represents some entity, its name is interpreted by default as this entity. If it is necessary to interpret it as an object, the operator % is used.

For instance,
def app(f) = f()
5.app(%fact)  //  120

def add1 = this + 1

// add1 is interpreted as a function:
5.add1  //  6

// %add1 is interpreted as an object:
5.app(%add1)  //  6

Anonymous Functions

Libretto allows dynamic function definitions based on using the operator %. For instance,
%(x){x + 1}
This is an anonymous function with one argument. Syntactically, this definition resembles a named function definition with the omitted keyword def and % instead of the name. Using parentheses we can apply this function to an argument:
%(x){x + 1}(5)  //  6
An anonymous function can be handled as an ordinary object, for instance, be assigned to a variable:
{
  fix fun = %(x){x + 1}
  fun(5)  //  6
}
or passed as a parameter to another function:
def app(f) = f()
def app(f, arg) = f(arg)

5.app(%(){this + 1})  //  6
5.app(%(n){this + n}, 100)  //  105

{
  fix times = %(s){s * this}
  5.app(times, “abc”)  //  “abcabcabcabcabc”
}
Here we see that anonymous functions are also evaluated in the context and can contain the keyword this.

In nullary anonymous functions the empty list of arguments can be omitted:
%(){this + 1}   is equivalent to     %{this + 1}:

5.app(%{this + 1})  //  6

Closures

When an anonymous function is created, it is supplied with a referencing environment containing references to variables and fields, which are defined beyond the function but have occurrences in its body. For instance,
{
  var x = 1
  fix fun1 = %{x + 100}
  x = 2
  fix fun2 = %{x + 100}
  fun1()  //  102
  fun2()  //  102
}
If some name can not be recognized at the moment of a function creation, it is interpreted as a field, the value of which should be available from the context at the moment of the anonymous function evaluation:
class C(f) {
  fix n = “Hello, ”
  def evaluate = f()
}

{
  fix x = “world!”
  C( %{n + x} ).evaluate  //  “Hello, world!”
}
Here n is interpreted not at the moment of the function creation, but at the moment of the function evaluation. If such a field can not be found, then the error occurs:
class C(f) {
  def evaluate = f()
}

{
  fix x = “world!”
  C(%{n + x}).evaluate  //  ERROR: unknown field or variable n
}
In case when a variable and a field have the same name, this name is interpreted as the variable. If we want to explicitly show that this name is the name of a field, it is necessary to use the operator @:
%{@n + x}

Partial Functions

The underdetermined class PartialFunction is a subclass of Function:
PartialFunction extends Function {
  def isDefinedAt(input)
  def do(input)
}
The basic feature of particular functions is that they can be defined on specific (restricted) value ranges. The method isDefinedAt determines the value range of a partial function. The method do applies the function to arguments. An attempt to apply a partial function to arguments beyond its value domain raises an exception.

The programmer can define her/his own partial functions, for instance,
object oneOrTwo extends PartialFunction {
  def isDefinedAt(n: Int) = n == 1 or n == 2
  def do(n: Int) = if (n == 1) “one” else if (n == 2) “two”
}

oneOrTwo(1)  //  “one”
oneOrTwo(4)  //  ERROR: Argument is out of partial function domain
There is a special notation for defining a unary partial function as a sequence of case-expressions enclosed in curly brackets. For instance, oneOrTwo can be redefined as follows:
{
  fix oneOrTwo = {case 1 => “one”; case 2 => “two”}
  oneOrTwo(1)  //  one
}
For partial functions defined in the ‘case’ notation the abstract methods isDefinedBy and do are determined automatically:
oneOrTwo.isDefinedBy(4)  //  ()
The empty sequence plays in Libretto the role of false. Partial ‘case’ functions are handled in the same way as other anonymous functions:
def ap(f) = f(this)

1.ap(%(x){x + 1})  //  2
1.ap({case 1 => “one”; case 2 => “two”})  //  “one”
The keys of case expressions are compared with actual values by pattern matching (see section Operator match and Pattern Matching). The predefined pattern matching operator match takes as its second argument a partial function:
fix class Person(name: String, hasChild: Person*)
object JOHN extends Person(“John”, Person(“Paul”))

JOHN match {
  case Person(n, ch*) if ch => “{n} has children”!
  case Person(n, _) => “{n} does not have children”!
}
  //  “John has children”
The expression fix class Person(...) defines an immutable structure – a special class similar to algebraic types in functional programming (see section Immutable Structures).

Syntactic Sugar for Functions

Libretto provides a lot of syntactic sugar for functions.

Operators

A collection of prefix, infix and postfix operators like +, -, *, ! is predefined in Libretto. The programmer can also define her/his own operators. This feature is very useful for domain specific language development. The functions defined as operators depend not only on arguments, but also on the context. In particular, this means that an infix operator can depend on the context as the third implicit argument represented by the keyword this. For instance,
def (x)++(y) = if (this) this + x + y else x + y

1.(2++3)  //  6
2++3  //  5
The arguments of an operator are specified in parentheses. Since the operator ++ is placed between the arguments, it is an infix operator. The arguments can be typed, so the polymorphic definitions of operators are also possible:
def String (x:String)++(y:String) = length + x.length + y.length 
def (x:Int)++(y:Int) = if (this) this + x + y else x + y

100.(20++3)  //  123
“abc”.(“de”++”f”)  // 6
The following rules are applied for operators:

  • The arguments in operator definition headers must be enclosed in parentheses.
  • If a function is defined as a postfix or infix operator, the regular representation is not allowed for it (in particular, the function call ++(20, 3) is incorrect for the operator ++).

The factorial can be defined as a postfix operator:
def (x)!! = if (x == 1) 1 else (x - 1)!! * x
5!!   //  120 
A call of a unary function can be represented in the prefix form:
def double(n) = n * 2

double 2  //  4
Another example is a predefined pair operator, x->y, which

  • adds a new key/value pair to an external field, if it is evaluated in the context of this external field (see section External Fields); 
  • returns the pair as the list [x,y], otherwise. 

This operator can be defined as follows:
def Property (x)->(y) {x.@this = y}
def (x)->(y) = [x,y]
Thanks to this polymorphic definition we can provide the different behavior of the operator -> in different contexts. Now,
var Int map: String {
  1 -> “one”
  2 -> “two”
}
1.map  //  “one”
1 -> “one”  //  [1, “one”]
Here [1, “one”] is a list (see section Class List).

Class constructors also can be defined as operators:
class (from)-->(to)
def (a1: -->) + (a2: -->) = (a1.from + a2.from) --> (a1.to + a2.to)

(1 --> “a”) + (2 --> “b”)  //  3 --> “ab”

% in Formal Parameters

A bit of syntactic sugar using % and providing a more compact representation of anonymous functions as parameters is defined in Libretto. For instance,
def f(%fun) = fun()
1.f(this + 1)  //  2
is equivalent to
def f(fun: Function) = fun()
1.f(%{this + 1})  //  2 

Underscored Variables as Arguments

This syntactic sugar provides the compact definitions of anonymous functions with arguments: variables in the body of an anonymous function, whose names start with the underscore, are interpreted as function arguments ordered by their first occurrence in the body. For instance,
%{_a + _b * _a}   is equivalent to  %(_a, _b){_a + _b * _a}
This method can be used for passing parameters:
def f(%fun) = fun(2,3)
f(_a + _b * _a)  //  8
As a more detailed example let us consider a program, which implements a tree with integers as leaves. The following methods are defined on integer trees: leaves counting, getting the minimum and the maximum leaf of a tree, summing up leaves, and checking if an integer is among the leaves of a tree. This program is written in a functional style and based on the higher order function treeFold:
class Tree
fix class Branch(left: Tree, right: Tree) extends Tree
fix class Leaf(n: Int) extends Tree

def Tree treeFold(%bf, %lf) {
  case Leaf(n) => n.lf()
  case Branch(l, r) => l.treeFold(bf,lf).bf(r.treeFold(bf,lf))
}
def Tree countLeaves = treeFold($ + _a, 1) 
def Tree minValue = treeFold(min($, _a), $)
def Tree maxValue = treeFold(max($, _a), $)
def Tree sumValue = treeFold($ + _a, $)
def Tree contains(x) = treeFold($ or _a, x == $)
The construct fix class Branch(...) defines an immutable structure (see section Immutable Structures). The function treeFold, receives as its context a tree node and handles this node either with the parameterized function bf – if the node belongs to the class Branch, – or with the parameterized function lf – if the node belongs to the class Leaf. treeFold is defined as a partial function. $ works with context values in paths (see section Pervasive Context).

Exclamation Mark

To denote the application of nullary and unary functions we can use the exclamation mark. Let us define
def plus1 = this + 1
def plus(n) = this + n
Now, the expression
1.plus1!  is equivalent to   1.plus1()
and the expression
“abc”.plus!“xyz”  is equivalent to   “abc”.plus(“xyz”)
The exclamation mark is convenient for parametric strings (which behave in Libretto as nullary and unary anonymous functions):
“abc{1+2}”!   equals   “abc{1+2}”()
and with functional parameters:
def app(%fun, x) = fun!x
app(_a + 1, 5)  //  6
Note that the exclamation mark can be defined as a regular operator:
def (x)! = x()
def (x)!(y) = x(y)

The Rightmost Argument is a Function

If a function f has a definition with n+1 arguments and does not have a definition with n arguments, then the expression
f(x1,..., xn) {...}
is syntactically equivalent to regular
f(x1, ..., xn, %{...})
In particular, for the function
def app(arg, fun) = fun!arg
the call
app(5) {this + 10}
is interpreted as the call
app(5, %{this + 10})  //  15
Underscored variables can be used to introduce the function parameters:
f(5) {_x + _y}
is equivalent to
f(5, %(_x, _y){_x + _y})
This syntactic sugar does not work with functions, at least one definition of which contains the default value for the rightmost parameter (e.g., def f(x, y = 5) = …).

The Rightmost Argument is a Sequence

If the last argument in a function definition is marked by *, then it is allowed to omit parentheses in the sequence of the last argument. Define, for instance,
def f(n*) = n.size
Then
f(1,2,3,4,5)  is equivalent to  f((1,2,3,4,5))
f()  is equivalent to  f(())
In all other arguments except the rightmost one the parentheses should not be omitted:
def f(m*, n*) = (m.size, n.size)

f((1,2,3),4,5)  //  3 2
f((1,2,3))  //  3 0
f(1,2,3,4,5)  //  1 4
In the last expression the argument m is matched only with 1, all the other numbers are included in the sequence for n.

This bit of syntactic sugar works similarly in class constructors:
class C(n: Int*)
C(1,2,3,4,5).n.size  //  5

Special Methods

The definitions of special methods can be added to class declarations. The aim of special methods in Libretto is to ease object handling, support the development of more compact and readable code, and improve chances for writing bug-free programs. Besides, the special methods play an important role when we use Libretto as a language workbench for the development of various domain specific languages.

In order to distinguish special methods from user-defined ones (for which the camelCasing naming style is recommended), the names of many special methods contain the underscore symbol _.

In Libretto the following special methods are defined:

Getters and Setters

These functions provide advanced control over objects via the introduction of virtual fields. Setters and getters have names, which must start with set_ and get_, respectively. For instance, an age value must be positive integer, so it is reasonable to accompany age assignment with value verification:
class Person {
  private var ageLocal: Int
  def set_age(a: Int) =
    if (a > 0) {ageLocal = a}
      else error(“age value must be positive”)
}
The unary setter set_age defines the virtual field age and assigns it a value. Now:
object JOHN extends Person
JOHN.age = 15
JOHN.age = -3
    //  ERROR: age value must be positive
Also we can define the nullary getter get_age:
class Person {
  ...
  def get_age = ageLocal
}
which provides the access to the value of the virtual field age:
JOHN.age  //  15
The private field ageLocal is not accessible from outside, so age handling is secure:
JOHN.ageLocal  //  ERROR: ageLocal is private in class Person

Method missing_

This method works in situations, when a required field or a nullary function does not exist:
class Number(fix value: Int) {
  def missing_(name: String) = name match {
      case “I” => 1
      case “II” => 2
      case “III” => 3
      case “IV” => 4
  }
}

object Num extends Number(5)
Num.value  //  5
Num.III  //  3
If a required field is not defined on an object, missing_ is invoked in the context of the object with the name of the non-existing field as argument.

Similarly missing_ works with nullary functions:
class C {
  def missing_(name) = if (name==“n”) 5
}

C().n()  //  5

Methods do and undo

The method do is a polymorphic tool intended for the evaluation of expressions and the application of functions to arguments. There are three basic constructs, to which do is applied: objects, anonymous functions and parametric strings.

Application to Objects

Functions are objects, on which the method do is defined. The definition
def f(x: Int) =  x + 1
is syntactic sugar for the following definition:
object f extends Function {
  def do(x: Int) = x + 1
}
A function call with arguments enclosed in parentheses, e.g.
f(5)  //  6
is syntactic sugar for
%f.do(5)  //  6
For nullary and unary functions the exclamation mark can be also used:
f!5  //  6
The method do can be defined on a class, and then all objects of the class become functions:
class C(x: Int) extends Function {
  def do(y: Int) = x * x + y * y
}

{
  fix f = C(3)
  f(4)  //  25
  C(5)(6)  //  61
  C(5)!6  //  61
}

Application to Anonymous Functions

The method do evaluates anonymous functions (see section Anonymous Functions) by applying them to arguments:
{
  fix f = %(x) {x + 1}
  f.do(5)  //  6
  f(5)  //  6
  f!5  // 6

  fix x = 115
  %{x + 1}.do()  //  116
  %{x + 1}()  //  116
  %{x + 1}!  //  116
}
Syntactic sugar (f(x) and f!x instead of f.do(x)) is also valid for anonymous functions.

Anonymous functions can depend on the context:
{
  fix f = %(x) {$ + x}

  (1,2,3).f!5  //  6 7 8
  (“abc”, “bcd”, “cde”) ?[contains(“bc”)]. %{$ + $}!  
                                        //  “abcabc” “bcdbcd” 
}
Consider a more complicated example – a definition of the famous Y-combinator λh.(λf.(f f)) λf.(h λn.((f f) n)), which defines the semantics of recursive functions:
{
  fix Y = %(h){ %(f){f!f} ! %(f){h ! %(n){f!f!n} } }
}
Now using Y the recursive definition of the factorial can be given:
{
  fix fact = %(n){Y ! %(g) {%(n){ if (n < 2) 1 else n * (g(n - 1)) }} }
  fact!5  //  120
}

Application to String Functions

The method do can be also applied to parametric strings (see section Parametric Strings). In strings it evaluates expressions in curly brackets:
«1 + 2 = {1 + 2}».do()  //  “1 + 2 = 3”
In Libretto, a lazy method for parametric string evaluation is implemented: until do is applied to «1 + 2 = {1 + 2}» it stays unchanged.

Similarly to other types of functions, do in parametric strings can be hidden:
«1 + 2 = {1 + 2}»()  //  “1 + 2 = 3”
«1 + 2 = {1 + 2}»!  //  “1 + 2 = 3”
Since parametric strings can be nested, the process of their evaluation also can be nested:
«{“{1+2}”}»  //  “{“{1+2}”}”
«{“{1+2}”}»!  //  “{1+2}”
«{“{1+2}”}»!!  // “3”
The problems with string escaping are major causes of vulnerabilities in web programming. Thus, convenient string escaping is important for the development of script applications. The method do together with parametric strings provides such a tool in Libretto. For instance, let xmlEsc be an escaping function for XML-documents:
“<b>ccc</b>”. xmlEsc  //  “<b>ccc</b>”
Then we can apply it as follows:
“<a>{«<b>ccc</b>»}</a>”!xmlEsc  //  “<a><b>ccc</b></a>”
Here the argument of the parametric string is an escaping function used for postprocessing values in curly brackets. This provides convenient and secure string processing. Compare the last expression with
“<a>{«<b>ccc</b>»}</a>”!  //  <a><b>ccc</b></a>

Passing Messages to Processes

The method do is also used for sending messages to processes in actor models (see section Actor Model).

Method undo

The method undo is interpreted in Libretto as a function inverse to a given function. This method is often called a decomposer because having taken a structure as input it should return the components, from which this structure is composed. In particular, decomposers are used for pattern matching (see section Operator match and Pattern Matching): a matched structure is decomposed into components, and then these components are compared with the pattern.

In Libretto, immutable structures (similar to algebraic types in functional programming) can be defined. For immutable structures Libretto offers special fix-class syntactic sugar (see section Immutable Structures). For instance,
fix class C(n)
For immutable structures the method undo is defined automatically – the above declaration is syntactic sugar for
class C(fix n)
def %C undo(str: C) = str.n
Thanks to undo, pattern matching is applicable to the objects of the class C:
C(5) match {
  case C(x) => x.print()
}
  //  5
The programmer can give her/his own definitions of undo bearing the full responsibility for its behavior. For instance, we can define a ‘pseudoclass’ Person, in which the role of objects is played by strings.
object Person {
  def do(nm: String, snm: String) = “{nm} {snm}”!
  def undo(person: String) = person.split(“ ”)
}

def getName(person: String) = person match {
  case Person(name, _) => name
  case _ => ()
}
The ‘objects’ of the ‘pseudoclass’ Person are strings containing names and surnames separated by whitespace. The method do composes a name and a surname into a string, and the method undo decomposes this string back to the pair of these name and surname (by applying the predefined function split). For matching the string with the pattern Person(name, _), undo is applied and then the obtained pair is associated with the variables of the pattern. For instance
{
  fix john = Person(“John”, “Smith”)

  john // “John Smith”
  getName(john)  //  “John”
  getName(“John”)  //  ()
  getName(Person(“Tom”, “Hughes”))  //  Tom
}
Matching can be used in assignments (see section Assignment with Matching):
{
  fix Person(nm, sn) = john
  nm  //  “John”
  sn  //  “Smith”

  fix Person(_, sn) = “Tom Hughes”
  sn  //  Hughes
  Person(_, sn) = “hehe”    // ERROR: matching failed in assignment
}
If matching is successful, then the variables are assigned the matched strings. An attempt to assign unmatchable values (“hehe”) raises an error.

Methods next_ and nextSeq_

These methods provide the rules for handling the instances of a given class in the context of other objects. We can say that these methods modify the behavior of the dot operator. If in a path A.B the types of A and B match with the types of the context and the first argument of some definition of next_, respectively, then the evaluation of A.B turns into the evaluation of A.next_(B). For instance,
def String next_(s: String) = this + s
Now “aaa”.“bbb” is equal to “aaabbb”. One more example:
class Person {
  fix name: String
  var age: Int

  def next_(s: String) {name = s}
  def next_(n: Int) {age = n}
}
Now
Person().“John”.16
is equivalent to
Person() {name = “John”; age = 16}
Note that the function next_ defined on Person always returns its context value (this is because all assignment operators in Libretto except the deletion operator -- return their context value, see sections starting from Assignment Operators). This feature allows ‘pipeline’ data processing in multistep paths. It is very useful for the development of user-defined domain specific languages based on Libretto.

For applicability of next_, the explicit occurrence of the dot operator is not necessary. The method is applied whenever an object appears in the context corresponding to the signature of some definition of next_. For instance, Person()."John".16 can be rewritten as
Person() {"John"; 16}
Here string "John" and integer 16 appear in the block evaluated in the context of a Person’s instance. The only difference is that the result of the last expression is 16. The following expression is fully equivalent to Person()."John".16:
Person() {"John"; 16; $}
Unlike next_, which processes the context sequence element by element, the method nextSeq_ handles the context collection as a whole.

The methods next_ and nextSeq_ are useful for customizing the Libretto syntax in DSL development.

Method dot_

dot_ is another method responsible for modification of the dot operator behavior. The method dot_, if defined on some class, allows the instances of this class to be interpreted as collections iterated by the dot operator. For instance,
class Range(min: Int, max: Int) {
  def dot_(fun) {
    var res
    min to max as n. (res += n.fun!)
    res
  }
}
The instances of the class Range produce the sequence of integers ranging from min to max. When dot_ is called, its argument fun takes the rest of the path in the form of an anonymous function. This function is evaluated by dot_ in the context of each value of the collection. For instance,
Range(2, 4) as m. Range(100, 102). (m * $) 
  //  200 202 204 300 303 306 400 404 408
Unlike next_, which depends on the types of both the context and the value, dot_ depends only on the type of the context. If both dot_ and next_ are defined on a class, then next_ has priority over dot_ and is tried first. This approach allows us to use next_ for defining exceptions for the method dot_.

Special Methods Defined Externally

Special methods can be defined not only within class or object definitions, but also as external methods. This allows the programmer to avoid metaprogramming in many situations. Any special method can be defined as external: undo, missing_, next_, etc. For instance,
class C(var n: Int)
def C next_(x: Int) {n = x}

C(5).115.n  //  115
In this example, the class C is defined, and then separately the function next_ is introduced as an external method for C. In the query, an object of the class C is created, in which the field n takes the value of 5. Then the field n is assigned 115.

Objects also have a similar opportunity:
def twice(x) = x * 2
def %twice undo(x) = if (x mod 2 == 0) x div 2
Here the special method undo, which is defined externally, represents the inverse function of twice:
42 match { case twice(x) => x }  //  21 
This query returns the solution of the ‘equation’ twice(x) == 42.

Nested Functions

Functions can be introduced within the definitions of other functions. The scope of a nested function is limited to the body of the external function. For instance,
def g(x) {
  def f(y) = x + y
  f(3)
}
g(4)  //  7
Here the function f is defined and accessible only in the body of g. The variables of the external function are visible in the body of the nested function (x in the example).

Nested functions allow the programmer to conceal data, divide the tasks into subtasks, and operate economically with namespaces.

Nested functions are not class methods, so the programmer can specify their context type explicitly (as in external methods). In the example above, the context of f is typed by default as Any. Here is a nested function with the explicitly typed context:
def g(x) {
  def Int f = this + x
  def String f = “{x} and {this}”!

  (5.f, “5”.f)
}

g(2)  //  7  “2 and 5”
Thus, nested functions also allow polymorphic definitions.

Nested functions overshadow the access to external functions with the same name:
def f(y) = y + 100
def g(x) {
  def f(y) = x + y
  f(3)
}
g(4)  //  7
f(1)  //  101

User-defined Keywords

A user-defined keyword is a function associated with an entity definition. Such a keyword provides implicit data processing relevant to this entity. Syntactically, user-defined keywords occur before the entity definitions they mark. For instance,
def add1(varName) = this + varName

{
  add1 var x = “jo”
  x  //  “jox”
  x = “ko”
  x  //  “kox”

  add1 var y = “a”
  y  //  “ay”
}
The function add1 is used as a keyword for the variables x and y. Its only argument is the name of the marked variable. The context of the function is the value assigned to the variable. The function add1 is invoked as a preprocessor before each assignment. E.g. it substitutes the assigned value “jo” with “jo”.add1(“x”).

User-defined keywords can be applied not only to variables, but also to all entities of Libretto including packages, classes, fields, objects and functions. The semantics of user-defined keywords is defined as follows.

If a user-defined keyword is applied to a variable, then it is invoked each time, when the variable is modified, with the new value of the variable as its context and the variable’s name as its first argument.

An example of a variable keyword is shown above.

If a user-defined keyword is applied to a field, then it is invoked each time, when the field is modified – in the context of the new value of the field, with the object, to which the field belongs, as the first argument, and the field object as the second one. The field is assigned the value returned by the keyword function.

For instance, the assignment in the object cc
def fun(obj, field) = ...

class C {
  fun var field
}
object cc extends C()

cc.field = v
is performed as if the following code is executed:
cc.field = v.fun(cc, %field)
If a user-defined keyword is applied to an entity different from a field, it is invoked on the load of the package in which the entity is defined.

For instance,
package A {
  def fun() = ...
  ...
  fun class C
  ...
}
is equivalent to
package P {
  def fun() = ...
  ...
  class C
  ...
  %C.fun()
}
In this example the function fun can contain programming code for the modification of C (see section Metaprogramming).

Functions that are used as keywords can contain additional arguments. In the following example each new value assigned to a field is stored in a text file:
def dump(obj, prop, filename) {
  fix file
  try {
    file = filename.open()
    file.add(“{obj} / {this}\n”!)
  }
  catch {case _ => ()}
  finally {file.close()}

  this
}
Introduce new fields:
class C {
  dump(“x.txt”) var x
  dump(“y.txt”) fix y
}
object c extends C()
For each assignment, e.g.
c.x = 5
actually the following code is performed
c.x = 5.dump(c, %x, “x.txt”)
that is, the file x.txt is augmented with new line “c / 5”.

User-defined keywords resemble annotations in Java. Besides, user-defined keywords provide the possibility for advanced control over program execution. For instance, ref in the STM library of Libretto (see section Multitasking) is defined as a user-defined keyword. User-defined keywords significantly improve the expressiveness of Libretto as a language workbench as well. They are also good for efficient debugging.

No comments:

Post a Comment