The Object Constraint Language: Getting Your Models Ready for MDA (2nd Edition)
A number of standard OCL operations enable you to loop over the elements in a collection. These operations take each element in the collection and evaluate an expression on it. Loop operations are also called iterators or iterator operations . Every loop operation has an OCL expression as parameter. This is called the body , or body parameter , of the operation. The following sections explain each of the loop operations in more detail. Table 9-3 shows an overview of the loop operations defined on the four collection types. Table 9-3. Loop operations on all collection types
9.3.1 Iterator Variables
Every iterator operation may have an extra (optional) parameter, an iterator variable . An iterator variable is a variable that is used within the body parameter to indicate the element of the collection for which the body parameter is being calculated. The type of this iterator variable is always the type of the elements in the collection. Because the type is known, it may be omitted in the declaration of the iterator variable. Thus, the next two examples are both correct. The following invariants state that the number of the loyalty account must be unique within a loyalty program: context LoyaltyProgram inv : self.Membership.account->isUnique( acc acc.number ) context LoyaltyProgram inv : self.Membership.account->isUnique( acc: LoyaltyAccount acc.number ) It is recommended to use iterator variables when the type of the elements in the collection has features with the same names as the contextual instance. For example, if the class LoyaltyProgram itself has an attribute number , then leaving out the iterator variable seems to render the preceding constraints ambiguous. Is the number parameter refering to the number of the loyalty program or to the number of the loyalty account? Although the OCL specification clearly defines the meaning of the constraint, the human reader will be guided by the use of iterator variables. The OCL specification states that in a loop expression, namespaces are nested. The innermost namespace is that of the type of the elements of the collection; in this case, LoyaltyAccount . When a name in the body parameter cannot be found in the innermost namespace, it will be searched in other namespaces. First, the namespaces of any enclosing loop expressions will be searched. Next, the namespace of the contextual instance will be searched. Thus, the following invariant is still a correct representation of the intended meaning: context LoyaltyProgram inv : self.Membership.account->isUnique( number ) When the body parameter should refer to the feature of the contextual instance, it should be prefixed self . In the preceding example, this results in a rather pointless, but correct invariant. For every account in the program, the number of the program itself will be tested . This number will be the same for every element of the collection; therefore, the expression will always have false as result: context LoyaltyProgram inv : self.Membership.account->isUnique( self.number ) The iterator variable cannot be omitted in all circumstances. It can be omitted only if an explicit reference to the iterator is not needed in the expression. For example, the following expression cannot be rewritten without use of an iterator variable because of the reference to the iterator: context ProgramPartner inv : self.programs.partners-> select(p : ProgramPartner p <> self) This expression results in the collection of all program partners that are in the same loyalty programs as the context program partner. 9.3.2 The isUnique Operation
Quite often in a collection of elements, we want a certain aspect of the elements to be unique for each element in the collection. For instance, in a collection of employees of a company, the employee number must be unique. To state this fact, we can use the isUnique operation. The parameter of this operation is usually a feature of the type of the elements in the collection. The result is either true or false. The operation will loop over all elements and compare the values by calculating the parameter expression for all elements. If none of the values is equal to another, the result is true; otherwise , the result is false. An example was given in Section 9.3.1. 9.3.3 The sortedBy Operation
In Section 9.1, we mentioned that both Sequence or OrderedSet instances are ordered and not sorted. We can demand an ordering on the elements of any collection using the sortedBy operation. The parameter of this operation is a property of the type of the elements in the collection. For this property, the lesserThan operation (denoted by <) must be defined. The result is a sequence or ordered set, depending on the type of the original collection. Applying this operation to a sequence will result in a sequence; applying this operation to an ordered set will result in an ordered set. The operation will loop over all elements in the original collection and will order all elements according to the value derived from calculating the parameter property. The first element in the result is the element for which the property is the lowest . Again, we take as an example the number of the LoyaltyAccount in the R&L system. The following defines an attribute sortedAccounts that holds all loyalty accounts of a loyalty program sorted by number: context LoyaltyProgram def : sortedAccounts : Sequence(LoyaltyAccount) = self.Membership.account->sortedBy( number ) 9.3.4 The select Operation
Sometimes an expression using operations and navigations results in a collection, but we are interested only in a special subset of the collection. The select operation enables us to specify a selection from the original collection. The result of the select operation is always a proper subset of the original collection. The parameter of the select operation is a boolean expression that specifies which elements we want to select from the collection. The result of select is the collection that contains all elements for which the boolean expression is true. The following expression selects all transactions on a CustomerCard that have more than 100 points : context CustomerCard inv : self.transactions->select( points > 100 )->notEmpty() We can explain the meaning of the select operation in an operational way, but select is still an operation without side effects; it results in a new set. The result of select can be described by the following pseudocode: element = collection.firstElement(); while( collection.notEmpty() ) do if( <expression-with-element> ) then result.add(element); endif element = collection.nextElement(); endwhile return result; 9.3.5 The reject Operation
The reject operation is analogous to select , with the distinction that reject selects all elements from the collection for which the expression evaluates to false. The existence of reject is merely a convenience. The following two invariants are semantically equivalent: context Customer inv : Membership.account->select( points > 0 ) context Customer inv : Membership.account->reject( not (points > 0) ) 9.3.6 The any Operation
To obtain any element from a collection for which a certain condition holds, we can use the any operation. The body parameter of this operation is a boolean expression. The result is a single element of the original collection. The operation will loop over all elements in the original collection and find one element that upholds the condition specified by the body parameter. If the condition holds for more than one element, one of them is randomly chosen . If the condition does not hold for any element in the source collection, the result is undefined (see Section 10.6). Again, we take as example the number of the LoyaltyAccount in the R&L system. The following expression from the context of LoyaltyProgram results in a loyalty account randomly picked from the set of accounts in the program that have a number lower than 10,000: self.Membership.account->any( number < 10000 ) 9.3.7 The forAll Operation
We often want to specify that a certain condition must hold for all elements of a collection. The forAll operation on collections can be used for this purpose. The result of the forAll operation is a boolean value. It is true if the expression is true for all elements of the collection. If the expression is false for one or more elements in the collection, then forAll results in false. For example, consider the following expression: context LoyaltyProgram inv : participants->forAll( age() <= 70 ) This expression evaluates to true if the age of all participants in a loyalty program is less than or equal to 70. If the age of at least one (or more) customers exceeds 70, the result is false. The forAll operation has an extended variant in which multiple iterator variables can be declared. All iterator variables iterate over the complete collection. Effectively, this is a short notation for a nested forAll expression on the collection. The next example, which is a complex way to express the isUnique operation, shows the use of multiple iterator variables: context LoyaltyProgram inv : self.participants->forAll(c1, c2 c1 <> c2 implies c1.name <> c2.name) This expression evaluates to true if the names of all customers of a loyalty program are different. It is semantically equivalent to the following expression, which uses nested forAll operations: context LoyaltyProgram inv : self.participants->forAll( c1 self.participants->forAll( c2 c1 <> c2 implies c1.name <> c2.name )) Although the number of iterators is unrestricted, more than two iterators are seldom used. The multiple iterators are allowed only with the forAll operation and not with any other operation that uses iterators. 9.3.8 The exists Operation
Often, we want to specify that there is at least one object in a collection for which a certain condition holds. The exists operation on collections can be used for this purpose. The result of the exists operation is a boolean. It is true if the expression is true for at least one element of the collection. If the expression is false for all elements in the collection, then the exists operation results in false. For example, in the context of a LoyaltyAccount, we can state that if the attribute points is greater than zero, there exists a Transaction with points greater than zero. context LoyaltyAccount inv : points > 0 implies transactions->exists(t t.points > 0) Obviously, there is a relationship between the exists and the forAll operations. The following two expressions are equivalent: collection->exists( <expression> ) not collection->forAll( not < expression> ) 9.3.9 The one Operation
The one operation gives a boolean result stating whether there is exactly one element in the collection for which a condition holds. The body parameter of this operation, stating the condition, is a boolean expression. The operation will loop over all elements in the original collection and find all elements for which the condition holds. If there is exactly one such element, then the result is true; otherwise, the result is false. Again, we take as an example the number attribute of the LoyaltyAccount class in the R&L system. The following invariant states that there may be only one loyalty account that has a number lower than 10,000: context LoyaltyProgram inv : self.Membership.account->one( number < 10000 ) Note the difference between the any and one operations. The any operation can be seen as a variant of the select operation: its result is an element selected from the source collection. The one operation is a variant of the exists operation: its result is either true or false depending on whether or not a certain element exists in the source collection. 9.3.10 The collect Operation
The collect operation iterates over the collection, computes a value for each element of the collection, and gathers the evaluated values into a new collection. The type of the elements in the resulting collection is usually different from the type of the elements in the collection on which the operation is applied. The following expression from the context of LoyaltyAccount represents a collection of integer values that holds the values of the point attribute in all linked Transaction instances: transactions->collect( points ) You can use this expression to state an invariant on this collection of integer values. For example, you could demand that at least one of the values be 500: context LoyaltyAccount inv : transactions->collect( points )-> exists( p : Integer p = 500 ) The result of the collect operation on a set or bag is a bag and on an ordered set or sequence, the result is a sequence. The result of the collect is always a flattened collection (see Section 9.1.5). 9.3.11 Shorthand Notation for collect
Because the collect operation is used extensively, a shorthand notation has been introduced. This shorthand can be used only when there can be no misinterpretations. Instead of the preceding constraint, we can write context LoyaltyAccount inv : transactions.points->exists(p : Integer p = 500 ) In this expression, transactions is a set of Transaction s; therefore, only the set properties can be used on it. The notation transactions.points is shorthand for transactions->collect(points) . Thus, when we take a property of a collection using a dot, this is interpreted as applying the collect operation, where the property is used as the body parameter. 9.3.12 The collectNested Operation
The collectNested operation iterates over the collection, like the collect operation. Whereas the result of the collect is always a flattened collection (see Section 9.1.5), the result of the collectNested operation maintains the nested structure of collections within collections. The following expression from the context of Customer represents a collection of collections of Service instances. In fact, the type of this expression is Bag(Set(Service)) : self.programs->collect(partners)-> collectNested( deliveredServices ) 9.3.13 The iterate Operation
The iterate operation is the most fundamental and complex of the loop operations. At the same time, it is the most generic loop operation. All other loop operations can be described as a special case of iterate . The syntax of the iterate operation is as follows : collection->iterate( element : Type1; result : Type2 = <expression> <expression-with-element-and-result>) The variable element is the iterator variable. The resulting value is accumulated in the variable result, which is also called the accumulator . The accumulator gets an initial value, given by the expression after the equal sign. None of the parameters is optional. The result of the iterate operation is a value obtained by iterating over all elements in a collection. For each successive element in the source collection, the body expression ( <expression-with-element-and-result> ) is calculated using the previous value of the result variable. A simple example of the iterate operation is given by the following expression, which results in the sum of the elements of a set of integers: Set{1,2,3}->iterate( i: Integer, sum: Integer = 0 sum + i ) In this case, we could have simplified the constraint by using the sum operation defined on Integer . The sum operation is a shortcut for the specific use of the iterate operation. A more complex example of the iterate operation can be found in the R&L model. Suppose that the class ProgramPartner needs a query operation that returns all transactions on services of all partners that burn points. It can be defined as follows: context ProgramPartner def : getBurningTransactions(): Set(Transaction) = self.deliveredServices.transactions->iterate( t : Transaction; resultSet : Set(Transaction) = Set{} if t.oclIsTypeOf( Burning ) then resultSet.including( t ) else resultSet endif ) First, a bag of transactions is obtained by collecting the transactions of all services of this partner. For every successive transaction in the bag, the if expression is evaluated. At the start, resultSet is empty, but when a burning transaction is encountered , it is added to resultSet . If the current element is not a burning transaction, the result of the evaluation of this element is an unchanged resultSet . This value will be used in the evaluation of the next element of the source collection. |