In this post we consider and discuss functions in Libretto.
A function can return not only a single value but also ordered collections, for instance,
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
We can redefine
A function in Libretto can depend on the context and arguments simultaneously:
Function arguments also depend on the context:
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, ‘
The semantics of paths is heavily based on the context (see section Paths and Steps). A special symbol
On the outer level of function bodies the values of ‘
Thus, using
The function
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:
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).
A type in Libretto is defined as a pair class/cardinality (cardinality is a maximum allowed number of elements). In the following example
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,
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
When
Erroneous data in the context can be also handled by local traps (see section Local Exception Handling):
So, the most appropriate definition has the least signature among all definitions applicable to the call. For instance,
For instance, Libretto allows the following type-free definition:
The same situation is with class constructors (see section Constructors and Factory Methods). We can define a type-free constructor
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:
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
For instance, a resource can be an instance of the class
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
A version with a preliminary check (‘ask for permission’) corresponds to the following Libretto code:
The next Libretto query follows the EAFP principle. To handle the problem it uses a local trap (see section Local Exception Handling):
An external method definition is based on the context typing declaration (
The polymorphic definitions of external methods are also possible:
The basic characteristic of external methods is that their behavior is not distinguishable from that of in-class methods:
If two methods, in-class and external, are equally applicable, then the priority is granted to the external method:
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.
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 the definition of
The predefined function
The behavior of element functions and collection functions is dramatically different on the empty context:
The actual arguments of a call of a collection function are also evaluated in the context of the whole context sequence, for instance,
In Libretto the following rule is applied, which determines the semantics of paths (see section Paths and Steps):
That is, only collection function calls are allowed to be evaluated in the empty context:
Now consider another example – a so-called
These definitions of the set operations are not very efficient, but correct:
For instance,
The name
For instance,
In nullary anonymous functions the empty list of arguments can be omitted:
The programmer can define her/his own partial functions, for instance,
The factorial can be defined as a postfix operator:
This operator can be defined as follows:
Class constructors also can be defined as operators:
A bit of syntactic sugar using % and providing a more compact representation of anonymous functions as parameters is defined in Libretto. For instance,
This bit of syntactic sugar works similarly in class constructors:
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:
Method
This method works in situations, when a required field or a nullary function does not exist:
Similarly
Methods
The method
Anonymous functions can depend on the context:
Similarly to other types of functions,
In Libretto, immutable structures (similar to algebraic types in functional programming) can be defined. For immutable structures Libretto offers special
Methods
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
For applicability of
The methods
Objects also have a similar opportunity:
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
Nested functions overshadow the access to external functions with the same name:
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.
An example of a variable keyword is shown above.
For instance, the assignment in the object
For instance,
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:
User-defined keywords resemble annotations in Java. Besides, user-defined keywords provide the possibility for advanced control over program execution. For instance,
- Function Definitions
- Pervasive Context
- Nullary Function Definition
- Default Parameters
- Explicit Parameter Passing
- Context, Parameter and Field Typing
- Functions as Methods
- Polymorphic Methods
- Parameter Polymorphism
- Dynamic Dispatch
- Polymorphic Definitions vs Typeless Definitions in Libretto
- Duck Typing
- External Methods
- External Methods on Objects
- The Import of External Methods
- Collection Functions
- Functions as Objects
- Anonymous Functions
- Closures
- Partial Functions
- Syntactic Sugar for Functions
- Special Methods
- Special Methods Defined Externally
- Nested Functions
- User-defined Keywords
Function Definitions
A named function definition starts with the keyworddef
. 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) // 120The 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 125In 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” // 10000The 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 definedef plus1() = this + 1This 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 + 1The 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) // 10The 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 // 9This 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)) // 7The 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 // 6The 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) // 3The 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 4The 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 120and the function
sum
is a collection function:
def * sum { var x = 0 this. (x = x + $) x } (1,2,3,4,5).sum // 15The 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 + 1In 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).factthe 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 // 2Parenthesis-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) // 111To 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 offact
as Int
:
def Int fact = if (this == 0) 1 else this * (this - 1).factLibretto 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 xFor 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 fWhile 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 // 5The 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 1Methods 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 // 10Since 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 // 10The 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.56We can check how
square
works on erroneous data:
GeomFigure().square // ERROR: Method square is abstract 5.square // ERROR: Only geometric figures have squareIn 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.56For 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 Cls2The function
f
defined on both classes Cls1
and Cls2
is polymorphic by context:
c1.f // “This is Cls1” c2.f // 115The 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 pairCi
andDi
eitherCi
equalsDi
, orCi
inherits fromDi
. - 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()) // 2The 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) = 2When the most appropriate definition can be found, the computation is successful:
f(C1(), D2()) // 1 f(C2(), D1()) // 2But
f(C2(), D2()) // ERROR: ambiguous definitions for function fThe 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 onInheritance 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 + 5Now:
1.plus5 // 6The 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 packagep1
:
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 4Here for each call of
plus1
‘this
’ 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 // 15uses 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 // 15Since
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 // 5Here
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 // 0The 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) // 9the 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 AnyHere
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 3In 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 4For 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).factNow
%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 // 120Thus, 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) // 6An 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 classPartialFunction
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 domainThere 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 // 5The 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”) // 6The 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!! // 120A call of a unary function can be represented in the prefix form:
def double(n) = n * 2 double 2 // 4Another 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) // 2is 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) // 8As 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 definedef plus1 = this + 1 def plus(n) = this + nNow, 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) // 6Note 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 functionf
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!argthe call
app(5) {this + 10}is interpreted as the call
app(5, %{this + 10}) // 15Underscored 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.sizeThen
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 4In 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 withset_
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 positiveAlso 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 // 15The 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 // 3If 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 methoddo
is defined. The definition
def f(x: Int) = x + 1is 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) // 6is syntactic sugar for
%f.do(5) // 6For nullary and unary functions the exclamation mark can be also used:
f!5 // 6The 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 methoddo
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 methoddo
is also used for sending messages to processes in actor models (see section Actor Model).
Method undo
The methodundo
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.nThanks to
undo
, pattern matching is applicable to the objects of the class C
:
C(5) match { case C(x) => x.print() } // 5The 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 + sNow
“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”.16is 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 408Unlike
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 // 115In 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 2Here the special method
undo
, which is defined externally, represents the inverse function of twice
:
42 match { case twice(x) => x } // 21This 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) // 7Here 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 = vis 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 = 5actually 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