Dictionary
There is nothing built into Traditional DBL that behaves exactly like Dictionary in .NET. However, DBL contains a few of the necessary building blocks, so we’re going to use this as an opportunity to build our own Dictionary class. This will be a good exercise in using some of the collections and concepts we’ve covered so far.
What’s this useful for?
Depending on your historical context, you might be wondering, why should I care about a dictionary when I can just use an ISAM file? Alternatively, you might be wondering why there isn’t much of a built-in in-memory associative lookup data structure. I’ll start by trying to sell you on the benefits of an in-memory dictionary.
Using in-memory dictionaries offers several benefits:
Speed: Accessing and modifying data in memory is orders of magnitude faster than disk operations.
Efficiency: In-memory operations reduce the overhead of disk I/O, making data processing more efficient.
Simplicity: Working with data in memory often simplifies the code, reducing the complexity associated with file management.
Serialization: There's no need to serialize and deserialize data when it's already in memory. More importantly, there's no need to worry about data structures like string that can't be written directly to ISAM files.
Now it’s time to discuss the downsides, and these are probably why in-memory dictionaries aren’t used much in traditional DBL code. DBL programs often handle large volumes of data, and while the amount of RAM installed on your production servers may have grown significantly over the last 30 years, there are still some operations where you should work in on-disk structures like a temporary ISAM file.
That said, there are still plenty of scenarios where an in-memory dictionary is a good fit. For example, if you need to perform a series of lookups on a small set of data, it’s often more efficient to load the data into memory and perform the lookups there, rather than repeatedly accessing the disk. This is especially true if the data is already in memory, such as when it’s being passed from one routine to another. In such cases, using an in-memory dictionary can be a good option. As with all things architecture and performance related, your mileage may vary, and you should always test your assumptions.
Implementation
Let’s jump into a high-level overview for our custom implementation of a dictionary-like data structure, combining the Symbol Table API with System.Collections.ArrayList to manage string lookups of arbitrary objects.
Overview
- Purpose: To create a dictionary for string-based key lookups.
- Key components:
- Symbol Table API: For handling key-based lookups.
- System.Collections.ArrayList: For storing objects.
- Operations Supported: Add, Find, Delete, and Clear entries.
Class structure
StringDictionary
class
- Purpose: Acts as the main dictionary class.
- Key components:
symbolTableId
: The identifier for the symbol table.objectStore
: An ArrayList to store objects.freeIndices
: An ArrayList to manage free indices inobjectStore
.
KeyValuePair
inner class
- Purpose: Represents a key-value pair.
- Components:
Key
: The key (string).Value
: The value (object).
Constructor: StringDictionary()
- Initializes
objectStore
andfreeIndices
. - Calls
nspc_open
to create a symbol table with specific flags. - Flags used:
D_NSPC_SPACE
: Leading and trailing spaces in entry names are significant.D_NSPC_CASE
: Case sensitivity for entry names.
Destructor: ~StringDictionary()
- Closes the symbol table using
nspc_close
.
Methods overview
-
Add method:
- Adds a new key-value pair to the dictionary.
- Checks for duplicate keys using
nspc_find
. - If no duplicate, adds the key-value pair using
nspc_add
.
-
TryGet method:
- Tries to get the value for a given key.
- Uses
nspc_find
to locate the key. - If found, retrieves the value from
objectStore
.
-
Get method:
- Retrieves the value for a given key.
- Similar to
TryGet
but throws an exception if the key is not found.
-
Set method:
- Sets or updates the value for a given key.
- If the key exists, updates the value.
- If the key doesn’t exist, adds a new key-value pair.
-
Remove method:
- Removes a key-value pair from the dictionary.
- Uses
nspc_find
to locate the key. - Deletes the entry using
nspc_delete
.
-
Contains method:
- Checks if a key exists in the dictionary.
-
Clear Method:
- Clears the dictionary.
- Uses
nspc_reset
to clear the symbol table.
-
Items Method:
- Returns a collection of all key-value pairs in the dictionary.
Internal Methods
-
AddObjectInternal:
- Manages adding objects to the
objectStore
. - Uses
freeIndices
to reuse free slots inobjectStore
.
- Manages adding objects to the
-
RemoveObjectInternal:
- Manages removing objects from
objectStore
. - Adds the index to
freeIndices
.
- Manages removing objects from
Symbol Table API integration
- The Symbol Table API (%NSPC_ADD, %NSPC_FIND, %NSPC_DELETE, etc.) is used for managing keys in the dictionary.
objectStore
holds the actual objects, while the symbol table keeps track of the keys and their corresponding indices inobjectStore
.
Error handling
- The class includes error handling for situations like duplicate keys or keys not found.
Usage
- This
StringDictionary
class can be used for efficient key-value pair storage and retrieval, especially useful in scenarios where the keys are strings and the values are objects of arbitrary types.
import System.Collections
.include 'DBLDIR:namspc.def'
namespace DBLBook.Collections
public class StringDictionary
public class KeyValuePair
public method KeyValuePair
key, @string
value, @object
proc
this.Key = key
this.Value = value
endmethod
public Key, @string
public Value, @Object
endclass
private symbolTableId, i4
private objectStore, @ArrayList
private freeIndices, @ArrayList
public method StringDictionary
proc
objectStore = new ArrayList()
freeIndices = new ArrayList()
symbolTableId = nspc_open(D_NSPC_SPACE | D_NSPC_CASE, 4)
endmethod
method ~StringDictionary
proc
xcall nspc_close(symbolTableId)
endmethod
private method AddObjectInternal, i4
value, @object
proc
if(freeIndices.Count > 0) then
begin
data freeIndex = (i4)freeIndices[freeIndices.Count - 1]
freeIndices.RemoveAt(freeIndices.Count - 1)
objectStore[freeIndex] = value
mreturn freeIndex
end
else
mreturn objectStore.Add(value)
endmethod
private method RemoveObjectInternal, void
index, i4
proc
freeIndices.Add((@i4)index)
;;can't just call removeAt because it would throw off all of the objects that are stored after it
;;so we just add to a free list and manage the slots that way
objectStore[index] = ^null
endmethod
public method Add, void
req in key, @string
req in value, @object
record
existingId, i4
newObjectIndex, i4
proc
if(nspc_find(symbolTableId, key,, existingId) == 0) then
begin
newObjectIndex = AddObjectInternal(new KeyValuePair(key, value))
nspc_add(symbolTableId, key, newObjectIndex)
end
else
throw new Exception("duplicate key")
endmethod
public method TryGet, boolean
req in key, @string
req out value, @object
record
objectIndex, i4
kvp, @object
proc
if(nspc_find(symbolTableId, key,objectIndex) != 0) then
begin
kvp = objectStore[objectIndex]
value = ((@KeyValuePair)kvp).Value
mreturn true
end
else
begin
value = ^null
mreturn false
end
endmethod
public method Get, @object
req in key, @string
record
objectIndex, i4
kvp, @Object
proc
if(nspc_find(symbolTableId, key,objectIndex) != 0) then
begin
kvp = objectStore[objectIndex]
mreturn ((@KeyValuePair)kvp).Value
end
else
throw new Exception("index not found")
endmethod
public method Set, void
req in key, @string
req in value, @object
record
objectIndex, i4
proc
if(nspc_find(symbolTableId, key,objectIndex) != 0) then
begin
objectStore[objectIndex] = new KeyValuePair(key, value)
end
else
Add(key, value)
endmethod
public method Remove, void
req in key, @string
record
objectAccesCode, i4
objectIndex, i4
proc
if((objectAccesCode=%nspc_find(symbolTableId,key,objectIndex)) != 0)
begin
nspc_delete(symbolTableId, objectAccesCode)
RemoveObjectInternal(objectIndex)
end
endmethod
public method Contains, boolean
req in key, @string
proc
mreturn (nspc_find(symbolTableId, key) != 0)
endmethod
public method Clear, void
proc
nspc_reset(symbolTableId)
freeIndices.Clear()
objectStore.Clear()
endmethod
public method Items, [#]@StringDictionary.KeyValuePair
record
itm, @StringDictionary.KeyValuePair
result, [#]@StringDictionary.KeyValuePair
itemCount, int
i, int
proc
itemCount = 0
foreach itm in objectStore
begin
if(itm != ^null)
incr itemCount
end
result = new KeyValuePair[itemCount]
i = 1
foreach itm in objectStore
begin
if(itm != ^null)
begin
result[i] = itm
incr i
end
end
mreturn result
endmethod
endclass
endnamespace