In this section we introduce some advanced features of Orc. These include curried function definitions and curried calls, writing an arbitrary expression as the target of a call, a special syntax for writing calls in an object-oriented style, extensions to pattern matching, and new forms of declarations, and ML-style datatypes.
Table 1.4. Advanced Syntax of Orc
| E | ::= | ... | Expression | |
| | | E G+ | generalized call | ||
| G | ::= | Argument group | ||
( E , ... , E ) | curried arguments | |||
| | | .
field | field access | ||
| P | ::= | ... | Pattern | |
| | | P as X | as pattern | ||
| | |
=X | equality pattern | ||
| | | X( P , ... , P )
| datatype pattern | ||
| D | ::= | ... | Declaration | |
| | |
class X = classname
| class declaration | ||
| | |
type X = DT | ... | DT | datatype declaration | ||
| | |
include " filename "
| inclusion | ||
| DT | ::= | X(_, ... ,_) | Datatype |
In many object-oriented programming languages, one calls a method or accesses a field of an
object using the dot operator; for example, obj.m() calls the method
m of the object obj.
There is a special kind of site call in Orc which serves a similar purpose. One may write
x.msg, for any identifiers x and msg. This treats
the value bound to x as a site, and calls it with a special
message value msg.
If the site understands the message msg (for example, if x is
bound to a Java object with a field called msg), the site interprets the message
and responds with some appropriate value. If the site does not
understand the message sent to it, it does not respond, and no publication occurs.
If x cannot be interpreted as a site, no call is made.
Typically this capability is used so that sites may be syntactically treated like objects,
with multiple methods and fields. For example, a channel c might understand the messages
get and put, to get values from and put values on that channel,
respectively. Such calls would be written c.get(), or c.put(6).
A call such as c.put(6) actually occurs in two steps. First c.put sends the message
put to the site c; this publishes a site whose only purpose
is to put values on the channel. Next, that site is called on the argument
6, sending 6 on the channel. Readers familiar with functional programming
will recognize this technique as currying.
It is sometimes useful to stage the arguments to a function; that is, rather than writing a function on two arguments, one instead writes a function on one argument which returns a function taking the second argument and performing the remainder of the evaluation.
This technique is known as currying and it is common in functional programming languages. We can write curried functions using closures. Suppose we want to define a curried addition function on two arguments, and later apply that function to the arguments 3 and 4. We could write such a program in the following way:
def Sum(a) = ( lambda(b) = a+b ) val f = Sum(3) f(4)This defines a function
Sum which, given an argument a, creates a function
which will take an argument b and add a to b. It then creates the
function which adds 3 to its argument, binds that to f, and then invokes
f on 4 to yield 3+4.
When defining a curried function, we have abstracted it in two steps, and when applying it we have written two separate calls. However, this is verbose and not very clear. Orc has a special syntax for curried function definitions and curried applications that will simplify both of these steps. Function definitions may have multiple argument sequences; they are enclosed in parentheses and concatenated. Curried function calls chain together multiple applications in a similar way. Here is the previous program, written in this simplified syntax:
def Sum(a)(b) = a+b Sum(3)(4)Naturally, this syntax is backwards compatible; e.g. both of the following programs are also equivalent:
def Sum(a) = ( lambda(b) = a+b ) Sum(3)(4)
def Sum(a)(b) = a+b val f = Sum(3) f(4)
Clauses of a function may be defined with both curried arguments and patterns in arguments. However, the combination of these two features can produce unintuitive behavior. Consider the following clausal, curried function definition:
def sum(x)(0) = x def sum(x)(y) = x + y sum(2)(3)One might expect
sum(2)(3) to evaluate to 5. However, this is not
the case; instead sum(2)(3) remains silent due to a pattern matching failure.
This is because the compiler always expands curried definitions before considering patterns,
so the above program is in fact equivalent to:
def sum(x) = ( lambda(0) = x ) def sum(x) = ( lambda(y) = x + y ) sum(2)(3)Now the problem becomes apparent. The first clause will always match when the function is applied to its first argument, and then only one clause is available when the second argument is available: the clause with succeeds only on
0.
Therefore, use caution when writing function definitions that use both currying and clauses.
In taking apart a value with a pattern, it is often useful to capture intermediate results.
val (a,b) = ((1,2),(3,4)) val (ax,ay) = a val (bx,by) = bWe can use the
as keyword to simplify this program fragment, giving a name to an entire
sub-pattern. Here is an equivalent version of the above code.
val ((ax,ay) as a, (bx,by) as b) = ((1,2),(3,4))
When a variable occurs in a pattern, it is bound to the value being matched against that pattern. What
if instead we would like to use the value of a bound variable as a test pattern, in a way similar to
a literal value like 0?
We can use an equality pattern to do this. An equality pattern is a variable name preceded by =.
Consider the following definition, which compares its argument to the value bound to variable x,
returning true if they are equal and false otherwise.
def isx(=x) = true def isx(_) = false
The equality pattern becomes much more useful when it is embedded within other patterns. For example, consider
this definition withz. If one of the arguments passed to it is equal to z, then the function
returns its other argument. Otherwise it remains silent.
def withz(=z,y) = y def withz(x,=z) = x
We have seen Orc's predefined data structures: tuples and lists. Orc also provides the capability for programmers to define their own data structures, using a feature adopted from the ML/Haskell language family called datatypes (also called variants or tagged sums).
Datatypes are defined using the type declaration:
type Tree = Node(_,_,_) | Empty()
This declaration defines two new sites named Node and Empty.
Node takes three arguments, and publishes a tagged value
wrapping those arguments. Empty takes no arguments and does the same.
Once we have created these tagged values, we use a new pattern called a datatype pattern to match them and unwrap the arguments:
type Tree = Node(_,_,_) | Empty() {- Build up a small binary tree -} val l = Node(Empty(), 0, Empty()) val r = Node(Empty(), 2, Empty()) val t = Node(l,1,r) {- And then match it to extract its contents -} t >Node(l,j,r)> l >Node(_,i,_)> r >Node(_,k,_)> ( i | j | k )
One pair of datatypes is so commonly used that it is already predefined in the standard library:
Some(_) and None(). These are used as return values for calls that
need to distinguish between successfully returning a value (Some(v)), and successfully
completing but having no meaningful value to return (None()). For example, a lookup
function might return Some(result) if it found a result, or return None()
if it successfully performed the lookup but found no suitable result.
When Orc is run on top of an object-oriented programming language, classes
from that language may be used as sites in Orc itself, via the class
declaration.
{- Use the String class from Java's standard library as a site -} class String = java.lang.String val s = String("foo") s.concat("bar")
This program binds the variable String to Java's String class.
When it is called as a site, it constructs a new instance of String, passing
the given arguments to the constructor.
This instance of String is a Java object; its methods are called and its fields
are accessed using the dot (.) notation, just as one would expect
in Java. For complete details of how Orc interacts with Java, see
Java Integration.
It is often convenient to group related declarations into units that can be
shared between programs. The include declaration offers a simple
way to do this. It names a source file containing a sequence of Orc declarations;
those declarations are incorporated into the program as if they had textually
replaced the include declaration. An included file may itself contain
include declarations.
{- Contents of fold.inc -} def foldl(f,[],s) = s def foldl(f,h:t,s) = foldl(f,t,f(h,s)) def foldr(f,l,s) = foldl(f,rev(l),s)
{- This is the same as inserting the contents of fold.inc here -} include "fold.inc" def sum(L) = foldl(lambda(a,b) = a+b, L, 0) sum([1,2,3])
Note that these declarations still obey the rules of lexical scope. Also, Orc does not detect shared declarations; if the same file is included twice, its declarations occur twice.