• No results found

Chapter 7 Specifying and Checking Effects for Framework APIs

7.2 Safe, Reusable Parallel Frameworks

7.2.3 Getting More Flexibility

As noted above, the list node container is a somewhat artificial example; it is too specialized to be really useful. We now show how to extend the example to make it more generic. Doing this will require some extensions to the DPJ effect system, as discussed below.

Making the Effects Generic: The first thing that is too restrictive is the bound on the effects of the user-

definedoperateOn. For instance, what if the user wants to specify anoperateOnmethod that reads some other region that is disjoint from R:*, where R is the region bound to RN in the instantiation of the framework interface? That is safe and should be allowed, because it cannot interfere with the effect writes RN:* ofperformOnAll. Yet it is disallowed by the effect specification writes Rin the API.

To address this problem, we use effect polymorphism [82]. We give theOperationinterface an effect parameterE(similar to a region parameter, but it specifies an effect) that becomes bound to an actual effect

when the interface is instantiated into a type. To make this strategy work, we need to solve two problems: (1) constraining the effect arguments so that the effects of invoking the user-supplied method on different objects are noninterfering; and (2) ensuring soundness of subtyping when we add effect parameters.

1 public interface Operation<effect E> {

2 public <region R>void operateOn(ListNode<R> elt) writes R effect E;

3 }

4

5 public <effect E | effect E # reads Cont writes RN:* effect E> 6 void performOnAll(Operation<effect E> op)

7 reads RC writes RN:* effect E;

Figure 7.5: Making the effects of theOperationinterface generic

Constraining the effect arguments: Obviously the framework cannot let the effect variableEbecome bound to an arbitrary effect in the user’s code, because then we would be back to the problem of a user-supplied method with unregulated effects. Instead, we introduce an effect constraint that restricts the effect of the user-supplied method.

Figure 7.5 shows how to write the effect variables and constraints. We define theOperationinterface (line 1) with an effect variable E. We also give theperformOnAllmethod (lines 6–8) a constrained method effect parameterE. After the parameter declaration is a constraint specifying that the effect bound toEmust be noninterfering withreads RC writes RN:* effect E. This constraint ensures that the supplied effect will not interfere with (1) the effectreads RCof reading fields of the container; (2) the effect writes RN:* of updating the nodes; and (3) itself. The latter means that E must either be a read-only effect, or it must be an effect such as a set insert that is declared to commute with itself (see Chapter 3).

As an example, here is a user-supplied method that puts all the Nodeobjects in regionNodeRegion and reads regionGlobalRegionto initialize all the objects with the same global value:

public class MyOperation implements

NodeContainer.Operation<reads GlobalRegion> { public <region R>void operateOn(Node<R> elt)

reads GlobalRegion writes R {

/* Assume global is in region GlobalRegion */

elt.data = global; }

}

Notice that the constraints are satisfied. First, GlobalRegionand NodeRegionare different regions, soreads GlobalRegiondoes not interfere with the effectwrites NodeRegion:*of updating the nodes. Second,reads GlobalRegionis a read-only effect, so it is noninterfering with itself.

As a matter of notation, notice that in lines 6–8 of Figure 7.5, the effect appearing in the constraint on the method effect parameter E(line 6) is identical to the effect of the method for which the parameter is declared (line 8). This is a common case. In this case, as a shorthand, we allow the user to omit the constraint and just declare the parameterE#. Using this shorthand, lines 6–8 of Figure 7.5 would look like this:

public <effect E#>void performOnAll(Operation<effect E> op)

reads RC writes RN:* effect E;

Soundness of subtyping: Once we add class types like C<E>, where E is an effect argument, we need a rule for deciding ifC<E1>is a subtype of C<E2>. We could require thatE1 and E2 be identical effects,

but this would be unnecessarily restrictive. Instead, we letE1be a subeffect ofE2. With this approach, the

key to showing the soundness of effect is to show type preservation, i.e., that the dynamic types of object references always agree with the static types of variables that hold them.

However, enforcing type preservation in the presence of effect variables is tricky. For example, consider the following snippet:

class C<effect E> { C<effect E> f; } C<writes r> x = new C<pure>();

By the subtyping rule stated above, this code is legal. But then what is the static type ofx.f? The obvious answer isC<writes r>(substituting writes rfrom the type of xforEin the declaration off), but this is incorrect. For in that case, a reference of type C<writes r>could be legally assigned tox.f. But the dynamic type ofx.fisC<pure>, andwrites ris not a subeffect ofpure, so the assignment violates type preservation.

As noted in Chapters 3 and 4, a similar problem occurs with Java generic wildcards and in basic DPJ with partially specified RPLs. The solution is to make the static type ofx.f C<effect E′

>, whereE′

is a fresh effect parameter (called a capture parameter). The tricky thing here is that all nonempty effects must be captured when substituted for an effect parameter in a type. This is because all nonempty effects are

essentially wildcards: the runtime effect could be equal to the static effect, or it could be empty (or possibly something else, e.g.,readsR instead ofwritesR, orreadsR1instead ofreadsR1, R2).

Making the Type Generic: The second thing that is too restrictive is that we made the class specialized to

list nodes. Instead, we would like to write a generic class

DisjointContainer<type T, region Cont>.

Notice, however, that there are two places where we used the region argument to the Nodetype to write the API. First, in writing the NodeFactoryinterface (line 10 of Figure 7.4), we used a method-local parameterRin the return type ofcreate. Second, in writing the effect ofperformOnAll(lines 6–8 of Figure 7.5), we used the regionRNto write both the effect constraint and the effect of updating the contained objects. If we just replaced these types with an ordinary type variableT, then we would not be able to write the node factory pattern at all, we would not be able to constrain the effectEproperly, and we would be forced to use a more conservative effect (such aswrites *) for the effect ofoperateOn.

To solve this problem, we can use a type constructor [92, 12] that takes a region argument. In our language, type constructors work as follows:

1. A type variableTcan be declaredtype T<region R>, whereRdeclares a fresh parameter. We call Ra type region parameter, by analogy with a class region parameter, which declares a region parameter in a class definition. When a typeT becomes bound toT,T must have at least one region argument, andRrepresents the first region argument. For instance, ifT = C<r>, thenRrepresents the regionr.

2. We write uses of the variableTasT<r>, wherer is a valid region in scope. Ritself is valid because it was declared in the type variable. T<R>represents the unmodified type provided as an argument to the variable, whileT<r>represents the same type with the region in its first argument position replaced byr.

For convenience, a bare use of T is allowed within the class body, and this is equivalent to T<R> (in other words, the type constructor Talso functions as a type, with implicit argument R). We can also write n parameters (T<region R1,. . .,Rn>) and arguments (T<r1,. . .,rn>), for n ≥ 1. In this case the

left.

1 public interface DisjointContainer<type T<region Elt>, 2 region Cont | Elt:* # Cont> {

3

4 public DisjointContainer(DisjointContainer<T,Cont> cont) writes Cont; 5

6 public <effect E#>DisjointContainer(Factory<T, effect E> fact, int size) 7 writes Cont effect E;

8 public interface Factory<type T<region Elt>, effect E> { 9 public <region R>T<R> create(int i) effect E;

10 }

11

12 public <effect E#>void performOnAll(Operation<T,effect E> op) 13 reads Cont writes Elt:* effect E;

14 public interface Operation<type T<region Elt>, effect E> {

15 public <region R>void operateOn(T<R> elt) writes R effect E;

16 }

17 18 }

Figure 7.6: API for an abstract disjoint container with generic types and effects

Final Container API: Figure 7.6 shows the final disjoint container API. Line 1 declares an interface

DisjointContainerwith one type parameter Tand one region parameter Cont. The type parame- ter has one region parameterEltthat names the first region argument of the type bound toT. In line 11, we writeT<R>to require that the return type ofcreatehave the method region parameterRas its first region argument. In line 16, the regionEltis available to write the effects ofperformOnAll. We do the same thing for the type parameter of theOperationinterface, in line 17.

Here is an example implementation ofoperateOn, wherechas type

DisjointContainer<Node<N>,C>.

public class MyOperation implements

DisjointContainer.Operation<Node<NodeRegion>,pure> { public <region R>void operateOn(Node<R> elt) writes R {

++elt.data; }

}

c.performOnAll(new MyOperation());

This code is identical to the example given in Section 4.2.2, except that it instantiates the generic con- tainer instead of the specialized one. The effect argument is pure, because no effect is needed for this

implementation ofoperateOn, except forwrites R, which is already given by the interface (line 19 of Figure 7.6). The effect of the call toperformOnAllisreads C writes N:*.