# # Hash

Hashes represent a list of key-value pairs that can conveniently be accessed with `O(1)`

cost.
Think of JavaScript's objects or Python's dictionaries:

```
h = {"key": "val"}
h.key # "val"
h["key"] # "val"
```

Note that the `hash.key`

hash property form is the preferred one, as it's more concise and mimics other programming languages.

Hash keys must be strings, but hash values can be of any type. Accessing a key that does not exist returns `null`

.

An individual hash element may be assigned to via its `hash["key"]`

index or its property `hash.key`

. This includes compound operators
such as `+=`

. Note that a new key may be created as well using `hash["newkey"]`

or `hash.newkey`

:

```
h = {"a": 1, "b": 2, "c": 3}
h # {a: 1, b: 2, c: 3}
# index assignment
h["a"] = 99
h # {a: 99, b: 2, c: 3}
# property assignment
h.a # 99
h.a = 88
h # {a: 88, b: 2, c: 3}
# compound operator assignment to property
h.a += 1
h.a # 89
h # {a: 88, b: 2, c: 3}
# create new keys via index or property
h["x"] = 10
h.y = 20
h # {a: 88, b: 2, c: 3, x: 10, y: 20}
```

It is also possible to extend a hash using the `+=`

operator
with another hash. Note that any existing keys on the left side
will be replaced with the same key from the right side:

```
h = {"a": 1, "b": 2, "c": 3}
h # {a: 1, b: 2, c: 3}
# extending a hash by += compound operator
h += {"c": 33, "d": 4, "e": 5}
h # {a: 1, b: 2, c: 33, d: 4, e: 5}
```

In a similar way, we can make a **shallow** copy of a hash using
the `+`

operator with an empty hash. Be careful, the empty hash
must be on the left side of the `+`

operator:

```
a = {"a": 1, "b": 2, "c": 3}
a # {a: 1, b: 2, c: 3}
# shallow copy a hash using the + operator with an empty hash
# note well that the empty hash must be on the left side of the +
b = {} + a
b # {a: 1, b: 2, c: 3}
# modify the shallow copy without changing the original
b.a = 99
b # {a: 99, b: 2, c: 3}
a # {a: 1, b: 2, c: 3}
```

If the left side is a `hash["key"]`

or `hash.key`

and the
right side is a hash, then the resulting hash will have a
new nested hash at `hash.newkey`

. This includes `hash["newkey"]`

or `hash.newKey`

as well:

```
h = {"a": 1, "b": 2, "c": 3}
h # {a: 1, b: 2, c: 3}
# nested hash assigned to hash.key
h.c = {"x": 10, "y": 20}
h # {a: 1, b: 2, c: {x: 10, y: 20}}
# nested hash assigned to hash.newkey
h.z = {"xx": 11, "yy": 21}
h # {a: 1, b: 2, c: {x: 10, y: 20}, z: {xx: 11, yy: 21}}
```

Nested hashes can be accessed by chaining keys, though it's best to use `?.`

to "drill down" through keys that may not exist:

```
h = {"a": 1, "b": 2, "c": {"x": 10, "y": 20}, "z": {"xx": 11, "yy": 21}}
h.c.y # 20
h["c"].x # 10
h.z["yy"] #21
h.z.zz # null
h.x.pp
# ERROR: invalid property 'pp' on type NULL
# [1:4] h.x.pp
h.x?.pp # null
```

## # Supported functions

### # items()

Returns an array of [key, value] tuples for each item in the hash. Only the first-level items in a nested hash are returned:

```
h = {"a": 1, "b": 2, "c": 3}
h.items() # [[a, 1], [b, 2], [c, 3]]
items(h) # [[a, 1], [b, 2], [c, 3]]
nh = {"a": 1, "b": 2, "c": {"x": 10, "y": 20}, "z": {"xx": 11, "yy": 21}}
nh.items() # [["a", 1], ["b", 2], ["c", {"x": 10, "y": 20}], ["z", {"xx": 11, "yy": 21}]]
```

### # keys()

Returns an array of keys in the hash. Only the first-level keys in a nested hash are returned:

```
h = {"a": 1, "b": 2, "c": 3}
h.keys() # [a, b, c]
keys(h) # [a, b, c]
nh = {"a": 1, "b": 2, "c": {"x": 10, "y": 20}, "z": {"xx": 11, "yy": 21}}
nh.keys() # ["z", "a", "b", "c"]
```

### # pop(k)

Removes and returns the item matching key `k`

from the hash. If `k`

is not found, `hash.pop(k)`

returns `null`

.

Only the first-level items can be popped.

```
h = {"a": 1, "b": 2, "c": {"x": 10, "y":20}}
h.pop("a") # {a: 1}
h # {b: 2, c: {x: 10, y: 20}}
h.pop(c.x)
ERROR: identifier not found: c
[1:7] h.pop(c.x)
h.pop(c["x"])
ERROR: identifier not found: c
[1:7] h.pop(c["x"])
h.pop("c") # {c: {x: 10, y: 20}}
h # {b: 2}
h.pop("d") # null
h # {b: 2}
```

### # str()

Returns the string representation of the hash:

```
h = {"k": "v"}
h.str() # "{k: v}"
str(h) # "{k: v}"
```

### # values()

Returns an array of values in the hash. Only the first-level values in a nested hash are returned:

```
h = {"a": 1, "b": 2, "c": 3}
h.values() # [1, 2, 3]
values(h) # [1, 2, 3]
nh = {"a": 1, "b": 2, "c": {"x": 10, "y": 20}, "z": {"xx": 11, "yy": 21}}
nh.values() # [1, 2, {"x": 10, "y": 20}, {"xx": 11, "yy": 21}]
```

## # User-defined functions

Since hash values can be of any type, we can create objects with custom functions, such as:

```
hash = {"greeter": f(name) { return "Hello $name!" }}
hash.greeter("Sally") # "Hello Sally!"
```