
Chapter 5 Expressions and values
omake provides a full programming-language including many
system and IO functions. The language is object-oriented – everything is
an object, including the base values like numbers and strings. However,
the omake language differs from other scripting languages in
three main respects.
-
Scoping is dynamic.
- Apart from IO, the language is entirely functional – there is no
assignment operator in the language.
- Evaluation is normally eager – that is, expressions are evaluated as soon
as they are encountered.
To illustrate these features, we will use the osh(1) omake program shell.
The osh(1) program provides a toploop, where expressions can be entered
and the result printed. osh(1) normally interprets input as command text
to be executed by the shell, so in many cases we will use the value
form to evaluate an expression directly.
osh> 1
*** omake error: File -: line 1, characters 0-1 command not found: 1
osh> value 1
- : "1" : Sequence
osh> ls -l omake
-rwxrwxr-x 1 jyh jyh 1662189 Aug 25 10:24 omake*
5.1 Dynamic scoping
Dynamic scoping means that the value of a variable is determined by the most
recent binding of the variable in scope at runtime. Consider the following
program.
OPTIONS = a b c
f() =
println(OPTIONS = $(OPTIONS))
g() =
OPTIONS = d e f
f()
If f()
is called without redefining the OPTIONS
variable,
the function should print the string OPTIONS = a b c
.
In contrast, the function g()
redefines the OPTIONS
variable and evaluates f()
in that scope, which now prints the
string OPTIONS = d e f
.
The body of g
defines a local scope – the redefinition of the
OPTIONS
variable is local to g
and does not persist
after the function terminates.
osh> g()
OPTIONS = d e f
osh> f()
OPTIONS = a b c
Dynamic scoping can be tremendously helpful for simplifying the code
in a project. For example, the OMakeroot file defines a set of
functions and rules for building projects using such variables as
CC
, CFLAGS
, etc. However, different parts of a project
may need different values for these variables. For example, we may
have a subdirectory called opt
where we want to use the
-03
option, and a subdirectory called debug
where we
want to use the -g
option. Dynamic scoping allows us to redefine
these variables in the parts of the project without having to
redefine the functions that use them.
section
CFLAGS = -O3
.SUBDIRS: opt
section
CFLAGS = -g
.SUBDIRS: debug
However, dynamic scoping also has drawbacks. First, it can become
confusing: you might have a variable that is intended to be private,
but it is accidentally redefined elsewhere. For example, you might
have the following code to construct search paths.
PATHSEP = :
make-path(dirs) =
return $(concat $(PATHSEP), $(dirs))
make-path(/bin /usr/bin /usr/X11R6/bin)
- : "/bin:/usr/bin:/usr/X11R6/bin" : String
However, elsewhere in the project, the PATHSEP
variable is
redefined as a directory separator /
, and your function
suddenly returns the string /bin//usr/bin//usr/X11R6/bin
,
obviously not what you want.
The private
block is used to solve this problem. Variables
that are defined in a private
block use static scoping – that
is, the value of the variable is determined by the most recent
definition in scope in the source text.
private
PATHSEP = :
make-path(dirs) =
return $(concat $(PATHSEP), $(dirs))
PATHSEP = /
make-path(/bin /usr/bin /usr/X11R6/bin)
- : "/bin:/usr/bin:/usr/X11R6/bin" : String
5.2 Functional evaluation
Apart from I/O, omake programs are entirely functional. This has two parts:
-
There is no assignment operator.
- Functions are values, and may be passed as arguments, and returned from
functions just like any other value.
The second item is straightforward. For example, the following program defines
an increment function by returning a function value.
incby(n) =
g(i) =
return $(add $(i), $(n))
return $(g)
f = $(incby 5)
value $(f 3)
- : 8 : Int
The first item may be the most confusing initially. Without assignment, how is
it possible for a subproject to modify the global behavior of the project? In fact,
the omission is intentional. Build scripts are much easier to write when there
is a guarantee that subprojects do not interfere with one another.
However, there are times when a subproject needs to propagate
information back to its parent object, or when an inner scope needs to
propagate information back to the outer scope.
The export
directive can be used to propagate all or part of an
inner scope back to its parent. The export
directive should be
the last statement in a block. If used without arguments, the entire
scope is propagated back to the parent; otherwise the arguments should
be the names of variables to propagate. The most common usage is
to export the definitions in a conditional block. In the following
example, the variable B
is bound to 2 after the conditional.
The A
variable is not redefined.
if $(test)
A = 1
B = $(add $(A), 1)
export B
else
B = 2
export
5.3 Eager evaluation
Evaluation in omake is eager. That is, expressions are evaluated as soon as they are
encountered by the evaluator. One effect of this is that the right-hand-side of a variable
definition is expanded when the variable is defined.
osh> A = 1
- : "1"
osh> A = $(A)$(A)
- : "11"
In the second definition, A = $(A)$(A)
, the right-hand-side is evaluated first, producing the
sequence 11
. Then the variable A
is redefined as the new value. When combined
with dynamic scoping, this has many of the same properties as conventional imperative programming.
osh> A = 1
- : "1"
osh> printA() =
println($"A = $A")
osh> A = $(A)$(A)
- : "11"
osh> printA()
11
In this example, the print function is defined in the scope of A
. When it is called on
the last line, the dynamic value of A
is 11
, which is what is printed.
However, dynamic scoping and imperative programming should not be confused. The following example
illustrates a difference. The second printA
is not in the scope of the definition
A = x$(A)$(A)x
, so it prints the original value, 1
.
osh> A = 1
- : "1"
osh> printA() =
println($"A = $A")
osh> section
A = x$(A)$(A)x
printA()
x11x
osh> printA()
1
See also Section 6.5 for further ways to control the evaluation order through the use
of “lazy” expressions.
5.4 Objects
omake is an object-oriented language. Everything is an object, including
base values like numbers and strings. In many projects, this may not be so apparent
because most evaluation occurs in the default toplevel object, the Pervasives
object, and few other objects are ever defined.
However, objects provide additional means for data structuring, and in some cases
judicious use of objects may simplify your project.
Objects are defined with the following syntax. This defines name
to be an object with several methods an values.
name. = # += may be used as well
extends parent-object # optional
class class-name # optional
# Fields
X = value
Y = value
# Methods
f(args) =
body
g(arg) =
body
An extends
directive specifies that this object inherits from
the specified parent-object
. The object may have any number of
extends
directives. If there is more than on extends
directive, then fields and methods are inherited from all parent
objects. If there are name conflicts, the later definitions override
the earlier definitions.
The class
directive is optional. If specified, it defines a name
for the object that can be used in instanceof
operations, as well
as ::
scoping directives discussed below.
The body of the object is actually an arbitrary program. The
variables defined in the body of the object become its fields, and the
functions defined in the body become its methods.
5.5 Field and method calls
The fields and methods of an object are named using object.name
notation.
For example, let's define a one-dimensional point value.
Point. =
class Point
# Default value
x = $(int 0)
# Create a new point
new(x) =
x = $(int $(x))
return $(this)
# Move by one
move() =
x = $(add $(x), 1)
return $(this)
osh> p1 = $(Point.new 15)
osh> value $(p1.x)
- : 15 : Int
osh> p2 = $(p1.move)
osh> value $(p2.x)
- : 16 : Int
The $(this)
variable always represents the current object.
The expression $(p1.x)
fetches the value of the x
field
in the p1
object. The expression $(Point.new 15)
represents a method call to the new
method of the Point
object, which returns a new object with 15 as its initial value. The
expression $(p1.move)
is also a method call, which returns a
new object at position 16.
Note that objects are functional — it is not possible to modify the fields
or methods of an existing object in place. Thus, the new
and move
methods return new objects.
5.6 Method override
Suppose we wish to create a new object that moves by 2 units, instead of
just 1. We can do it by overriding the move
method.
Point2. =
extends $(Point)
# Override the move method
move() =
x = $(add $(x), 2)
return $(this)
osh> p2 = $(Point2.new 15)
osh> p3 = $(p2.move)
osh> value $(p3.x)
- : 17 : Int
However, by doing this, we have completely replaced the old move
method.
5.7 Super calls
Suppose we wish to define a new move
method that just calls the old one twice.
We can refer to the old definition of move using a super call, which uses the notation
$(classname::name <args>)
. The classname
should be the name of the
superclass, and name
the field or method to be referenced. An alternative
way of defining the Point2
object is then as follows.
Point2. =
extends $(Point)
# Call the old method twice
move() =
this = $(Point::move)
return $(Point::move)
Note that the first call to $(Point::move)
redefines the
current object (the this
variable). This is because the method
returns a new object, which is re-used for the second call.