Thursday, April 18, 2013

Systems Programming with Go, Rust, and ParaSail

Here is a paper that goes with the talk I am giving this coming Tuesday, April 23 at the DESIGN West, aka Embedded Systems Conference, in San Jose, on a comparison between Go, Rust, and ParaSail.

If you are in the Bay Area, come on down.  The talk is ESC-218, on Tuesday April 23 from 2:00-3:00PM in Salon 3:  http://www.ubmdesign.com/sanjose/schedule-builder/session-id/98


NOTE: Some browsers are having trouble with this version of the paper.  It was cut and pasted from a Word document, which is always dicey.  A PDF version of this paper is available on the ParaSail Google Group:

   http://groups.google.com/group/parasail-programming-language


Systems Programming in the Distributed, Multicore World with Go, Rust, and ParaSail 
S. Tucker Taft
AdaCore
24 Muzzey Street, 3rd Floor
Lexington, MA  02421
taft@adacore.com

Abstract                
The distributed, multicore train is stopping for no programmer, and especially the systems programmer will need to be ready to hop on to the distributed parallel programming paradigm to keep their systems running as efficiently as possible on the latest hardware environments.  There are three new systems programming languages that have appeared in the last few years which are attempting to provide a safe, productive, and efficient parallel programming capability.  Go is a new language from Google, Rust is a new language from Mozilla Research, and ParaSail is a new language from AdaCore.  This talk will describe the challenges these languages are trying to address, and the various similar and differing choices that have been made to solve these challenges.
Keywords multicore, distributed, parallel programming, systems programming language, Go language, Rust language, ParaSail language

1. Introduction
The distributed, multicore train is stopping for no programmer, and especially the systems programmer will need to be ready to hop on to the distributed parallel programming paradigm to keep their systems running as efficiently as possible on the latest hardware environments.  There are three new systems programming languages that have appeared in the last few years which are attempting to provide a safe, productive, and efficient parallel programming capability.  Go is a new language from Google [1], Rust is a new language from Mozilla [2], and ParaSail is a new language from AdaCore [3][4].
The designers of Go, Rust, and ParaSail all are facing a common challenge -- how to help programmers address the new distributed and multicore architectures, without having the complexity of programming going past that which is manageable by the professional, yet still human, programmer.  All programming languages evolve, and as a rule, they tend to get more complex, not less so.  If every time a new hardware architecture becomes important, the programming language is enhanced to provide better support for that architecture, the language can become totally unwieldy, even for the best programmers.  When the architectures changes radically, as with the new massively distributed and/or multicore/manycore architectures, this may mean that the language no longer hangs together at all, and instead has become a federation of sublanguages, much like a house that has been added onto repeatedly with a different style for each addition.
Because of the complexity curse associated with language evolution, when there is a significant shift in the hardware landscape, there is a strong temptation to start over in programming language design.  After many years of a relatively stable programming language world, we now see a new burst of activity on the language design front, inspired in large part by the sense that our existing mainstream languages are either not going to be supportive enough, or that they are becoming simply too complex in trying to support both the old and new hardware paradigms through a series of language extensions.
2. Go from Google
Go, Rust, and ParaSail all emerged over the past few years, each with its own approach to managing complexity while supporting parallelism.  Go from Google is the brain child of Rob Pike and his colleagues at Google.  Rob was at Bell Labs in the early Unix and C days, and in many ways Go inherits the C tradition of simplicity and power. Unlike C, storage management has been taken over by the Go run-time through a general purpose garbage collection approach, but like C, care is still needed in other areas to ensure overall program safety. 
From the multicore perspective, Go uses goroutines for structuring large computations into a set of smaller potentially parallel computations.  Goroutines are easy to create – essentially any stand-alone call on a function or a method can be turned into a goroutine by simply prefixing it with the word “go.”  Once a goroutine is spawned, it executes independently of the rest of the code.  A goroutine is allowed to outlive the function that spawns it, thanks to the support of the garbage collector; local variables of the spawning function will live as long as necessary if they are visible to the spawned goroutine.
For goroutines to be useful, they need to communicate their results back to the spawning routine.  This is generally done using strongly-typed channels in Go.  A channel can be passed as a parameter as part of spawning a goroutine, and then as the goroutine performs its computation it can send one or more results into the channel.  Meanwhile, at some appropriate point after spawning the goroutine, the spawner can attempt to receive one or more values from the channel.  A channel can be unbuffered, providing a synchronous communication between sender and receiver, or it can provide a buffer of a specified size, effectively creating a message queue. 

Communication between goroutines can also go directly through shared global variables.  However, some sort of synchronization through channels or explicit locks is required to ensure that the shared variables are updated and read in an appropriate sequence.

Here is an example Go program that counts the number of words in a string, presuming they are separated by one or more separator characters, dividing multi-character strings in half and passing them off to goroutines for recursive word counts:
 func Word_Count
   (s string; separators string) int = {
     const slen = len(s)
     switch slen {
       case 0: return 0 // Empty string
       case 1:          // single-char string
         if strings.ContainsRune
               (separators, S[0]) {
             return 0  // A single separator
         } else {
             return 1  // A single non-separator
         }
       default:   // divide string and recurse
         const half_len = slen/2
         // Create two chans and two goroutines
         var left_sum = make(chan int)   
         var right_sum = make(chan int)
         go func(){left_sum <- span="" word_count="">
              (s[0..half_len], separators)}()
         go func() {right_sum <- span="" word_count="">
              (s[half_len..slen], separators)}()
         // Read partial sums
         // and adjust total if word was divided
         if strings.ContainsRune
              (separators, s[half_len-1]) ||
           strings.ContainsRune
              (separators, s[half_len]) {
             // No adjustment needed
             return <-left_sum right_sum="" span="">
         } else {// Minus 1 because word divided
             return <-left_sum -="" 1="" right_sum="" span="">
         }
     }
  }
2.1 Unique Features of Go
Go has some unusual features.  Whether a declaration is exported is determined by whether or not its name begins with an upper-case letter (as defined by Unicode); if the declaration is a package-level declaration or is the declaration of a field or a method, then if the name starts with an upper-case letter, the declaration is visible outside the current package.

Every Go source file begins with a specification of the package it is defining (possibly only in part).  One source file may import declarations from another by specifying the path to the file that contains the declarations, but within the importing code the exported declarations of the imported code are referenced using the imported file’s package name, which need not match that of the imported file’s filename.  Of course projects would typically establish standard naming conventions which would align source file names and package names somehow.

Go provides a reflection capability, which is used, for example, to convert an object of an arbitrary type into a human-readable representation.  The “%v” format in Go’s version of printf does this, allowing arbitrarily complex structs to be written out with something as simple as:

fmt.Printf(“%v”, my_complex_object)

Printf is implemented in Go itself, using the “reflect” package.

There are no uninitialized variables in Go.  If a variable is not explicitly initialized when declared, it is initialized by default to the zero of its type, where each type has an appropriately-defined zero, typically either the zero numeric value or the nil (aka “null”) pointer value, or some composite object with all components having their appropriate zero value.
2.2 What Go Leaves Out
Because complexity was a major concern during all three of these language designs, some of the most important design decisions were about what to leave out of the language.  Here we mention some of the features that Go does not have.

Go does not permit direct cyclic dependencies between packages.  However, the Go interface capability permits the construction of  recursive control or data structures that cross packages, because an interface declared in one package can be implemented by a type declared in another package without either package being directly dependent on the other.

Like C, Go has no generic template facility.  There are some builtin type constructors, such as array, map, and chan, which are effectively parameterized type constructors, but there is no way for the user to create their own such type constructor.  Unlike C, there is no macro facility which might be used to create something like a parameterized type.  Nevertheless, Go’s flexible interface and reflection capabilities allow the creation of complex data structures that depend only on the presence of a user-provided method such as Hash and the DeepEqual function of the reflect package.

Go does not allow user-defined operators.  Various operators are built in for the built-in types, such as int and float32.  Interestingly enough, Go does include built-in complex types (complex64 and complex128) with appropriate operators.

Go does not have exceptions.  However, functions and methods  may return multiple results, and often errors are represented by having a second return value called error that is non-nil on error.  Unlike in C, you cannot ignore such an extra parameter; unless you explicitly assign it to an object of name “_”.  When things go really wrong in Go, a run-time panic ensues, and presumably during development, you are tossed into a debugger.
3. Rust from Mozilla Research
The modern web browser represents one of the most complex and critical pieces of software of the internet era.  The browser is also a place where performance is critical, and there are many opportunities for using parallelism as a web page is “rendered.”  The Rust language arose originally as a personal project by one of the engineers at Mozilla Research (Graydon Hoare), and has grown now into a Mozilla-sponsored research effort.  Rust has been designed to help address the complexity of building components of a modern browser-centric software infrastructure, in the context of the new distributed multicore hardware environment.

Like Go, Rust has chosen to simplify storage management by building garbage collection into the language.  Unlike Go, Rust has chosen to restrict garbage collection to per-task heaps, and adopt a unique ownership policy for data that can be exchanged between tasks.  What this means is that data that can be shared between tasks is visible to only one of them at a time, and only through a single pointer at a time (hence an owning pointer).  This eliminates the possibility of data races between tasks, and eliminates the need for a garbage collector for this global data exchange heap.  When an owning pointer is discarded, the storage designated by the pointer may be immediately reclaimed – so no garbage accumulates in the global exchange heap.

Here is a Rust version of the Word Count program, recursing on multi-character strings with subtasks encapsulated as futures computing the subtotals of each string slice:

fn Word_Count
   (S : &str; Separators : &str) -> uint {
     let Len = S.len();
     match Len {
       0 => return 0; // Empty string
       1 => return    // one-char string
         if Separators.contains(S[0]) { 0 }
           else { 1 };  // 0 or 1 words
       _ =>           // Divide and recurse
         let Half_Len = Len/2;
         let Left_Sum = future::spawn {
           || Word_Count(S.slice
                 (0, Half_Len-1), Separators)};
         let Right_Sum = future::spawn {
           || Word_Count(S.slice
                 (Half_Len, Len-1), Separators)};
         // Adjust sum if a word is divided
         if Separators.contains(S[Half_Len]) ||
           Separators.contains(S[Half_Len+1]) {
             // No adjustment needed
             return
              Left_Sum.get() + Right_Sum.get(); 
         } else {
             // Subtract one because word divided
             return
              Left_Sum.get()+Right_Sum.get() – 1;
         }
     }
  }

Rust does not have special syntax for spawning a “task” (Rust’s equivalent of a “goroutine”) nor declaring the equivalent of a “channel,” but instead relies on its generic template facility and a run-time library of  threading and synchronization capabilities.  In the above example, we illustrate the use of futures which are essentially a combination of a task and an unbuffered channel used to capture the result of a computation.  There are several other mechanisms for spawning and coordinating tasks, but they all depend on the basic tasking model, as mentioned above, where each task has its own garbage-collected heap for task-local computation (manipulated by what Rust calls managed pointers), plus access via owning pointers to data that can be shared between tasks (by sending an owning pointer in a message).
3.1 Rust Memory Management Performance
One of the major advantages of the Rust approach to memory management is that garbage collection is local to a single task.  By contrast, in Go each garbage collector thread has to operate on data that is potentially visible to all goroutines, requiring a garbage collection algorithm that synchronizes properly with all of the active goroutines, as well as with any other concurrent garbage collector threads (presuming garbage collection itself needs to take advantage of parallel processing to keep up with multithreaded garbage generation). 

In Rust, a conventional single-threaded garbage collector algorithm is adequate, because any given garbage collector is working on a single per-task heap.  Furthermore, storage visible via owning pointers needs no garbage collection at all, as releasing an owning pointer means that the associated storage can also be released immediately.
3.2 The Costs of Safety and Performance
One of the downsides of added safety and performance can be added complexity.  As we see, Rust has added safety by allowing access to sharable data only via pointers that give exclusive access to one task at a time, and added performance because garbage collection is single-threaded.  However, as a result, Rust needs several kinds of pointers.  In fact, there are four kinds of pointers in Rust, managed pointers (identified by ‘@’as a prefix on a type) for per-task garbage-collected storage, owning pointers (identified by ‘~’) for data that is sharable between tasks, borrowed pointers (identified by ‘&’) that can temporarily refer to either per-task or sharable data, and raw pointers (identified by ‘*’), analogous to typical C pointers, with no guarantees of safety.
4. ParaSail from AdaCore
The ParaSail language from AdaCore takes support for parallelism one step further than Go or Rust, by treating all expression evaluation in the language as implicitly parallel, while also embracing full type and data-race safety.  Rather than adding complexity to accomplish this, the explicit goal for ParaSail was to achieve safety and pervasive parallelism by simplifying the language, eliminating impediments to parallelism by eliminating many of the features that make safe, implicit parallelism harder.
4.1 What ParaSail Leaves Out
Some of the features left out of ParaSail include the following:

·       No pointers
·       No global variables
·       No run-time exception handling
·       No explicit threads, no explicit locking nor signaling
·       No explicit heap, no garbage collection needed
·       No parameter aliasing

So what is left?  ParaSail provides a familiar class-and-interface-based object-oriented programming model, with mutable objects and assignment statements.  But ParaSail also supports a highly functional style of programming, aided by the lack of global variables, where the only variable data visible to a function is via parameters explicitly specified as var parameters.   This means that the side-effects of a function are fully captured by its parameter profile, which together with the lack of parameter aliasing allows the compiler to verify easily whether two computations can safely be performed in parallel.

By design, every expression in ParaSail can be evaluated in parallel.  If two parts of the same expression might conflict, the expression is not a legal ParaSail expression.  The net effect is that the compiler can choose where and when to insert parallelism strictly based on what makes the most sense from a performance point of view.  Here, for example, is the Word Count example in ParaSail, where parallelism is implicit in the recursive calls on Word Count, without any explicit action by the programmer:

func Word_Count
  (S : Univ_String;
   Separators :
        Countable_Set := [' '])
   -> Univ_Integer is
    case |S| of
      [0] => return 0  // Empty string
      [1] =>
        if S[1] in Separators then
            return 0   // No words
        else
            return 1   // One word
        end if
      [..] =>          // Divide and recurse
        const Half_Len := |S|/2
        const Sum := Word_Count
            (S[1 .. Half_Len], Separators) +
          Word_Count
            (S[Half_Len <.. |S|], Separators)
        if S[Half_Len] in Separators or else
          S[Half_Len+1] in Separators then
            return Sum  // No adjustment needed
        else
            return Sum-1   // Adjust sum
        end if
    end case
end func Word_Count

Although there is no explicit use of a parallel construct, the sum of the two recursive calls on Word_Count can be evaluated in parallel, with the compiler automatically creating a picothread for each recursive call, waiting for their completion, and then summing the results, without the programmer having to add explicit directives.
4.2 Implicit and Explicit Parallelism in ParaSail
Explicit parallelism may be specified if desired in ParaSail, or the programmer can simply rely on the compiler to insert it where it makes the most sense.  The general philosophy is that the semantics are parallel by default, and the programmer needs to work a bit harder if they want to force sequential semantics.  For example, statements in ParaSail can be separated as usual with “;” (which is implicit at the end of the line when appropriate), or by “||” if the programmer wants to request explicit parallelism, or by “then” if the programmer wants to disallow implicit parallelism.  By default the compiler will evaluate two statements in parallel if there are no data dependences between them. 

As another example of ParaSail’s implicit and explicit parallelism, the iterations of “for I in 1..10 loop” are by default executed in any order, including parallel if there are no data dependences between the loop iterations, while “for I in 1..10 forward loop” or “for I in 1..10 reverse loop” may be specified to prevent parallel evaluation, and “for I in 1..10 concurrent loop” may be used to specify that parallel evaluation is desired, and it is an error if there are any data dependences between the iterations.  In all these cases, the compiler will ensure that any parallel evaluation is safe and data-race free, and will complain if there are potential race conditions when parallel evaluation semantics are specified.
4.3 Simplicity breeds Simplicity in ParaSail
There is somewhat of a virtuous cycle that occurs when a programming language is simplified, in that one simplification can lead to another.  By eliminating pointers and a global heap from ParaSail, the language can provide fully automatic storage management without the need for a garbage collector.  Objects in ParaSail have value semantics meaning that assignment copies the value of the right-hand side into the left-hand side, with no sharing of data.  A built-in move operation is provided for moving the value of the right-hand side into the left-hand side, along with a swap for swapping values, thereby reducing the cost of copying while still preserving value semantics.

Every type in ParaSail has an extra value, called null, which is used to represent an empty or otherwise uninitialized value.  An object or component may have a null value of its type T only if it is declared to be “optional T”.  Optional components may be used to implement trees and linked lists, without the use of explicit pointers, and without the potential sharing issues associated with pointers, even if behind the scenes the compiler uses pointers to implement optional objects or components.  The availability of optional components effectively allows an object to grow and shrink, meaning that dynamic structures like hash tables, and higher level notions such as maps and sets, can be implemented in ParaSail without any explicit pointers, with the advantages of purely value semantics.

The lack of explicit pointers means that all objects in ParaSail effectively live on the stack, even though they may still grow and shrink.  Each scope has its own region, effectively a local heap that expands and contracts to hold the objects associated with the scope.  No garbage collector is needed because when an object goes out of scope, or an object or one of its component is set back to null, the associated storage may be immediately reclaimed, much like storage designated by owning pointers in Rust.  By simplifying the type model, the storage management in ParaSail is dramatically simpler and of higher performance, with no complex parallel garbage collection algorithm required.
5. Implementation Status and Conclusions
The programming language design world has been rejuvenated by the new challenges of distributed and multicore architectures.  Three new programming languages designed for building industrial-strength systems have emerged, Go, Rust, and ParaSail.  Each of these languages tries to make parallel programming simpler and safer, while still providing the level of power and performance needed for the most critical systems development tasks. 

From an implementation status point of view, Go is the most stable of these three new languages, with two compilers available, and a number of systems built with Go now being deployed.  Rust and ParaSail are still under development, but both are available for trial use, with Rust having early on achieved the compiler boot strap milestone, where the Rust compiler is itself implemented in Rust.  All three languages provide opportunities for the professional programmer to expand their horizons, and see what sort of new paradigms and idioms will become more important as we leave behind the era of simple sequential processing, and enter the era of massively parallel, widely distributed computing.
References        
[1]    The Go Programming Language, Google Corporation, http://golang.org
[2]    The Rust Programming Language, Mozilla Research, http://www.rust-lang.org
[3]    ParaSail: Less is More with Multicore, S. Tucker Taft, http://www.embedded.com/design/other/4375616/ParaSail--Less-is-more-with-multicore (retrieved 4/1/2013).
[4]    Designing ParaSail: A New Programming Language, S. Tucker Taft, http://parasail-programming-language.blogspot.com





Sunday, February 17, 2013

ParaSailing without a (garbage) chute

Here is a video of a presentation on ParaSail given at Mozilla Research in Mountain View, CA a couple of weeks ago, to the folks who are developing the Rust language:

https://air.mozilla.org/region-based-storage-management-parasailing-without-a-garbage-chute/

A particular area of interest was the use of pointer-free region-based storage management which eliminates the need for a garbage collector.  Rust has per-task heaps which are garbage collected, but in a single-threaded environment, plus a global heap which is managed more like one of ParaSail's regions, where all pointers into the global heap are unique/owned pointers.  Setting such a pointer to null allows immediate reclamation of the associated storage, preventing accumulation of any garbage.

Slides are on the ParaSail google group:
  https://groups.google.com/forum/#!topic/parasail-programming-language/hMiRrQijNPg

Friday, January 25, 2013

The ParaSail "import" clause -- library level visibility

We have postponed implementing the "import" clause for ParaSail until just recently.  That may seem somewhat surprising.  Up until now, any ParaSail module could refer to any other ParaSail module, provided it used its full expanded name (e.g. a name of the form Acme::Killer_App::Driver).  This works fine for small programs, but as programs grow larger, there is often a desire to be more restrictive (on the one hand) as far as interdependencies, and provide a shorthand (on the other hand) for long module names.

So now in ParaSail, you can both restrict visibility, and create shorthands, using a (Java-like) import clause:

   import Acme::Killer_App::Driver

will both give visibility on this Driver module, and provide a shorthand of simply "Driver" to refer to it.  Alternatively, you can gain visibility on all of the modules in a hierarchy, along with shorthands for the top-level items in the hierarchy:

   import Acme::Killer_App::*

gives visibility on Acme::Killer_App::Driver, Acme_Killer_App::Utilities, etc. as well as allowing the use of the shorthand names "Driver" and "Utilities" directly in the source code that follows the import clause.

In the absence of any applicable import clauses, a default is provided based on the name of the module being compiled.  If the module being compiled has the name "A::B::C" then the default import clause, when none is supplied, is "import A::B::*".  That is, the module has visibility on the entire hierarchy of modules rooted at its parent module A::B, and can use the names of its sibling modules directly as a shorthand (e.g. can refer to A::B::D as simply "D").

Whether or not explicit import clauses apply to a piece of ParaSail source code, the following two implicit import clauses always apply:

    import PSL::Core::*
    import PSL::Containers::*

This ensures visibility on, and shorthands for, the standard ParaSail types such as Boolean and Univ_Integer, and the standard ParaSail containers such as Vector and Map.

Within a single source file containing multiple modules, a given set of import clauses apply to the modules following the import clause(s) up until the end of the source file, or to the next set of import clauses.  Whenever a new set of import clauses is given, the new set completely replace any earlier imports.   So there is no inheritance or accumulation of import clauses.  Only one set applies at a time (after always adding in the implicit ones for PSL::Core::* and PSL::Containers::*).

So why did we choose this particular approach for the import clause, and how does it compare to that of other languages?

Programming languages take many different approaches to controlling visibility between standalone program units defined at the top level of some source file.  C and C++ use a strictly textual #include approach, where what is defined by some included declaration is visible, and what is not declared in any included text file is invisible.  There are many well known troublesome issues with such an approach, but it has the advantage of simplicity.  However, some of the nice simplicity is lost when you add in the notion of "extern" declarations and single-definition rules and such.  Much of the nastiness comes from nested includes, and multiple includes with redefinitions of macros which allow the various includes of the same file to have different effects.

Languages like Modula, Ada, Java, etc., adopted a module-based approach to standalone unit visibility, where an import clause or a with clause or something similar is used to indicate what other modules are available for use within a given module.  The import clause specifies the name of the module or modules to be imported, using their programming language name rather than their file name.  Some implementations of such languages require that modules be stored in files whose filenames are derivable from the module name, but that is not always required, and in Java is only required for a public class or interface, but not for package-visible classes or interfaces that come along for the ride in the same file.  In module-based approaches, there is typically no transitive importing, in that each module must specify the modules on which it wants visibility -- it doesn't get visibility on a module B just because it imports a module A that imports B.  It must import B directly.

Note that Ada does allow the body of a library unit to inherit the with clauses from its spec, as well as from its parent units in the library unit hierarchy.  Hence a with clause on the spec for a package Acme would be inherited by the Acme package body, as well as the Acme.Killer_App spec and body, and so on.  For most other languages, there is no inheriting of import clauses between source files, even when one is defining the implementation for a module and the other is defining the interface for the module.

The new language Go has an interesting approach, where filenames are used in the import clauses, but they are treated more like modules, in that there is no automatic transitivity, and the imported files never need be read more than once, even if they are indirectly imported several times.  Go was designed in part in response to the unpleasant effects of the C++ #include rules, which in large systems can dramatically slow down compilation.

Other solutions to the C/C++ #include unpleasantries exist, some based on a "#pragma Once" approach which says that a given include file should only be read once, and others based on an idiomatic use of #ifndef/#endif's bracketing the text of each include file.

Another dimension over which importing differs is the provision of shorthands for imported declarations.  In Java, for example, no import clause is required if the programmer is willing to refer to an externally-defined class by using its full name (such as java.io.InputStream).  A Java import clause is primarily designed to provide a shorthand for a class/interface name (by writing "import java.io.InputStream"), or for all of the classes/interfaces in a given package (by writing "import java.io.*").  Java also provides an implicit import of java.lang.*, providing shorthands for all of the most basic Java classes and interfaces without need for an explicit import clause.  Modula (and Python) provides a shorthand when using an import clause of the form "from M import A, B" which makes the names A and B directly visible.

Ada has a different approach to shorthands.  The contents of any package may be made directly nameable using a "use" clause (e.g. "use Pkg;" makes Pkg.A nameable as simply "A").

So now back to the approach adopted for ParaSail.  ParaSail follows the Java model as far as shorthands, in that an "import Acme::Killer_App::Driver" has the effect of making "Driver" usable directly within the code that follows.  But unlike in Java, if the programmer gives explicit import clauses, then they restrict what modules the source code may use.  If the name of a module is not covered by one of the explicit import clauses, and is not within PSL::Core::* or PSL::Containers::*, then no reference may be made to it, even with its full name.  What this means is that if an import clause is present, it ensures that no unexpected dependencies on outside modules can occur due to some buried full-name reference.

ParaSail also eschews any inheritance of import clauses from other modules, such as the parent module or the interface of the module.  Import clauses apply to a piece of source code, namely the source code that follows the import clauses, up until the next set of import clauses, or the end of the file.  This means that identifying what other modules are relevant to understanding a given piece of source code is fully circumscribed by the applicable import clauses.

Thursday, January 10, 2013

Rev 3.8 release brings initial I/O library, import clauses

We are pleased to release alpha revision 3.8 of the ParaSail compiler and virtual machine, available at the same URL:

http://bit.ly/Mx9DRb

This release includes a number of new features, including support for the import clause, and a standard library now defining modules with names like PSL::Core::Boolean and PSL::Containers::Set (rather than simply Boolean or Set). There are some new I/O modules, including PSL::Core::IO, PSL::Core::File_Input_Stream, and PSL::Core::File_Output_Stream. These last two support simple textual file I/O. We will be providing additional libraries oriented toward a graphical user interface in an upcoming release. The reference manual has also been updated to cover some of the newer features. As usual, consult the release notes for more details on the 3.8 revision.

We will write a separate blog entry about the design of the import clause -- it brought out some interesting issues. Every ParaSail module has an implicit import of PSL::Core::* and PSL::Containers::* so the introduction of the import clause shouldn't cause any compatibility issues with existing code.

Saturday, November 24, 2012

Rev 3.7 release with map/reduce; Python-like syntax

Revision 3.7 of the alpha release 0.5 of the ParaSail compiler and virtual machine is now available at the same URL we have been using recently:
 
    http://bit.ly/Mx9DRb

This release has a number of new features, including a special syntax for map/reduce style computations, filters added to iterators, and support for an optional Python-like syntax.  For a description and some examples of the new map/reduce syntax, see:

  http://parasail-programming-language.blogspot.com/2012/11/special-syntax-for-mapreduce-in-parasail.html

Here is a simple example using the map/reduce syntax (and a filter) to compute the square of N by summing the first N odd integers:
    func Square(N : Univ_Integer {N >= 0}) -> Univ_Integer is
        return (for I in 1 ..< 2*N {I mod 2 == 1} => <0> + I);
    end func Square;
Filters are mentioned in the above blog entry, and illustrated in the above example. Here is another example where a "functional" version of QSort uses filters with a triplet of container comprehensions.  The filters are immediately before the "=>", enclosed in {...}:
    
    func QSort(Vec : Vector<Element>) -> Vector<Element> is
        if |Vec| <= 1 then
            return Vec;  // The easy case
        else
            const Mid := Vec[ |Vec|/2 ];  // Pick a pivot value
            return 
                QSort( [for each E of Vec {E < Mid} => E] )  // Recurse
              | [for each E of Vec {E == Mid} => E]
              | QSort( [for each E of Vec {E > Mid} => E] ); // Recurse
        end if;
    end func QSort; 
Note that we have added a "magnitude" operator "|...|" which can be defined as appropriate for a given interface; for vectors |V| is equivalent to Length(V).

The ParaSail compiler now recognizes a Python-like variant, in addition to the "normal" ParaSail syntax. In the Python-like syntax variant, "is", "then", "of", and "loop" are replaced by ":"; semicolons are optional; "end if", "end case", etc. are not used. "end class", "end interface, "end func" are optional. Indenting is significant. There are also some Python-inspired synonyms: "def" may be used instead of "func"; "elif" may be used instead of "elsif"; "# " may be used instead of "//" to introduce a comment (notice the required space).

The Python-like syntax is best illustrated by example. Here is the same QSort function given above using the Python-like syntax:
    
    def QSort(Vec : Vector<Element>) -> Vector<Element>:
        if |Vec| <= 1:
            return Vec  # The easy case
        else:
            const Mid := Vec[ |Vec|/2 ]  # Pick a pivot value
            return 
                QSort( [for each E of Vec {E < Mid} => E] )  # Recurse
              | [for each E of Vec {E == Mid} => E]
              | QSort( [for each E of Vec {E > Mid} => E] )  # Recurse
Note that the above is strongly typed (unlike Python), but because of type inference on declarations, types only show up in the parameter and result types.

Currently the two syntax variants can be mixed and matched. At some point we may have some restrictions on mixing and matching. In any case semicolons will remain optional in both variants, and probably will start disappearing from examples given in future blog entries.

Both syntax variants are being supported at the moment as we are still ambivalent about whether the Python-like syntax is preferable. We have tentatively named this ParaSail-with-Python-like-syntax variant "PARython" or perhaps "Parython." We may be adding more scripting-oriented libraries so PARython might be usable in more contexts where Python is chosen today.

Sunday, November 4, 2012

Special syntax for Map/Reduce in ParaSail?

The Map/Reduce operation has become widely known recently.  A traditional way of implementing a Map/Reduce operation is to pass to the map_reduce operation, a container such as a set, vector, or list, a mapping function which is applied to each element, and a reducing function which is used to combine two mapped elements into one result.  The mapping step is relatively straightforward, but the reducing step can come in various forms, as it depends on whether the reducing function is associative, commutative, symmetric, etc.  Most functional languages have at least two standard reducing mechanisms, often called foldl and foldr.  These differ in the way the elements of the sequence are combined, either starting at the left or starting at the right.  Typically there is also an initial value which most often corresponds to the identity element for the reducing function, and this becomes the result if the sequence is empty.  If there is no convenient identity element (such as for maximum), versions of foldl and foldr are provided that use the use the first or last element as the initial value (called foldl1 and foldr1 in Haskell), but then only work on non-empty sequences.  Here is the wikipedia entry on the various forms of fold/reduce:

    http://en.wikipedia.org/wiki/Fold_%28higher-order_function%29

Because ParaSail supports passing functions as parameters to other functions, Map/Reduce in all its forms can be supported in the "traditional" way.  The question is whether some alternative syntax might be useful as well, analogous to the special syntax provided for quantified expressions (which essentially use and or or as the reducing function over a set of boolean values), and for container comprehensions (which essentially use "|" as a reducing operation to combine values into a new container).  As examples, here is a ParaSail quantified expression that asserts an array is sorted:

  (for all I in 1 ..< Length(Vec) => Vec[I] <= Vec[I+1])

and here is a container comprehension that creates a vector of squares:

   const Squares : Vector<Integer> := [for I in 1..10 => I**2]

Quantified expressions and container comprehensions could also be implemented by passing functions as parameters, but clearly the special syntax makes it somewhat more convenient, and arguably easier to understand.  So the question is: is there an analogous situation for the general map/reduce operation?  Would a special syntax make it more convenient and/or easier to understand?

Here is an example using possible syntax for a general map/reduce:

   (for I in 1 .. Length(Vec) => <0> + Vec[I]**2)

This is to be interpreted as a map/reduce operation which produces the sum of the squares of the values of the Vec.  The initial result (in the case of an empty vector) is given inside the <...>, and then after each element is combined with the ongoing result, the new result replaces the <...> for the next element.  Here would be a dot product of two vectors (first we assert they are of the same length):

  {Length(Vec1) == Length(Vec2)}
 (for I in 1 .. Length(Vec1) => <0> + (Vec1[I] * Vec2[I]))

Here is a computation of factorial(N):

   (for I in 1 .. N => <1> * I)

If we wanted to compute the maximum of the elements of some function evaluated over a range of inputs, and we wanted to use the first value as the initial result (analogous to foldl1 in Haskell) we could write (after first asserting we have a non-empty set of inputs):

    {N > 0}
   (for I in 1 <.. N => Max(<F(1)>, F(I)))

This general syntax could be used with any ParaSail iterator, so if we wanted to count the number of symbols in a symbol table whose names are shorter than 3 letters, we could write:

   (for each S of Sym_Table => <0> + (Length(S.Name) < 3? 1: 0))

Here we might better use a filter annotation on the iterator (unfortunately, iterator filters are not yet implemented in the ParaSail compiler):

   (for each S of Sym_Table {Length(S.Name) < 3} => <0> + 1)
   
An explicit reverse or forward could be used with the iterator if we want to distinguish between foldr and foldl for reducing ordered sequences of values.  Otherwise, the default is an unordered parallel reduction.

Note that a quantified expression can be re-expressed in this more general map/reduce syntax.  For example, the above assertion of Vec being sorted could be re-expressed as:

   (for I in 1 ..< Length(Vec) => <#true> and then Vec[I] <= Vec[I+1])

Similarly, a container comprehension could be re-expressed using this general syntax.  For example, the table of squares could be re-expressed as:

   (for I in 1 .. 10 forward => <[]> | [I**2])

Overall this special syntax seems to provide a nice alternative for map/reduce.  In the traditional approach, we would have to "bundle" up the mapping and reducing operations into functions, whereas with this special syntax, we can write the operations directly in a relatively "normal" looking expression syntax.

We would appreciate comments on this proposed special map/reduce syntax.