| C511-001/002 | Organization of Programming Languages | Winter 1999 |
|---|---|---|
Homework Assignment Number 2 |
Due: January 25, 1999 (soft), February 7 (hard - NEW!!!)
Send Result (Java code) to: hchen@boole.ececs.uc.edu
Motivation:
| You will see that the addition of threads to a language increases the generality of our streams results from homework number 1. |
The Problem:
| Write Java code to support general multiple consumers and producers. |
Recall, a stream is a possibly infinite list of tokens which, for the purposes of this course, is implemented as a pair of objects consisting of the first token and a procedure capable of producing the remaining tokens. Recall also that streams can support demand or data-driven computation, an idea introduced about two decades after imperative languages such as Fortran first appeared (and support for which is therefore absent from the early imperative languages). Finally, recall that effective stream implementations require some form of collateral computation, usually in the form of co-routines.
Although direct stream and co-routine support is not available from the older imperative languages, we found in assignment number 1 that it was possible to build a control abstraction in C which supports some limited form of both. Unfortunately, the limitations are quite severe. One of the limitations, imposed by the strong typing (early type bindings) of function pointers in C, is mitigated to a large extent by Object Oriented advancements provided by C++: that is, in C we are forced to write code which must be modified as new streamable objects are defined but we can write C++ code so that only categorical additions to existing code are required. The second limitation remains: that is, because in C and C++ direct access to internal points of functions is not permitted, a coder is forced to design producer and consumer procedures in a particular way - for example, instead of looping to produce tokens as would normally be expected, a producer must be designed to represent one iteration of a loop and must return a pointer to itself plus enough state to represent the entire history of the computation up to suspension. Clearly, this is not acceptable for general use.
In this assignment you will see that threads help improve the generality of stream and co-routine abstractions immensely. In particular, any piece of code that is written using, say the equivalent of printf for output, may be changed only by replacing printf with put, a method you will define in this assignment, to turn the code into a producer of stream tokens; and any piece of code that is written using scanf or similar functions for input may be changed to a stream token consuming function by replacing scanf with get, also to be coded in this assignment. You will write a class Stream that supports get and put. You will almost surely have to use threads in this assignment. Solutions without threads are possible but also have limitations and we will frown on such limitations.
The following Java code will implement the simple Successor function using your Stream class:
class Successor extends Stream
{
int number;
public Successor(int n) { number = n; }
public void run ()
{
while (true)
{
put((TokenObject)new IntObject(number++));
}
}
}
where TokenObject and IntObject are given as
interface TokenObject { }
class IntObject implements TokenObject
{
int number;
public IntObject (int n) { number = n; }
public int valueOf () { return number; }
}
An example of a producer/consumer is the following Java code which takes an integer n and a stream s of integers as input and returns a stream consisting of all integers of s multiplied by n:
class Times extends Stream
{
int number;
Stream stream;
public Times (int n, Stream s) { stream = s; number = n; }
public void run ()
{
while (true)
{
int n = ((IntObject)(stream.get())).valueOf();
put((TokenObject)new IntObject(number*n));
}
}
}
The goal is to envision networks of streams involving splits, joins, and cascades of streams. As a very simple example, the above two classes may be used as follows:
Stream s = new Times(3, new Successor(1)); ... System.out.println(((IntObject)(s.get())).valueOf()); ... System.out.println(((IntObject)(s.get())).valueOf()); ...
A more interesting example is the following problem:
| Given a list p of prime
numbers in increasing order, output a stream s of
integers in increasing order such that for every integer i in
stream s, the prime factors of i are all in p and
every integer whose prime factors are all in p is also in
stream s. Thus, if p is the list (3,5,11), s is the stream (3,5,9,11,15,25,27,33,45...) |
A solution to this problem is written as follows:
class Hamming extends Stream
{
int primes[], index;
public Hamming (int p[], int i) { primes = p; index = i; }
public void run ()
{
put((TokenObject)new IntObject(primes[index]));
Stream s = new Times(primes[index], new Hamming(primes, index));
if (index != 0) s = new Merge(s, new Hamming(primes, index-1));
while (true)
{
TokenObject token = s.get();
put(token);
}
}
}
This example, very elegant with our generalized stream control abstraction in Java, shows that your solution should support dynamic stream networks.
Some of the above may seem challenging at first. You must battle through it and attend lectures for more information if you have such difficulty. This is nothing to be afraid of since, when you are employed after graduation, you will be expected to do the same. Thus, instead of complaining that there is not enough information for you to continue, you should use this opportunity to practice how you will perform on the job. Trust me, it is better to beat your head against the wall in a controlled environment where that sort of thing is expected than on the job where results are demanded quickly.