- Chapter 13 Implementation of the Basic Collection Protocol
Implementation of the Basic Collection Protocol
The protocol for the classes in the Collection hierarchy was presented in Chapters 9 and 10. This chapter presents the complete implementation of class Collection and the implementation of the basic protocol for instance creation, accessing, testing, adding, removing, and enumerating for each subclass of Collection. These implementations make effective use of a framework in class Collection that is refined in its subclasses. Messages in Collection are implemented in a very general way or as self subclassResponsibility. Messages are implemented as
if the method depends on the representation of the instances. Each subclass must override such messages to fulfill any "subclass responsibilities." Subclasses may override other messages, for efficiency purposes, with a new method that takes advantage of the representation. A subclass may implement some messages with
which results in a report that the message should not be sent to instances of the class. For example, SequenceableCollections cannot respond to remove:ifAbsent:; therefore the method is implemented as self shouldNotImplement.
❏ Collection instance creation protocol
In addition to the messages new and new:, an instance of a Collection can be created by sending any one of four messages made up of one, two, three, or four occurrences of the keyword with:. The messages new and new: are not reimplemented in Collection; they produce an instance that is an empty collection. Each of the other four instance creation methods is specified in Collection in a similar way. First an instance is created (with the expression self new) and then the arguments, in order, are added to the instance. The new instance is returned as the result. The instance is created using self new, rather than super new or self basicNew, because a subclass of Collection might reimplement the message new. Any subclass of Collection that represents fixed-size objects with indexed instance variables must reimplement the following instance creation messages since such a subclass cannot provide an implementation for new.
instance creation with: anObject | newCollection | newCollection ← self new. newCollection add: anObject. ↑newCollection with: firstObject with: secondObject | newCollection | newCollection ← self new. newCollection add: firstObject. newCollection add: secondObject. ↑newCollection with: firstObject with: secondObject with: thirdObject | newCollection | newCollection ← self new. newCollection add: firstObject. newCollection add: secondObject. newCollection add: thirdObject. ↑newCollection with: firstObject with: secondObject with: thirdObject with: fourthObject | newCollection | newCollection ← self new. newCollection add: firstObject. newCollection add: secondObject. newCollection add: thirdObject. newCollection add: fourthObject. ↑newCollection
The implementation of each of the instance creation messages depends on the ability of the newly-created instance to respond to the message add:. Class Collection cannot provide implementations of the following messages because they depend on the representation used by a subclass:
add: anObject remove: anObject ifAbsent: aBlock do: aBlock
All other messages in the basic collection protocol are implemented in terms of these three messages. Each subclass must implement the three basic messages; each can then reimplement any others in order to improve its performance.
❏ Collection adding protocol
The protocol for adding elements to a collection is implemented in class Collection as follows.
adding add: anObject self subclassResponsibility addAll: aCollection aCollection do: [ :each | self add: each ]. ↑aCollection
Notice that the implementation of addAll: depends on both do: and add:. The order of adding elements from the argument, aCollection, depends on both the order in which the collection enumerates its elements (do:) and the manner in which the elements are included into this collection (add:).
❏ Collection removing protocol
The messages remove: and removeAll: are implemented in terms of the basic message remove:ifAbsent:, which must be provided in a subclass. These methods report an error if the element to be removed is not in the collection. The method remove:ifAbsent: can be used to specify different exception behavior.
removing remove: anObject ifAbsent: exceptionBlock self subclassResponsibility remove: anObject self remove: anObject ifAbsent: [self errorNotFound] removeAll: aCollection aCollection do: [ :each | self remove: each]. ↑aCollection private errorNotFound self error: 'Object is not in the collection'
As usual, the category private refers to messages introduced to support the implementations of other messages; it is not to be used by other objects. Most error messages that are used more than once will be specified as private messages in order to create the literal message string once only.
❏ Collection testing protocol
All the messages in the protocol for testing the status of a collection can be implemented in Collection.
testing isEmpty ↑self size = 0 includes: anObject self do: [ :each | anObject = each ifTrue: [↑true]]. ↑false occurrencesOf: anObject | tally | tally ← 0. self do: [ :each | anObject = each ifTrue: [tally ← tally + 1]]. ↑tally
The implementations of includes: and occurrencesOf: depend on the subclass's implementation of the basic enumerating message do:. The block argument of do: in the method for includes: terminates as soon as an element equal to the argument is found. If no such element is found, the last expression (↑false) is evaluated. The response to isEmpty and includes: are Boolean objects, true or false. The message size is inherited from class Object, but is reimplemented in Collection because size, as defined in Object, is only nonzero for variable-length objects.
accessing size | tally | tally ← 0. self do: [ :each | tally ← tally + 1]. ↑tally
This is a low-performance approach to computing the size of a collection which, as we shall see, is reimplemented in most of the subclasses.
❏ Collection enumerating protocol
An implementation of all of the messages that enumerate the elements of collections, except do:, can be provided in class Collection.
enumerating do: aBlock self subclassResponsibility collect: aBlock | newCollection | newCollection ← self species new. self do: [ :each | newCollection add: (aBlock value: each)]. ↑newCollection detect: aBlock ↑self detect: aBlock ifNone: [self errorNotFound] detect: aBlock ifNone: exceptionBlock self do: [ :each | (aBlock value: each) ifTrue: [↑each]]. ↑exceptionBlock value inject: thisValue into: binaryBlock | nextValue | nextValue ← thisValue. self do: [ :each | nextValue ← binaryBlock value: nextValue value: each]. ↑nextValue reject: aBlock ↑self select: [ :element | (aBlock value: element) = = false] select: aBlock | newCollection | newCollection ← self species new. self do: [ :each | (aBlock value: each) ifTrue: [newCollection add: each]]. ↑newCollection
In the methods associated with collect: and select:, the message species is sent to self. This message was not shown in Chapter 9 because it is not part of the external protocol of collections. It is categorized as private to indicate the intention for internal use only. The message is implemented in class Object as returning the class of the receiver.
private species ↑self class
Thus the expression
self species new
means "create a new instance of the same class as that of the receiver."
For some collections, it may not be appropriate to create a "similar" instance in this way; a new collection that is like it may not be an instance of its class. Such a collection will override the message species. In particular, an Interval responds that its species is Array (because it is not possible to modify an Interval); the species of a MappedCollection is the species of the collection it maps (since the MappedCollection is simply acting as an accessor for that collection).
If a collection cannot create an instance by simply sending the class the message new, it must reimplement messages collect: and select:. Since reject: is implemented in terms of select:, it need not be reimplemented.
The method for inject:into: evaluates the block argument once for each element in the receiver. The block is also provided with its own value from each previous evaluation; the initial value is provided as the argument of inject:. The final value of the block is returned as the value of the inject:into: message.
The reason for the introduction of two messages, detect: and detect:ifNone:, is similar to the reason for the two removing messages, remove: and remove:ifAbsent:. The general case (detect:) reports an error if no element meeting the detection criterion is found; the programmer can avoid this error report by specifying an alternative exception (detect: ifNone:).
❏ Collection converting protocol
The protocol for converting from any collection into a Bag, Set, OrderedCollection, or SortedCollection is implemented in a straightforward way--create a new instance of the target collection, then add to it each element of the receiver. In most cases, the new instance is the same size as the original collection. In the case of OrderedCollections, elements are added at the end of the sequence (addLast:), regardless of the order of enumerating from the source.
converting asBag | aBag | aBag ← Bag new. self do: [ :each | aBag add: each]. ↑aBag asOrderedCollection | anOrderedCollection | anOrderedCollection ← OrderedCollection new: self size. self do:[ :each | anOrderedCollection addLast: each]. ↑anOrderedCollection asSet | aSet | aSet ← Set new: self size. self do: [ :each | aSet add: each]. ↑aSet asSortedCollection | aSortedCollection | aSortedCollection ← SortedCollection new: self size. aSortedCollection addAll: self. ↑aSortedCollection asSortedCollection: aBlock | aSortedCollection | aSortedCollection ← SortedCollection new: self size. aSortedCollection sortBlock: aBlock. aSortedCollection addAll: self. ↑aSortedCollection
❏ Collection printing protocol
The implementations of the printOn: and storeOn: messages in Object are overridden in Collection. Collections print in the form
className (element element element )
Collections store themselves as an expression from which an equal collection can be constructed. This takes the form of
((className new) add: element; yourself)
((className new) add: element; add: element; yourself)
with the appropriate number of cascaded messages for adding each element, depending on whether the collection has no, one, or more elements. The message yourself returns the receiver of the message. It is used in cascaded messages to guarantee that the result of the cascaded message is the receiver. All objects respond to the message yourself; it is defined in class Object.
The general methods for printing and storing are
printing printOn: aStream | tooMany | tooMany ← aStream position + self maxPrint. aStream nextPutAll: self class name, '('. self do: [ :element | aStream position > tooMany ifTrue: [aStream nextPutAll: '...etc...). ↑self]. element printOn: aStream. aStream space]. aStream nextPut: $) storeOn: aStream | noneYet | aStream nextPutAll: '(('. aStream nextPutAll: self class name. aStream nextPutAll: 'new)'. noneYet ← true. self do: [ :each | noneYet ifTrue: [noneYet ← false] ifFalse: [aStream nextPut: $;]. aStream nextPutAll: 'add:'. aStream store: each]. noneYet i↑false: [aStream nextPutAll: '; yourself']. aStream nextPut: $) private maxPrint ↑5000
These methods make use of instances of a kind of Stream that acts as an accessor for a String. The method printOn: sets a threshold for the length of the String to be created; a long collection may print as
className (element element ...etc... )
The threshold is determined as the response to the message maxPrint which is set at 5000 characters. Subclasses can override the private message maxPrint in order to modify the threshold.
Note that this technique of using a method rather than a variable is a way of providing a parameter in a method. A variable cannot be used as the parameter because the variable, to be accessible to all instances, would have to be a class variable. Subclasses cannot specify a class variable whose name is the same as a class variable in one of its superclasses; thus if a subclass wants to change the value of the variable, it will do so for instances of its superclass as well. This is not the desired effect.
The printing format is modified in several subclasses. Array does not print its class name; Intervals print using the shorthand notation of the messages to: and to:by: to a Number. A Symbol prints its characters (without the ~ prefix of the literal form of a Symbol); a String prints its characters delimited by single quotes.
The storeOn: message is reimplemented in ArrayedCollection and several of its subclasses because instances are created using new: anInteger rather than simply new. Arrays, Strings, and Symbols store in their literal forms. Intervals use the shorthand notation of messages to: and to:by:. MappedCollections store using the converting message mappedBy: that is sent to the collection that is indirectly accessed.
Subclasses of Collection
For each subclass of Collection, we show the methods that implement the three required messages (add:, remove:ifAbsent:, and do:) and the messages in the adding, removing, testing, and enumerating protocols that are reimplemented. New collection protocol for a particular subclass as specified in Chapter 9 will generally not be presented in this chapter.
Bag represents an unordered collection in which an element can appear more than once. Since the elements of Bags are unordered, the messages at: and at:put: are reimplemented to report an error.
Instances of Bag have an instance of Dictionary as a single instance variable named contents. Each unique element of a Bag is the key of an Association in contents; the value of an Association is an Integer representing the number of times the element appears in the Bag. Removing an element decrements the tally; when the tally falls below 1, the Association is removed from contents. Bag implements new, size, includes:, and occurrencesOf:. A new instance must initialize its instance variable to be a Dictionary. The reimplementation of size is made efficient by summing all the values of elements of contents. The arguments of the testing messages are used as keys of contents. In implementing includes:, the responsibility for checking is passed to contents. In order to answer the query occurrencesOf: anObject, the method checks that anObject is included as a key in contents and then looks up the value (the tally) associated with it.
|instance variable names||contents|
instance creation new ↑super new setDictionary
accessing at: index self errorNotKeyed at: index put: anObject self errorNotKeyed size | tally | tally ← 0 contents do: [ :each | tally ← tally + each]. ↑tally testing includes: anObject ↑contents includesKey: anObject occurrencesOf: anObject (self includes: anObject) ifTrue: [↑contents at: anObject] ifFalse: [↑0] private setDictionary contents ← Dictionary new (in Collection) private errorNotKeyed self error: self class name, 's do not respond to keyed accessing messages'
To add an element is to add it once, but Bags can add multiple times. The implementation of add: calls on add:withOccurrences:. Removing an element checks the number of occurrences, decrementing the tally or removing the element as a key in contents if the tally is less than 1.
adding add: newObject ↑self add: newObject withOccurrences: 1 add: newObject withOccurrences: anInteger contents at: newObject put: anInteger + (self occurrencesOf: newObject). ↑newObject removing remove: oldObject ifAbsent: exceptionBlock | count | count ← self occurrencesOf: oldObject. count = 0 ifTrue: [↑exceptionBlock value]. count = 1 ifTrue: [contents removeKey: oldObject] ifFalse: [contents at: oldObject put: count - 1]]. ↑oldObject
Enumerating the elements of a Bag means selecting each element of the Dictionary and evaluating a block with the key of that element (i.e., the actual Bag element is the key of the Dictionary). This has to be done multiple times, once for each occurrence of the element, as indicated by the value associated with the key.
enumerating do: aBlock contents associationsDo: [ :assoc | assoc value timesRepeat: [aBlock value: assoc key]]
The elements of Sets are unordered like those of Bags, so the messages at: and at:put: produce an error report. A Set may not contain an element more than once, therefore, every insertion of an element must, in theory, check the entire collection. To avoid searching all elements, a Set determines where in its indexed instance variables to start a search for a particular element by using a hashing technique.
Each Set has an instance variable named tally. Maintaining this tally of the number of elements avoids the inefficiencies involved in determining the size of the Set by counting every non-nil element. Thus new, new:, and size are reimplemented; the first two in order to initialize the variable tally and the last simply to respond with the value of tally.
|instance variable names||tally|
instance creation new ↑self new: 2 new: anInteger ↑(super new: anInteger) setTally
accessing at: index self errorNotKeyed at: index put: anObject self errorNotKeyed size ↑tally private setTally tally ← 0
In the method for new:, super is used in order to avoid recursion. A private message of Set, findElementOrNil:, hashes the argument to produce the index at which to begin the probe of the Set. The probe proceeds until the argument, anObject, is found, or until nil is encountered. The response is the index of the last position checked. Then the testing messages are implemented as
testing includes: anObject ↑(self basicAt: (self findElementOrNil: anObject)) ~~ nil occurrencesOf: anObject (self includes: anObject) ifTrue: [↑1] ifFalse: [↑0]
The number of occurrences of any element in the Set is never more than 1. The three basic messages must make use of basicAt: and basicAt:put: because Sets report an error if at: or at:put: is used.
adding add: newObject | index | newObject isNil ifTrue: [↑newObject]. index ← self findElementOrNil: newObject. (self basicAt: index)isNil ifTrue: [self basicAt: index put: newObject, tally ~ tally + 1]. ↑newObject removing remove: oldObject ifAbsent: aBlock | index | index ← self find: oldObject ifAbsent: [↑aBlock value]. self basicAt: index put: nil. tally ← tally - 1. self fixCollisionsFrom: index. ↑oldObject enumerating do: aBlock 1 to: self basic Size do: [ :index | (self basicAt: index) isNil ifFalse: [aBlock value: (self basicAt: index)]]
The private message find:ifAbsent: calls on findElementOrNil:; if the element, oldObject, is not found, the argument aBlock is evaluated. In order to guarantee that the hashing/probing technique works properly, remaining elements might need to be compacted whenever one is removed (fixCollisionsFrom:). These methods are good examples of when the accessing messages basicAt:, basicAt:put:, and basicSize must be used.
A Dictionary is a collection of Associations. Class Dictionary uses a hashing technique to locate its elements which is like that of its superclass, Set, but hashes on the keys in the Associations instead of on the Associations themselves. Most of the accessing messages for Dictionary are reimplemented to treat the values of the Associations as the elements, not the Associations themselves.
Dictionary implements at: and at:put:, but redefines the argument associated with the keyword at: to be any key in the Dictionary (not necessarily an Integer index). The argument of includes: is the value of one of the Associations in the Dictionary, not one of the Associations themselves. The message do: enumerates the values, not the Associations. The argument to remove: is also a value, but this is an inappropriate way to delete from a Dictionary because elements are referenced with keys. Either removeAssociation: or removeKey: should be used. Thus the messages remove: and remove:ifAbsent: should not be implemented for Dictionary.
Much of the work in the accessing protocol is done in private messages, either those inherited from Set or similar ones for finding a key (findKeyOrNil:).
accessing at: key ↑self at: key ifAbsent: [self errorKeyNotFound] at: key put: anObject | index element | index ← self findKeyOrNil: key. element ← self basicAt: index. element isNil ifTrue: [self basicAt: index put: (Association key: key value: anObject). tally ← tally + 1] " element is an Association. The key already exists, change its value." ifFalse: [element value: anObject]. ↑anObject at: key ifAbsent: aBlock | index | index ← self findKey: key ifAbsent: [↑aBlock value]. ↑(self basicAt: index) value testing includes: anObject "Revert to the method used in Collection." self do: [ :each | anObject = each ifTrue: [↑true]]. ↑false adding add: anAssociation | index element | index ← self findKeyOrNil: anAssociation key. element ← self basicAt: index. element isNil ifTrue: [self basicAt: index put: anAssociation. tally ← tally + 1] ifFalse: [element value: anAssociation value]. ↑anAssociation removing remove: anObject ifAbsent: aBlock self shouldNotImplement enumerating do: aBlock self associationsDo: [ :assoc | aBlock value: assoc value] private errorKeyNotFound self error: 'key not found'
Notice the similarity between at:put: and add:. The difference is in the action taken if the element is not found--in the case of at:put:, a new Association is created and stored in the Dictionary; in the case of add:, the argument, anAssociation, is stored so that any shared reference to the Association is preserved.
The message collect: is reimplemented in order to avoid the problems of collecting possibly identical values into a Set which would result in throwing away duplications. The message select: is reimplemented in order to select Associations by applying their values as the arguments to the block.
enumerating collect: aBlock | newCollection | newCollection ← Bag new. self do: [:each I newCollection add: (aBlock value: each)]. ↑newCollection select: aBlock | newCollection | newCollection ← self species new. self associationsDo: [ :each | (aBlock value each value)ifTrue: [newCollection add: each]]. ↑newCollection
IdentityDictionary overrides at:, at:put:, and add: in order to implement checking for identical keys instead of equal keys. An IdentityDictionary is implemented as two parallel ordered collections of keys and values, rather than as a single collection of Associations. Thus do: must also be reimplemented. The implementation is not shown.
SequenceableCollection is the superclass for all collections whose elements are ordered. Of the messages we are examining, remove:ifAbsent: is specified as being inappropriate for SequenceableCollections in general, since the order of elements might have been externally specified and it is assumed that they should be removed in order. Because SequenceableCollections are ordered, elements are accessed using at:; the implementation is provided in class Object. The message do: is implemented by accessing each element at index 1 through the size of the collection. SequenceableCollections are created using the message new:. Therefore, collect: and select: must be reimplemented to create the new collection using new: rather than new. The methods for collect: and select: shown next use a WriteStream in order to access the new collection, and the message at: in order to access elements of the original collection.
accessing size self subclassResponsibility removing remove: oldObject ifAbsent: anExceptionBlock self shouldNotImplement enumerating do: aBlock | index length | index ← 0. length ← self size. [(index ← index + 1) < = length] whileTrue: [aBlock value: (self at: index)] collect: aBlock | aStream index length | aStream ← WriteStream on: (self species new: self size). index ← O. length ← self size. [(index ← index + 1) < = length] whileTrue: [aStream nextPut: (aBlock value: (self at: index))]. ↑aStream contents select: aBlock | aStream index length | aStream ← WriteStream on: (self species new: self size). index ← O. length ← self size. [(index ← index + 1) < = length] whileTrue: [(aBlock value: (self at: index)) ifTrue: [aStream nextPut: (self at: index)]]. ↑aStream contents
Notice that size is declared as a subclass responsibility in SequenceableCollection. The method inherited from the superclass Collection uses do: to enumerate and thereby count each element. But the method for do: as specified in SequenceableCollection determines the limit for indexing by requesting the size of the collection. Therefore, size must be reimplemented in order not to be stated in terms of do:.
Subclasses of SequenceableCollection
❏ Class LinkedList
Elements of LinkedList are instances of Link or of one of its subclasses. Each LinkedList has two instance variables, a reference to the first and to the last elements. Adding an element is assumed to be interpreted as adding to the end (addLast:); the method for addLast: is to make the element the next link of the current last link. Removing an element means that the element's preceding link must reference the element's succeeding link (or nil). If the element to be removed is the first one, then its succeeding link becomes the first one.
|instance variable names||firstLink|
accessing at: index | count element size | count ← 1. element ← self first. size ← self size. [count > size] whileFalse: [count = index ifTrue: [↑element] ifFalse: [count ← count + 1. element ← element nextLink]]. ↑self errorSubscriptBounds: index at: index put: element self error: 'Do not store into a LinkedList using at:put:' adding add: aLink ↑self addLast: aLink addLast: aLink self isEmpty ifTrue: [firstLink ← aLink] ifFalse: [lastLink nextLink: aLink]. lastLink ← aLink. ↑aLink removing remove: aLink ifAbsent: aBlock | tempLink | aLink = = firstLink ifTrue: [firstLink ~ aLink nextLink. aLink = = lastLink ifTrue: [lastLink ←nil]] ifFalse: [tempLink ← firstLink. [tempLink isNil ifTrue: [↑aBlock value]. tempLink nextLink = = aLink] whileFalse: [tempLink ← tempLink nextLink]. tempLink nextLink: aLink nextLink. aLink = = lastLink ifTrue: [lastLink ← tempLink]]. aLink nextLink: nil. ↑aLink enumerating do: aBlock | aLink | aLink ← firstLink. [aLink isNil] whileFalse: [aBlock value: aLink. aLink ← aLink nextLink]
A nil link signals the end of the LinkedList. Thus the enumerating message do: is implemented as a simple loop that continues until a nil is encountered in the collection.
❏ Class Interval
Intervals are SequenceableCollections whose elements are computed. Therefore, messages for adding and removing cannot be supported. Since elements are not explicitly stored, all accessing (at:, size, and do:) requires a computation. Each method checks to see if the last element computed is to be incremented (positive step) or decremented (negative step) in order to determine whether the limit (stop) has been reached.
|instance variable names||start|
instance creation from: startInteger to: stopInteger ↑self new setFrom: startInteger to: stopInteger by: 1 from: startInteger to: stopInteger by: stepInteger ↑self new setFrom: startInteger to: stopInteger by: stepInteger
accessing size step < 0 ifTrue: [start < stop ifTrue: [↑O] ifFalse: [↑stop -- start // step + 1]] ifFalse: [stop < start ifTrue: [↑O] ifFalse: [↑stop -- start // step + 1]] at: index (index > = 1 and: [index < = self size]) ifTrue: [↑start + (step * (index - 1))] ifFalse: [self errorSubscriptBounds: index] at: index put: anObject self error: 'you cannot store into an Interval' adding add: newObject self error: 'elements cannot be added to an Interval' removing remove: newObject self error: 'elements cannot be removed from an Interval' enumerating do: aBlock | aValue | aValue ← start. step < 0 ifTrue: [[stop < = aValue] whileTrue: [aBlock value: aValue. aValue ← aValue + step]] ifFalse: [[stop > = aValue] whileTrue: [aBlock value: aValue. aValue ← aValue + step]] collect: aBlock | nextValue i result | result ← self species new: self size. nextValue ← start. i ← 1. step < 0 ifTrue: [[stop < = nextValue] whileTrue: [result at: i put: (aBlock value: nextValue). nextValue ← nextValue + step. i ← i + 1]] ifFalse: [[stop > = nextValue] whileTrue: [result at: i put: (aBlock value: nextValue). nextValue ← nextValue + step. i ← i + 1]]. ↑result private setFrom: startInteger to: stopInteger by: stepInteger start ← startInteger. stop ← stopInteger. step ← stepInteger
❏ ArrayedCollections--Array, ByteArray, String, Text, and Symbol ArrayedCollection is a subclass of SequenceableCollection; each ArrayedCollection is a variable-length object. All instance creation methods are reimplemented to use new:, not new. ArrayedCollections are fixed-length so add: is disallowed; in its superclass, remove: was already disallowed and do: was implemented. only size, therefore, is implemented in ArrayedCollection--it is a system primitive that reports the number of indexed instance variables.
Of the subclasses of ArrayedCollection, Array, and ByteArray do not reimplement any of the messages we are examining in this chapter. Accessing messages for String--at:, at:put:, and size--are system primitives; in Text, all accessing messages are passed as messages to the instance variable string (which is an instance of String). Symbol disallows at:put: and returns String as its species.
❏ OrderedCollections and SortedCollections
OrderedCollection stores an ordered, contiguous sequence of elements. Since OrderedCollections are expandable, some efficiency is gained by allocating extra space for the sequence. Two instance variables, firstIndex and lastIndex, point to the first and the last actual elements in the sequence.
The index into OrderedCollection is converted to be within the range of firstIndex to lastIndex for accessing messages (at: and at:put:) and the size is simply one more than the difference between the two indices. Adding an element is interpreted to be adding to the end; if there is no room at the end, the collection is copied with additional space allocated (makeRoomAtLast is the private message that does this work). The actual location for storing an element is the computed index position after lastIndex. If an element is removed, then the remaining elements must be moved up so that elements remain contiguous (removeIndex:).
|instance variable names||firstIndex|
instance creation new ↑self new: 10 new: anInteger ↑(super new: anInteger) setIndices
accessing size ↑lastIndex - firstIndex + 1 at: anInteger (anInteger < 1 or: [anInteger + firstIndex - 1 > lastIndex]) ifTrue: [self errorNoSuchElement] ifFalse: [↑super at: anInteger + firstIndex - 1] at: anInteger put: anObject | index | index ← anInteger truncated. (index < 1 or: [index + firstIndex - 1 > lastIndex]) ifTrue: [self errorNoSuchElement] ifFalse: [↑super at: index + firstIndex - 1 put: anObject] adding add: newObject ↑self addLast: aLink addLast: newObject lastIndex = self basicSize ifTrue: [self makeRoomAtLast]. lastIndex ← lastIndex + 1. self basicAt: lastIndex put: newObject. ↑newObject removing remove: oldObject ifAbsent: absentBlock | index | index ← firstIndex. [index < = lastIndex] whileTrue: [oldObject = (self basicAt: index) ifTrue: [self removeIndex: index. ↑oldObject] ifFalse: [index ← index + 1]]. ↑absentBlock value private setIndices firstIndex ← self basicSize // 2 max: 1. lastIndex ← firstIndex - 1 max: 0 errorNoSuchElement self error: 'attempt to index non-existent element in an ordered collection'
The enumerating messages do:, collect:, and select: are each reimplemented--do: in order to provide better performance than the method provided in SequenceableCollection.
enumerating do: aBlock | index | index ← firstIndex. [index < = lastIndex] whileTrue: [aBlock value: (self basicAt: index). index ← index + 1] collect: aBlock | newCollection | newCollection ← self species new. self do: [:each | newCollection add: (aBlock value: each)]. ↑newCollection select: aBlock | newCollection | newCollection ← self copyEmpty. self do: [ :each | (aBlock value: each) ifTrue: [newCollection add: each]]. ↑newCollection
In the method for select:, the new collection is created by sending the original collection the message copyEmpty. This message creates a new collection with enough space allocated to hold all the elements of the original, although all the elements might not be stored. In this way, time taken in expanding the new collection is avoided.
SortedCollection is a subclass of OrderedCollection. The message at:put: reports an error, requesting the programmer to use add:; add: inserts the new element according to the value of the instance variable sortBlock. The determination of the position for insertion is done as a "bubble sort." collect: is also reimplemented to create an OrderedCollection rather than a SortedCollection for collecting the values of the block. The code is not shown; a bubble sort looks the same in Smalltalk-80 as it would in most programming languages.
Instances of MappedCollection have two instance variables--domain and map. The value of domain is either a Dictionary or a SequenceableCollection; its elements are accessed indirectly through map. The message add: is disallowed. Both at: and at:put: are reimplemented in MappedCollection in order to support the indirect access from map to the elements of domain. The size of a MappedCollection is the size of its domain.
|instance variable names||domain|
instance creation collection: domainCollection map: mapCollection ↑super new setCollection: domainCollection map: mapCollection new self error: 'use collection:map: to create a Mapped Collection'
accessing at: anIndex ↑domain at: (map at: anIndex) at: anIndex put: anObject ↑domain at: (map at: anIndex) put: anObject size ↑map size adding add: newObject self shouldNotImplement enumerating do: aBlock map do: [ :mapValue | aBlock value: (domain at: mapValue)] collect: aBlock | aStream | aStream ← WriteStream on: (self species new: self size). self do: [ :domainValue | aStream nextPut: (aBlock value: domainValue)]. ↑aStream contents select: aBlock | aStream | aStream ← WriteStream on: (self species new: self size). self do: [ :domainValue | (aBlock value: domainValue) ifTrue: [aStream nextPut: domainValue]]. ↑aStream contents private setCollection: domainCollection map: mapCollection domain ← domainCollection. map ← mapCollection species ↑domain species