The main concepts in the language are:
Question: how would you handle higher-order procedures without static scoping?
In comparison with other systems we have looked at, the strengths of Obliq are its treatment of serialization and distribution.
Actors Rapide Obliq Communication asynchr message send broadcast synchr message send Serialization serial objects multi-thread objects multi-thread objects serialized objects implicit, explicit mutex Distribution implicit implicit explicit Encapsulation objects objects weak object forms
Objects are collections of named fields, which may be data or methods, with four basic operations
Methods and Functions: Obliq has separate syntax for methods and procedures.
meth (self) self.x + y end proc (x) x+3 end
Examples:
let 1d_point = { x => 3, move =>meth (self, dist) self.x := self.x + y; self end }
Values and locations:
In Obliq, constant identifiers denote values, variable identifiers
denote locations. (This is like ML.)
Transmission:
A value may contain embedded locations.
For example, an array has a location for each entry, an object
a location for updatable fields, and a closure a location for each
free variable identifier.
On transmission, a value containing no embedded locations is
copied. A value containing embedded locations is copied up to the point
where locations appear. Then, local references to locations are replaced
by network references.
Closures:
In general, a closure consists of source code (or compiled code)
and its environment, typically represented by a pointer to an evaluation stack.
One way to transmit closures would be to copy an entire
evaluation stack. Instead, Obliq represents a closure using a table
of values in place of a stack pointer.
This table contains only the values of free variables,
not the entire stack, and may be transmitted with the source code.
However, the table may contain references to remote locations
(after transmission) if the code has free variable identifiers.
Objects:
An object consists of a set of locations at a single site.
While references to an object may be passed across the network,
an object remains at a single site.
However, an object may be "cloned" to a different site,
duplicating its state in the process.
Aliasing provides ... (?? remote acess to an object ??)
Concurrency and mutual exclusion:
The basic mechanism for producing concurrent computation is
explicit creation of sequential threads. This is done through
explicit fork and join statements.
Presumably (* check the manual *)
fork creates two new threads that replace the
exisiting thread and proceed concurrently.
The join statement is used to wait for forked threads to
complete (what if they, in turn, fork? Is this like cobegin/coend??)
and then continue a single thread.
(* this is not explained in the POPL overview paper, but see queue example below *)
Related primitives are:
condition() | signal(a) | broadcast -- create and signal a condition
watch s_1 until s_2 -- waiting for a signal and a boolean guard
pause(a) -- pause the current thread
mutex() -- create a mutex
lock s_1 do s_2 end -- locking a mutex in a scope
wait(a_1,a_2) -- waiting on a mutex for a condition
It looks like the novel aspect of Obliq is the way objects (data) are transmitted to different locations, not the treatment of concurrency (control of execution).
I gather that communication, in the form of sending a message to an object, is synchronous.
let p = { x => 0, y => 0, (* define point object *) move => meth (self,distx, disty) self.x = self.x + distx; self.y = self.y + disty; self; end }; p.x (* select component *) p.x := 5; (* set component *) p.move(2,1); (* invoke method *) p.move := meth (self,distx, disty) (* override method *) self.x = self.x - distx; self.y = self.y - disty; self; end
p.y := meth (self) self.x end (* override method *) p.move := meth (self,distx, disty) (* override method *) self.x = self.x + distx; self; endThis does change the way we select the x component, since we now should write p.y() instead of p.y, I think.
When a field of a remote object is updated, a closure is transmitted over the network and installed in the remote object.
clone(a_1, ... , a_n)creates a new object with the same field names as the union of a_1, ... , a_n, each initialized to the coresponding field (values, menthods or aliases) of a_i. Whether the objects named in the clone statement are local or remote, the new object is created at the local site. This may involve transmission of closures across the network.
Cloning provides a form of object extension, for example
clone(p, { color => green, darken => meth(s) s.color.hue := s.color.hue+1 end }produces a colored point by cloning a point. It's not clear to me whether the added methods are allowed to refer to the methods in the cloned object. I guess that since there are no static type restrictions, this is allowed. (* .. *)
alias y of b endIf the field x of object a is this alias, then x.a results in invocation (or selection) of field y of object b. An important difference between aliasing and delegation is that if method b of object y uses the self parameter, this will refer to the object b, not the object a that was sent the original message. Example:
let b = { w => 0, y => meth(self) self.w end }; let a = { w => 1, x => alias y of b end }; a.y (* returns 0, not 1 *)Aliases can be set using object updating, for example, a.w := alias w of b end.
If a.x is an alias for b.y, then method override of a.x results in redefinition of b.y.
A special case of aliasing is redirection of all operations, expressed by
redirect a to b endThe effect is to replace every field of a (including alias fields) to b. If a is local and b remote, then this creates a "local surrogate" for a remote object.
Questions: is aliasing syntactic sugar,
alias y of b end for meth (s) y.b endor is there something else going on here? What is the effect of
let b = clone(a); redirect a to b enddoes this migrate the object?
p.x := 5; (* external update *) p.move(2,1); (* move method performs internal updates *)
In a protected object, written
{protected ... }external updating, cloning and aliasing operations are disallowed. However, these may be done internally by methods if desired. This mechanism is useful for maintaining invariants of an object.
The capability to update, clone and alias is transferrable in certain ways. More specifically, we say an operation op(o), where op may be update, clone or alias, is internal or self-inflicted if o is the same object as the self of the current method, if any. The current methods is the last method that was invoked in the current thread of control and has not yet returned. In particular, procedure calls do not hide or change the current method.
Example: An operation internal to a nested object is not considered internal to the outer enclosing object:
let o = { m => meth(s) let o' = {n => meth(s') s.m end } in o'.n end ... }Note: the internal/external distinction is also important for serialized objects, discussed below.
Question: how do we get encapsulation in Obliq? One approach is through scoping, for example
let create = proc(init_x) let var x = init_x in { get_x => !x, double => meth () x := 2*x end }Each call to create will allocate a new location for x and return an object closure that has "private" access to this x. However, inheritance via cloning will not result in objects that have their own copy of private x. In other words, this way of achieving "private" instance variables seems reasonable for the objects that are immediately created, but does not work properly (or as usual, anyway) in the presence of inheritance.
A serialized object, written
{serialized ... }has an implicit associated mutex, called the object mutex. An object mutex serializes the execution of selection, update, cloning and aliasing operations according to these specific rules:
Explicit signaling and waiting: A condition may be created using the expression condition() and "slgnaled" using the expression signal(a) .
A watch statement makes it possible to coordinate multiple threads in an object, using signals and the implicit mutex of an object. Intuitively, the command
watch c until guard endwaits until the boolean-valued guard becomes true, with the condition c used to determine when to check the guard again if it is currently false. It is generally used when the object mutex is locked, unlocking the mutex if the thread waits because the guard fails. More specifically, this statement evaluates c to a condition and, if the guard evaluates to true, terminates the wait leaving the object mutex locked. If the guard is false, the object mutex is unlocked (allowing other methods of the object to execute) and the thread waits for the condition to be signalled. When the condition is signaled, the object mutex is locked and the boolean guard evaluated again, repeating the process.
Example: a serialized queue, using scoping for encapsulation (taken from Cardelli's slides).
let queue = (let NonEmpty = condition(); var q = []; (* the hidden queue data *) {protected, serialized, write => meth(s, elem) q := q @ [elem]; (* append elem to tail *) signal(nonEmpty); (* wake up readers *) end; read => meth(s) watch nonEmpty (* wait for writers *) until #(q)>0 end; (* until number of elements > 0 *) let q0 = q[0]; (* select first element *) q := q[1 for #(q)-1]; (* remove from queue *) q0; (* return first element *) end; } );A simple multi-threaded use of this queue
let t = fork( proc() queue.read() end, 0); (* fork a reader, which blocks *) queue.write(3); (* write to queue *) let result = join(t) (* wait for reader thread and return value *)
Exercise:
Suppose we add another method to queues that just reads the front of
the queue without deleting it.
front => meth(s) watch nonEmpty (* wait for writers *) until #(q)>0 end; (* until number of elements > 0 *) let q0 = q[0]; (* select first element *) q0; (* return first element *) end;The semantics of signal are that at least one blocked process will receive the signal and test its guard. With several front operations waiting for a condition, only one would be "woken up" and proceed. Explain this and show that the problem can be fixed by changing "signal" in write to "broadcast".
slide 12: name server
slides 18-19: shared variables as a result of static scope (remember: function call is synchronous so rexec completes before subsequent value of x is tested)
Can you test whether an object is local or remote? Would this be a good extension to Obliq?
Fault tolerance: what do you do if an object fails to respond to a message? (Communication appears synchronous...)