Eden | The Paradise of Functional Concurrent Programming S. Breitinger, R. Loogen Y. Ortega-Mallen, R. Pe~na-Mar Philipps-Universitat Marburg, Universidad Complutense de Madrid Workshop 05: Parallel languages and programming y
z
Abstract
The functional concurrent language Eden is an extension of the lazy functional language Haskell by constructs for the explicit speci cation of dynamic process systems. It employs stream-based communication and is tailored for distributed memory systems. Eden supports and facilitates the task of parallel and concurrent programming. To illustrate this statement the paper includes elegant solutions to traditional concurrency problems.
1 Introduction Eden is a functional language with explicit process de nitions and implicit communication, suitable for parallel as well as for concurrent programming in general. It is tailored for parallel systems with distributed memory. More precisely, Eden extends the lazy functional language Haskell [HWe92] by new syntactic constructs for explicitly de ning processes. These processes communicate by the exchange of values via communication channels modelled by lazy lists. Communication is asynchronous and one-to-one. The kernel part of the language is based on the well-known approach of stream-based computation and communication [KM77]. In addition Eden incorporates special concepts for the ecient treatment of general reactive systems, i.e. systems which maintain some interaction with the environment and which may be time-dependent: Dynamic creation of reply channels: This feature simpli es the generation of complex communication topologies and increases the exibility of the language. It is related to the `incomplete message principle' known from concurrent logic languages [Sha89] and the `channel name passing' principle of the -calculus [MPW92], but is far more restricted in order to maintain the declarative nature of the language. Prede ned nondeterministic processes MERGE and SPLIT: In order to model many-to-one and one-to-many communication in process systems Eden provides prede ned nondeterministic merge and split processes. Eden incorporates a two level structure: the level of userde ned processes and the level of process systems. User-de ned processes can be seen as deterministic mappings from input channels to output channels. Nondeterminism is only handled at the system level which consists of all (prede ned and user-de ned) processes interacting via communication on channels. This distinction and the maintenance of referential transparency within user-de ned processes supports modularity. Supported by the DAAD (Deutscher Akademischer Austauschdienst) and the Spanish Ministry of Education and Science in the context of the German-Spanish Accion Integrada n.142B. y Fachbereich Mathematik, Fachgebiet Informatik, Hans Meerwein Strae, Lahnberge, D-35032 Marburg, Germany,
fbreiting,
[email protected] z
Sec. Dept. de Informatica y Automatica, Facultad de C.C. Matematicas, E-28040 Madrid, Spain,
fyolanda,
[email protected]
1
The development of Eden has been described in [BLOM96]. In the present paper we focus on the practical applicability of the language in parallel and concurrent programming. We present Eden solutions for several well-known parallel and concurrent problems. It is shown how complex programs can be developed in a modular and easily comprehensible way.
Plan of the paper In Section 2 we explain the main features of Eden. Section 3 is devoted to advanced programming examples: a parallel matrix multiplication as an example of a parallel system, and the Disk Scheduler as well as the Dining Philosophers as examples of reactive concurrent systems. In Section 4 we sketch the operational semantics of the language and make some comments upon the prototype implementation which is currently under development. Finally we discuss related work and draw conclusions.
2 Eden { The Language According to the classi cation introduced by Carriero and Gelernter [GC92], Haskell forms the computation model of Eden. This is extended by a coordination model that introduces processes in a functional style, embodying constructs which allow for the de nition and creation of processes, communication and synchronization, and the speci cation of interconnections between processes.
2.1 Process abstractions and process instantiations
Eden distinguishes between process abstractions, which specify process behaviour in a purely functional way, and process instantiations in which process abstractions are supplied with input values in order to create a new process. A process maps (streams of) input values in1, : : : , inm to (streams of) output values out1, : : : , outn . It is speci ed by a process abstraction of the following form: visualized as: absName par 1 : : : par k = process out1 in1 input in1 , : : : , in m @@R ?? output out 1 , : : : ,out n .. absName par1 : : : park .. body equation 1 . . .. . ? @@R ? equation r outn inm
' $ & %
end
The process abstraction is a general parameterized process scheme. The input and output ports given after the keyword process specify the interface of processes generated by the use of this abstraction. The body of a process abstraction consists of a list of equations which de ne the outports, auxiliary functions and common subexpressions. Every outport is de ned by a unique equation within the process body. The body can be seen as a functional program. A process abstraction is a special kind of -abstraction with the following type:
1 ! : : : ! k ! Process h (1; : : :; m) ! (~1 ; : : :; ~n) i; where 1; : : :; k denote the types of the parameters and 1; : : :; m and ~1; : : :; ~n are respectively 0
0
0
0
the types of the inports and of the outports. The tuple type which wraps up the inports of a process indicates that the corresponding channels must be provided `in one piece' while currying is admitted for the process parameters. Communication channels transmit completely evaluated values of arbitrary type. In order to model the transmission of a stream of values we introduce a new unary type constructor Strm. A 2
channel of type Strm transmits values of type one by one. Streams correspond to lazy lists and, accordingly, stream types are fully compatible with the corresponding list types. But note that a channel of type list is assumed to transfer exactly one list of type , which is nite because its evaluation must be completed before the transmission, while a channel of type Strm transmits a potentially in nite list component-wise. Example (process for merging sorted streams). A process which merges two sorted input streams into a single sorted output stream is speci ed by the following process abstraction: merger = process input s1, s2 :: Strm a output s :: Strm a body s = smerge s1 s2 smerge [] l = l smerge l [] = l smerge (x:l) (y:t) = if x (Int -> Int -> a -> Process (d, Strm b, Strm c) >) -> Process < ([Strm b],[Strm c]) -> ([[d]],[Strm b],[Strm c]) > grid matrix proc = process input fromright :: [Strm b], frombottom :: [Strm c] output results :: [[d]], toleft :: [Strm b], totop :: [Strm c] body (results,toleft,totop) = makegrid fromright frombottom matrix 1 1 makegrid :: [Strm b] -> [Strm c] -> [[a]] -> Int -> Int -> ([[d]],[Strm b],[Strm c]) makegrid rs bs [] i j = ([],[],bs) -- go down first column makegrid rs bs row:rows i j = (reslist:reslists,l:ls,ts) where (reslists,ls,mbs) = makegrid (tl rs) bs rows i+1 j (reslist,l,ts) = makerow (hd rs) mbs row i j makerow :: Strm b -> [Strm c] -> [a] -> Int -> Int -> ([d],Strm b,[Strm c]) makerow r bs [] i j = ([],r,[]) -- generate row processes makerow r bs val:rvals i j = (res:ress,l,t:ts) where (res,l,t) = proc i j val (ml,(hd bs)) -- cell instantiation (ress,ml,ts) = makerow r (tl bs) rvals i j+1 end
It is straightforward to use the above grid topology to de ne a torus topology: torus :: [[a]] -> (Int -> Int -> a -> Process (d, Strm b, Strm c) >) -> Process < () -> ([[d]]) > torus matrix proc = process input output outmatrix :: [[b]] body (outmatrix,row,col) = grid matrix proc (row,col) end
In the following we will use this torus to specify a parallel matrix multiplication algorithm. 7
3.2 Matrix Multiplication
Matrix multiplication is an important operation in many scienti c and engineering problems. It has a good potential for parallelization and has become a standard example for parallel programming. We consider here a parallel algorithm which uses a torus topology (see [And91, Section 8.4]). The matrices are partitioned into submatrices which are distributed on the torus nodes. For simplicity we assume the partition size to be 1 and the matrix dimension to be n n. Each element of the torus rst gets the corresponding elements of the input matrices. The torus node with the position (i; j ) then has to compute the element (i; j ) of the result matrix, i.e. the scalar product of the ith row of the rst input matrix and the j th column of the second input matrix. In order to place the elements to be multiplied on the same node, the rows of the rst matrix and the columns of the second matrix are rotated by (i ? 1) and (j ? 1) positions respectively before the proper computation starts. Then all torus nodes perform an iteration of n ? 1 steps in which they multiply corresponding elements of the matrices which they read subsequently from their input channels. matmult :: Int -> [[Int]] -> [[Int]] -> [[Int]] matmult n ass bss = torus (map uncurry.zip (zip ass bss)) (scalarproduct n) scalarproduct n i j (a,b) -- process abstraction for each torus node = process input inrow, incol :: Strm Int output result :: Int; outrow, outcol :: Strm Int body result = iterate (n-1) (drop (i-1) inrow) (drop (j-1) incol) (a0 * b0) iterate 0 row col val = val iterate (n+1) (r:row) (c:col) val = iterate n row col (val+r*c) a0 = (a:inrow)!!(i-1) b0 = (b:incol)!!(j-1) outrow = a:inrow -- rotate inrow outcol = b:outcol -- rotate outrow end
3.3 The Disk Scheduler
This is a problem for which C.A.R. Hoare provided a solution based on monitors in [Hoa74]. The problem is posed in the following terms: a set of users need exclusive access to a moving head disk. They address requests to a disk sche. . . . . . . . user 1 user i user n duler, indicating in the request the intended track to be accessed. The scheduler stores the requests requests and, whenever the disk is not busy, grants permisMERGE sion to the user whose track is nearest in the curreplies rent scanning direction. Requests to access tracks above the current track are stored in the so-called DS releases foreground queue, whereas requests to access the current track or below it are stored in a background queue. When the foreground queue is exhausted, DD disk the scheduler switches to the background queue. Hoare's solution implements the so-called elevator Figure 3: Disk scheduling process topology strategy: requests are rst scanned in ascending track order, and afterwards in descending order. An alternative solution providing more uniform waiting times is the circular scan (see for example [And91, Section 6.4]), where queues are scanned always in ascending track order. The Eden solution to this problem is depicted graphically in Figure 3. There, we provide two processes to serve user requests: the DS (disk scheduler) process, which receives user requests and stores them in track ascending order, and the DD (disk driver) process, which actually serves requests. Process DS transfers the next request to be served to process DD. The physical connection with the disk is supposed to be done by this process. The response associated to each request is sent 8
directly from process DD to the user involved. The Eden dynamic reply channel facility is used for that. In the drawing, this communication is represented by a dashed line. Moreover, there is a feedback communication from DD to DS to inform the latter by a release message that the disk is not busy. The nondeterminism in the arrival of user requests |and release messages from DD| to process DS , is modelled by connecting all these channels to a MERGE process which interleaves the signals into a single channel. The type declarations, type signatures and process abstractions corresponding to this design are given in Figure 4. The user process abstraction is modelled by an endless loop: produce a request, wait for the reply, reenter the cycle. The request indicates the track to be accessed, the parameters associated to the transaction and a channel name |previously created| to receive the reply. The disk scheduler diskSched implements the circular scan strategy mentioned above. Its \state" consists of an integer recording the current track being served, a boolean indicating whether the disk is free or not, and the foreground and background queues of pending requests. These are supposed to be implemented as min-queues with the usual operations to access or delete the minimum element and to add a new element to the queue. Ecient functional implementations for this data structure can be found in [Kin94, NPP95]. The process abstraction diskdriver serves transactions one at a time and sends back the result to the requesting user. instantiate :: Int -> User -> DiskSched -> DiskDriver -> () instantiate n u ds dd = let requests = [ u ({reply}) | i = Process < (Strm Signal) -> (Strm Trans) > = Process < (Strm Trans) -> ({ReplyTrans}, Strm Signal) > :: User :: DiskSched :: DiskDriver
user =
process input reply :: {ReplyTrans} output requests :: Strm Signal body (iniTrack, iniTrans) = ... requests = restOfTransactions iniTrack iniTrans resOfTransactions track trans = new reply. Request track trans $reply : waitReply reply waitReply (Reply replyParams) let (nextTrack, nextTrans) = changeState replyParams in restOfTransactions nextTrack nextTrans end diskSched = process input fromEnv :: Strm Signal output transacs :: Strm Trans body transacs = cycle 0 True emptyQ emptyQ fromEnv cycle ctrack free fq bq (s:ss) = case s of Request track trans $rep -> if free then Transac trans $rep : cycle track False fq bq ss else if track > ctrack then cycle ctrack free (add fq (track,trans,$rep)) bq ss else cycle ctrack free fq (add bq (track,trans,$rep)) ss Release -> if empty fq && empty bq then cycle ctrack True emptyQ emptyQ ss else if empty fq then let (t,trans,$rep) = min bq in Transac trans $rep : cycle t False (elim bq) fq ss else let (t,trans,$rep) = min fq in Transac trans $rep : cycle t False (elim fq) bq ss end diskdriver = process input transacs :: Strm Trans output releases :: Strm Signal reply :: {ReplyTrans} body releases = executeTransaction transacs executeTransaction (Transac params $reply : ts) = let results = accessToDisk params -- here accesses to disk are done in Release : executeTransaction ts