Skip to content

TM015 String Dictionaries

Dorai Sitaram edited this page Dec 26, 2015 · 1 revision

String dictionaries

Pyret string dictionaries come in two “brands”: immutable and mutable. To use them in a Pyret environment (file or repl),

import string-dict as SD

In the following, for convenience, we often abbreviate dictionary as dict, immutable string dictionary as SD (or ISD, if we want to emphasize its immutablity); and mutable string dictionary as MSD. The primary string-dict datatype is the immutable, and immutability is implied if no qualifier is used.

Making string dicts

To create an empty ISD, do

isd1 = SD.make-string-dict()

To create an ISD with some initialized contents, do

isd2 = [SD.string-dict: "a", 15, "b", 10]

This creates an ISD that maps "a" to 15 and "b" to 10. The strings "a" and "b" are the keys of the string dict. Their corresponding values are 15 and 10. These latter can be any Pyret values. Keys must be Pyret strings.

The corresponding constructions for MSDs are:

msd1 = SD.make-mutable-string-dict()
msd2 = [SD.mutable-string-dict: "a", 15, "b", 10]
Note
Implementation: A string dict is implemented with an “underlying” JavaScript object. At the initialization stage, the underlying object is either empty (if an empty string dict was created) or a simple hash that maps the initial keys to their initialized values. This holds for ISDs and MSDs. The underlying object representation starts to differ for the two brands as more keys are added to the string dict.
Note
Nomenclature: ISDs and MSDs have matching methods for accessing and querying them, but with slightly different signatures befitting their different goals. To emphasize this, the MSD method names have a -now suffix.

get-value[-now], get[-now]

A string dict’s get-value method takes a key as argument and returns its value if found. If the dict does not map the key to a value, an error is raised.

In addition, there is a get method that returns an option: The option contains the key’s value, if found, or an empty option, if the there is no value.

isd1 = [SD.string-dict: "a", 15]
isd1.get-value("a") => 15
isd1.get-value("b") =e=> Key b not found
isd1.get("a").or-else(42) => 15
isd1.get("b").or-else(42) => 42
msd1 = [SD.mutable-string-dict: "a", 15]
msd1.get-value-now("a") => 15
msd1.get-value-now("b") =e=> Key b not found
msd1.get-now("a").or-else(42) => 15
msd1.get-now("b").or-else(42) => 42
Note
Implementation: See the note for set below.

set[-now]

An ISD’s set method returns a new ISD that differs from the original ISD only in that it maps the key to the new value.

isd1 = [SD.string-dict: "a", 15]
isd1.get-value("a") => 15
isd1.get-value("b") =e=> Key b not found
isd2 = isd1.set("a", 20)
isd3 = isd2.set("b", 25)
isd3.get-value("a") => 20
isd3.get-value("b") => 25
isd2.get-value("a") => 20
isd2.get-value("b") =e=> Key b not found
isd1.get-value("a") => 15
isd1.get-value("b") =e=> Key b not found

An MSD’s set-now takes a key and a new value, and maps the key in the dict to the new value. If the key was already mapped, the old mapping is replaced. Otherwise, a new mapping is created.

msd2.set-now() returns mothing. It is performed purely for its side effect.

msd1 = [SD.mutable-string-dict: "a", 15]
msd1.get-value-now("a") => 15
msd1.get-value-now("b") =e=> Key b not found
msd1.set-now("a", 20)
msd1.set-now("b", 25)
msd1.get-value-now("a") => 20
msd1.get-value-now("b") => 25
Note
Implementation: An MSD’s underlying object simply updates itself on a set-now to include the new mapping of the key, by either modifying an existing mapping, or adding a new one.

An ISD cannot afford to do this, as the original ISD must be preserved. To this end, an ISD set uses a new underlying object that inherits from the original underlying object. The new key mapping is added to this new object.

This requires that the ISD and MSD get[-value] be implemented differently. An MSD’s get[-value]-now can retrieve the key value from the underlying object’s own properties. An ISD’s get[-value], on the other hand, may have to search the prototype chain of the underlying object in order to arrive at the key’s mapping. As an optimization to reduce the access time for subsequent get[-value]`s, an ISD’s `get[-value], once it finds its key in the prototype chain, adds the mapping directly to the underlying object.

Note
Time complexity: For MSDs, set-now and get[-value]-now directly translate to object access on a flat yet dynamic JavaScript object. The complexity of the Pyret set-now and get[-value]-now therefore mirrors JavaScript’s own access complexity. Dynamically growable objects may not give O(1) access in general, but an [implementation note from the Google V8 team](https://developers.google.com/v8/design#prop_access) suggests otherwise.

has-key[-now]

A string dict’s has-key method returns a boolean indicating if the argument key is present or not in the dict.

isd1 = [SD.string-dict: "a", 15]
isd2.has-key("a") => true
isd2.has-key("z") => false
msd1 = [SD.mutable-string-dict: "a", 15]
msd2.has-key-now("a") => true
msd2.has-key-now("z") => false
Note
Implementation: For an ISD, has-key will do the same optimization as get[-value]: If the key is in the prototype chain, it is promoted to the underlying object.

keys[-now]

A string dict’s keys method returns the set of all the keys defined in the dict:

isd1 = [SD.string-dict: "a", 15, "b", 10]
isd1.keys() => [tree-set: "a", "b"]
msd1 = [SD.mutable-string-dict: "a", 15, "b", 10]
msd1.keys-now() => [tree-set: "a", "b"]

remove[-now]

An ISD’s remove method returns a new ISD without the argument key.

isd1 = [SD.string-dict: "a", 15, "b", 10]
isd2 = isd1.remove("a")
isd2.get-value("a") =e=> Key a not found
isd1.get-value("a") => 15

An MSD’s remove-now method deletes the argument key from the MSD. It returns nothing, i.e., it is performed purely for its side effect.

msd1 = [SD.mutable-string-dict: "a", 15, "b", 10]
msd1.remove-now("a")
msd1.get-value-now("a") =e=> Key a not found
Note
Implementation: An MSD’s remove simply deletes the key from the underlying object. An ISD can’t afford to do this, so it creates a new mapping for the key that maps it to undefined.

count[-now]

A string dict’s count method returns the number of defined keys in it.

isd1 = [SD.string-dict: "a", 15, "b", 10, "c", 5]
isd1.count() => 3
msd1 = [SD.mutable-string-dict: "a", 15, "b", 10]
msd1.count-now() => 2

unfreeze

An ISD’s unfreeze method returns a new MSD that has the same keys and values as the ISD.

isd1 = [SD.string-dict: "a", 15, "b", 10, "c", 5]
msd1 = isd1.freeze()
msd1.get-value-now("c") => 5
msd1.count() => 3

Another possible name for this method: defrost.

seal

An MSD’s seal method returns a sealed version of the MSD. A sealed MSD allows access (get-now, get-value-now) but not modification (set-now, remove-now).

msd1 = [SD.mutable-string-dict: "a", 15, "b", 10]
msd2 = msd1.seal()
msd2.get-value-now("a") => 15
msd2.set-now("a", 24) =e=> Cannot modify sealed string dict

However, a sealed string dict can see modifications performed on the unsealed version.

msd1.set-now("a", 24)
msd2.get-value-now("a") => 24

freeze

An MSD’s freeze method returns a new ISD that has the same keys and values as the MSD.

msd1 = [SD.mutable-string-dict: "a", 15, "b", 10]
isd1 = msd1.freeze()
isd1.get-value("a") => 15
isd1.count() => 2

Equality

Two ISDs are “equal” iff they have the same keys bound to the same values. Similarly for two MSDs. An ISD is not equal to an MSD even if the two have the same key/values.

isd1 = [SD.string-dict: "a", 15, "b", 10]
isd2 = [SD.string-dict: "a", 15]
isd3 = isd2.set("b", 10)
isd1 == isd2 => false
isd1 == isd3 => true
msd1 = [SD.mutable-string-dict: "a", 15, "b", 10]
msd2 = [SD.mutable-string-dict: "a", 15]
msd3 = [SD.mutable-string-dict: "a", 15]
msd2.set("b", 10)
msd1 == msd2 => true
msd1 == msd3 => false