Walnut/Ordinary Programming/Objects and Functions

From Erights

Jump to: navigation, search


Contents

Objects and Functions

As noted earlier, if the file is to use the Swing gui toolkit, it must have a suffix of ".e-awt". If the file is to use the SWT gui toolkit, it must have a suffix of ".e-swt". If the file will run headless, it should be placed in a file with suffix ".e".

But first, we need to declare what version of E we're using here.

 ? pragma.syntax("0.9")

Functions

A basic function looks like this:

# E sample
def addNumbers(a,b) {
    return a + b
}
# Now use the function
def answer := addNumbers(3,4)

You can nest the definitions of functions and objects inside other functions and objects, giving you functionality comparable to that of inner classes in Java. Nested functions and objects play a crucial role in E, notably in the construction of objects as described shortly.

A parameterless function must still have an open/close paren pair. Calls to parameterless functions must also include the parens.

Functions can of course call themselves recursively, as in

# E sample
def factorial(n) {
    if (n == 0) {
        return 1
    } else {
        return n * factorial(n-1)
    }
}

Dynamic "type checking" and Guards

E guards perform many of the functions usually thought of as type checking, though they are so flexible, they also work as concise assertions. Guards can be placed on variables, parameters, and return values.

Guards are not checked during compilation. They are checked during execution, and will throw exceptions if the value cannot be coerced to pass the guard. Guards play a key role in protecting the security properties when working with untrusted code, as discussed in Secure Distributed Computing.

The available guards include the items below. Some of them are typical types (String, int). Others are used most often in distributed programming, and are explained later in the book. A detailed explanation of all the things you can do with guards is postponed to the Additional Features chapter.

  • int
  • char
  • float64
  • boolean
  • String
  • void
  • notNull
  • nullOk
  • near
  • vow
  • rcvr
  • pbc
  • Data
  • Java classes
  • E interfaces
  • subranges and more complex expressions

Here are some quick examples of guards being declared:

# E sample
# guarding a variable
def title :String := "abc"
# guarding a parameter, and a return value. Note the guard on the 
# return value is part of the def line for the function.
def reciprocal(x :float64) :float64 {
    return 1 / x
}

Different people use different strategies about how much type checking/guard information to include in their programs. In this book, the general style is to use guards sparingly, as might be typical in a rapid prototyping environment. One place where guards must be used with rigor is in the objects on the boundaries between trust realms, in security aware applications; see the Powerbox pattern in the Secure Distributed Programming section for an important example.

Objects

Objects, and object constructors, look considerably different in E than in Java or C++. We will start our exploration of objects with a simple singleton object.

A Singleton Object

Objects, functions, and variables are defined with the keyword "def"; all of these can be passed as arguments in parameter lists. Methods on an object, in contrast, are defined with the keyword "to":


 # E sample
 def origin {
     to getX() {return 0}
     to getY() {return 0}
 }
 # Now invoke the methods
 def myY := origin.getY()
 

Like functions, methods require a parenthesis pair even if there are no arguments. (But, Python programmers beware, methods are not functions. Methods are just the public hooks to the object that receive messages; functions are standalone objects).

When invoking the method, the object name and the method called are separated by a dot.

Stateful objects and object constructors

The "class" concept in Java is used to achieve multiple goals. In E, these goals are factored out in a different way. For example, Java classes supply a place to put constructors, which have a special syntax unique to constructors. In E, objects are constructed by ordinary functions.


 # E sample
 # Point constructor
 def makePoint(x,y) {
     def point {
         to getX() {return x}
         to getY() {return y}
         to makeOffsetPoint(offsetX, offsetY)  {
             return makePoint(x + offsetX, y + offsetY)
         }
         to makeOffsetPoint(offset) {
             return makePoint(x + offset, y + offset)
         }
     } 
     return point
 }
 # Create a point
 def origin := makePoint(0,0)
 # get the y value of the origin
 def y := origin.getY()

Inside the function makePoint, we define a point and return it. As demonstrated by the makeOffsetPoint method, the function (makePoint) can be referenced from within its own body. Also note that you can overload method names (two versions of makeOffsetPoint) as long as they can be distinguished by the number of parameters they take.

The (x, y) passed into the function are not ephemeral parameters that go out of existence when the function exits. Rather, they are true variables (implicitly declared with "def" ), and they persist as long as any of the objects that use them persist. Since the point uses these variables, x and y will exist as long as the point exists. This saves us the often tedious business in Java of copying the arguments from the parameter list into instance variables: x and y already are instance variables.

We refer to an object-making function such as makePoint as a "Maker". Let us look at a more serious example, with additional instance variables:


 # E sample
 def makeCar(var name) {
     var x := 0
     var y := 0
     def car {
         to moveTo(newX,newY) {
             x := newX
             y := newY
         }
         to getX() {return x}
         to getY() {return y}
         to setName(newName) {name := newName}
         to getName() {return name}
     } 
     return car
 }
 # Now use the makeCar function to make a car, which we will move and print
 def sportsCar := makeCar("Ferrari")
 sportsCar.moveTo(10,20)
 println(`The car ${sportsCar.getName()} is at X location ${sportsCar.getX()}`)
 

Inside the Maker, we create the instance variables for the object being made (x and y in this example), then we create the object (car). Note that the variable "name", passed into the function, is explicitly declared with "var", so that it can be altered later; in this case, it is reassigned in the setName() method.

Self-referencing objects

Sometimes in the body of an object you wish to refer to the object itself. A keyword like "this" is not required. The name given to the object is in scope in the body of the object, so just use it:


 # E sample
 def makeCar(name) {
     var x := 0
     var y := 0
     def car {
         to moveDistance(newX,newY) {car.moveTo(x + newX, y + newY)}
         # ....define other methods including moveTo as above ....
     }
     return car
 }

What if you need to reference the object during the object's construction, i.e., during the creation of the instance variables that precedes the definition of the object itself? In the below example, we give the car a weatherReportRadio that is supposed to alert the car to changing weather conditions. This radio requires, as a parameter during its construction, the car it will be alerting. So the radio needs the car during radio construction, and the car needs the radio during car construction.


 # E sample
 def makeRadio(car) {
     # define radios
 }
 def makeCar(name) { 
     var x := 0 
     var y := 0
     # using def with no assignment
     def car
     def myWeatherRadio := makeRadio(car)
     bind car {
         to receiveWeatherAlert() {
             # ....process the weather report....
             # myWeatherRadio.foo(...)
         } 
         to getX() {return x} 
         to getY() {return y}
         # ....list the rest of the car methods....
     }
     return car
 }

Here, we do a "def" of the car with no assignment, then we use the car, then we do a "bind" of the car which binds a value to the car. This looks and behaves like a "forward reference" from C. Under the covers, the statement "def car" creates a promise for the car, and the bind statement fulfills the promise. We discuss promises in greater depth in the Distributed Computing chapter, where they play a key role.

Secret Life of Functions, Multiple Constructors and "Static Methods"

Before we can talk about multiple constructors and the static-method-like behavior in E, it is time to reveal the truth about E functions. They are in fact simple objects with a single method, the "run" method, that is invoked by default if no other method is explicitly designated. For example, the following square function


 # E sample
 def square(x) {return x*x}

is really syntactic shorthand for


 # E sample
 def square {
     to run(x) {return x*x}
 }

In the second one, the run() method is explicitly defined. Using this explicit form in a Maker function, you can define multiple constructors, discriminated by the number of parameters they receive. Similarly, by adding methods to the Maker other than "run" methods, you get other "static methods". In the example below, we have a queue Maker with 2 constructors and a non-constructor method. The one-argument constructor requires an initial capacity; the no-argument constructor supplies an initial capacity of 10.


 # E sample
 def makeQueue {
    to run(initialCapacity) {
        # ....create a queue object with the specified initial capacity....
    }
    to run() {return makeQueue(makeQueue.getDEFAULT_CAPACITY())}
    to getDEFAULT_CAPACITY() {return 10}
 }
 # Now use both constructors
 def queue1 := makeQueue(100)
 def queue2 := makeQueue()
 println(`default capacity is: ${makeQueue.getDEFAULT_CAPACITY()}`)

Note also that one can use methods such as getDEFAULT_CAPACITY() to achieve the same effect as Java achieves with public static final variables.

Polymorphism

E enables polymorphism through method name matching. In this example, we move both a car and an airplane to a new location using a single common piece of code:


 # E sample
 def makeCar(name) {
     def car {
         to moveTo(x, y) {
              # move the car
         }
         # other methods
     }
     return car
 }
 def makeJet(name) {
     def jet { 
         to moveTo(x, y) { 
             # move the jet, very different code from the code for car 
         } 
         # other methods 
    } 
    return jet
 }
 def vehicles := [makeCar("car"), makeJet("jet")]
 for each in vehicles {
     each.moveTo(3,4)
 }       

Extends and Delegation

An object can refer calls to itself to another object (a "super-object", built by the "super-constructor", faintly similar to the way one uses superclasses in Java) using the extends keyword: think of better example


 ? def makePoint(x, y) { 
 >     def point { 
 >         to getX() {return x} 
 >         to getY() {return y}
 >         to getMagnitude() {return (x*x + y*y)**0.5} 
 >     }
 >     return point
 > } 
 # value: <makePoint>  
  
 ? def makePoint3D(x,y,z) { 
 >     def point3D extends makePoint(x,y) { 
 >         to getZ() {return z} 
 >         to getMagnitude() {
 >             def xy := super.getMagnitude()
 >             return (z*z + xy*xy)**0.5
 >         } 
 >     }
 >     return point3D 
 > } 
 # value: <makePoint3D>
 
 ? def place := makePoint3D(1,2,3)
 # value: <point3D>
 
 ? def x := place.getX()
 # value: 1
 
 ? def z := place.getZ()
 # value: 3

Here makePoint3D acts as an extension of makePoint. Inside the extends clause, we run the makePoint constructor to create the Point upon which the Point3D will be built. This specially constructed point is referred to in the point3D definition as "super"; you can see super being used in the getMagnitude method.

Using extends as described above follows the delegation pattern. Delegation is a simple pattern of object composition in which an object says, "if another object calls me with a method that I don't have defined here, just route the message to this other object, and send the result back to the caller." Delegation is similar to inheritance, but it is conceptually simpler.

Extends and Full Inheritance

Some experts consider inheritance to be a dangerous feature. A discussion of the pros and cons of inheritance is beyond the scope of this book. However, none of the full-blown examples in this book actually use full inheritance. Delegation, via the simple extends keyword plus polymorphism serve most of the purposes for which inheritance was intended, and is used everywhere here.

Having said that, there are times and places where full inheritance is the right answer. The extends keyword is also used, with one additional convention, to support such full inheritance. In inheritance, the "self" to which a super-object refers is the sub-object of which it is a part, so that the super-object can use methods in the sub-object:


 # E sample 
 def makeVehicle(self) {
     def vehicle {
         to milesTillEmpty() {
             return self.milesPerGallon() * self.getFuelRemaining()
         }
     }
     return vehicle
 }
 
 def makeCar() {
     var fuelRemaining := 20
     def car extends makeVehicle(car) {
         to milesPerGallon() {return 19}
         to getFuelRemaining() {return fuelRemaining}
     }
     return car
 }
 
 def makeJet() {
     var fuelRemaining := 2000
     def jet extends makeVehicle(jet) {
         to milesPerGallon() {return 2}
         to getFuelRemaining() {return fuelRemaining}
     }
     return jet
 }
 
 def car := makeCar()
 println(`The car can go ${car.milesTillEmpty()} miles.`)
     

As seen in the example, the super-object constructor specifies a parameter, "self", which can be used to refer to the sub-object. The sub-object includes itself in the the call to the super-object constructor, thus becoming "self".

E Interfaces

You can define an interface like this:

interface Foo {
}

def foo implements Foo {
}

foo :Foo

The interface body can contain method signatures (without bodies).

When an object is declared as "implements Foo", Foo acts as a "rubber-stamping" auditor, which marks the object as implementing the interface (whether or not it implements any of the methods defined in the interface). When Foo is used as a guard, it checks that the object was stamped by the auditor.

This allows your module to give out objects and recognise them when they are given back later. However, this cannot be used for security purposes because the interface (Foo) cannot be kept secret. This is because when a non-Foo object is checked with the Foo guard, object.__conformTo(Foo) is called.

The two functions of Foo (guard and auditor) may be split up using this syntax:

interface FooGuard guards FooAuditor { }

def foo implements FooAuditor { ... }

foo :FooGuard

This allows the guard to be given out freely while the auditor is closely held, preventing others from creating fake Foo objects but allowing anyone to check that a given object is a genuine Foo.

Presumably, you could also implement a public auditor that actually performed some kind of check on an object, and then used the rubber-stamping auditor to stamp it if it passed the checks [how?].

The interface construct is likely to change in future[1].

General method name matching and E.call()

Delegation with extends does a straight pass-through of the method call. It is possible to intercept collections of calls before delegating them, using match. In this example, we count the number of calls made to a car:


 # E sample
 def makeCalledCountCar(name) {
     def myCar := makeCar(name) 
     var myCallCount := 0
     def car {
         to getCallCount() {return myCallCount}
         match [verb,args] {
             myCallCount += 1
             E.call(myCar,verb,args)
         }
     }
     return car
 }           

In "match[verb,args]", the verb is a string specifying the name of the method being called, and the args is a list of arguments. If the car is called with a method that is not defined in a "to" statement, the name of the method and the arguments are bound to the variables inside the square brackets. We then increment the call count, and finally forward the message using the E.call function. E.call(...) takes as arguments the target object for the action, the name of the method to call, and the list of arguments for the method to invoke. You can manually use E.call() like this:


 # E syntax
 def car := makeCar("mercedes")
 E.call(car, "moveTo", [3,4])
 # which is equivalent to
 car.moveTo(3,4)

In this example, the elements for the argument list are placed in square brackets to form an E ConstList, described later in this chapter.

The match construct is useful for making "adapters" when interfacing to Java. Here we build a singleton object that can be used anywhere a java.awt.event.MouseListener would be used; the match clause is used to absorb and discard events in which we are not interested:


 # E sample
 def mouseListener {
     to mouseClicked(event) {
         # process the event
     }
     match [verb,args] {}
 }

This general purpose matching is also useful in security applications when building facets and revocable capabilities, as described in the chapter on Secure Distributed Computing.

Edoc, the E equivalent of JavaDoc

E supports the construction of javadoc-style comments that can be postprocessed into HTML documentation. For this purpose, the "/** ... */" comment format has been specially reserved; comments of this style can only appear in front of a function/object definition (a "def" statement), or in front of an object method definition (a "to" statement):


 # E sample
 /**
  * Add 2 numbers together.
  * <p>
  * Currently works with any objects that support the "plus" method
  *
  * @param a the first number. 
  * @param b the second number
  * @return  the result of adding. 
  */
 def adder(a, b) {return a + b}

For more information on how to generate Edoc HTML from your E programs, see the Additional Features.

Under the covers: Everything is an Object

We have already discussed the fact that functions are really objects with a single method, "run". Functions-as-objects have one practical consequence that would be surprising in the absence of understanding. You cannot create two functions with different numbers of parameters and have them overloaded:


 # E syntax
 def times2(a) {return a * 2}
 def compute(a,b) {
     def times2(c,d) {return (c + d) * 2}
     # ....do computation...
     # The following line will throw an exception
     def answer := times2(a)
     return answer
 }

This would throw an exception because the inner definition of the function-like object times2 completely shadows the outer times2.

Not only functions, but also built-in constants like integers and floats, are objects. Operators such as "+" are really shorthands for message passing: 3.add(4) is identical to 3 + 4. This is why the operation

"The number is " + 1

works but

1 + " is the number"

does not. A string knows how to handle a concatenate message with a number as the argument, but an integer is clueless what to do with an add of a string.

Since "+" is really just shorthand for "add", you can construct objects that work with the "+" operator just by implementing an "add(argument)" method in the object.

Under the covers: Miranda Methods

There are a number of messages to which all normal objects respond, known as Miranda Methods. One example of such a method is the printOn(textWriter) method, which defines how an object will write itself out as by default. Other Miranda methods will be described in other sections of the book. The full list of Miranda methods can be found in the Appendix. You can also see them in rune by typing help(def obj{}) duplicate of next section, fix

One important note about Miranda methods: they are not forwarded by the extends structure, and they are not intercepted by the match[verb,args] structure. They are always directly interpreted by the object that receives them.

Under the covers: Kernel-E

When you enter a normal E expression, E expands it into a more primitive form called Kernel-E. If you're ever confused about some E syntax, viewing the Kernel-E expansion may help you to understand what it means. To make rune display the Kernel-E:

? interp.setExpand(true)

Then:

? 1+1
# expansion: 1.add(1)

# value: 2

? <type:java.lang.String>
# expansion: type__uriGetter.get("java.lang.String")

# value: String

Under the covers: Slots

In E, names are actually bound to Slots. Evaluating a name as an expression calls the get/0 method on its slot. You can refer to the slot itself by prefixing the name with &:

? def x := 4
? x
# value: 4

? (&x).get()
# value: 4

Assigning to a variable calls the put/1 method on its slot. You can define your own slots like this:

? def mySlot {
>   to get() { return "hello" }
> }
# value: <mySlot>

? def &x := mySlot
? x
# value: "hello"

Common uses of slots include:

  • Lazy slots (that evaluate a given function the first time you use them).
  • Variables that check the type of each new value you assign (e.g. var x :String).
  • As a holder to pass to a function that will update the value (an “out parameter”). The & syntax deliberately echos the analogous facility in C.
  • Lamport slots

Getting Help About an Object With Rune

Another often convenient way of finding the list of methods associated with an object, be it a Java object or an E object or an object delivered as part of the E runtime, is to use the help(object) function of E, often used in rune.


 ? def add(a, b) {
 >     return a + b
 > }
 # value: <add>
 
 ? help(add)
 # value: an org.erights.e.elang.evm.EImplByProxy
 #        interface "__main$add" {
 #
 #            to run(:any, :any) :any
 #        }
 #
 ?

In this very simple example you see the "run" method listed here, which is the implicit method for a function. For a more sophisticated object than a simple add function, each method in the object would be listed as well.

help/1 doesn't show Miranda methods. To include them, use help/2:

? help(add, true)
# value: an org.erights.e.elang.evm.EImplByProxy
#        interface "__main$add" {
#            
#            /** Sugar method*/
#            to __conformTo(:nullOk[Guard]) :any
#            
#            /** Sugar method*/
#            to __getAllegedType() :nullOk[TypeDesc]
...
Personal tools
more tools