1.4. Advanced Features of Orc

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  

1.4.1. Special call forms

1.4.1.1. The . notation

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.

1.4.1.2. 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)

Warning

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.

1.4.2. Extensions to pattern matching

1.4.2.1. As pattern

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) = b
We 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))

1.4.2.2. Equality pattern

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

1.4.3. Datatypes

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.

1.4.4. New forms of declarations

1.4.4.1.  class declaration

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.

1.4.4.2.  include declaration

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.