ESC
Type to search...
S
Soli Docs

Hashes

Key-value collections (dictionaries/maps) with powerful methods for data manipulation.

Creating Hashes

Hash Literals

Create hashes using curly braces with key-value pairs.

# Colon syntax (keys are strings)
person = {
  name: "Alice",
  age: 30,
  city: "New York"
}

# Fat arrow syntax (=>)
scores = {"Alice" => 95, "Bob" => 87, "Charlie" => 92}

# Symbol keys (distinct from string keys)
config = { :host => "localhost", :port => 3000 }

# Empty hash
empty = {}

# Mixed values
mixed = {
  number: 42,
  string: "hello",
  bool: true,
  null_val: null,
  array: [1, 2, 3],
  nested: {a: 1, b: 2}
};
Nested Hashes

Hashes can contain other hashes for complex data structures.

user = {
  id: 1,
  name: "Alice",
  profile: {
    email: "[email protected]",
    phone: "555-1234",
    address: {
      street: "123 Main St",
      city: "NYC",
      zip: "10001"
    }
  },
  preferences: {
    theme: "dark",
    notifications: true
  }
};
Variable Shorthand

When a key name matches a variable name, you can omit the value. The variable's value is used automatically. Raises an error if the variable is not defined.

name = "Alice"
age = 30

# Shorthand - value taken from variable with same name
person = { name:, age: }
# Equivalent to: { "name": name, "age": age }

person["name"]  # "Alice"
person["age"]   # 30

# Mix shorthand with regular entries
city = "NYC"
user = { name:, age:, "email": "[email protected]", city: }

Accessing Values

Bracket Notation

Access values using square brackets with the key. Returns null if key doesn't exist.

person = {name: "Alice", age: 30}

# Access existing keys
print(person["name"]);   # "Alice"
print(person["age"]);    # 30

# Access non-existent key (returns null)
print(person["email"]);  # null

# Access nested values
user = {profile: {email: "[email protected]"}}
print(user["profile"]["email"]);  # "[email protected]"
Dot Notation

Access values using dot notation. Shorthand for bracket access. Works with nested hashes too.

person = {name: "Alice", age: 30}

# Dot notation access
print(person.name);   # "Alice"
print(person.age);    # 30

# Access nested values
user = {profile: {email: "[email protected]"}}
print(user.profile.email);  # "[email protected]"

# Dot notation also allows method chaining
data = {items: [1, 2, 3]}
print(data.items.length);  # 3

# Safe navigation with &. (returns null if any part is null)
user = {profile: null}
print(user&.profile&.email);  # null (no error)
print(user&.profile&.email || "no email");  # "no email"
Modifying Hashes

Hashes are mutable - you can add, update, or remove key-value pairs.

person = {name: "Alice", age: 30}

# Update existing key (bracket notation)
person["age"] = 31

# Add new key (bracket notation)
person["email"] = "[email protected]"

# Update or add with dot notation (both work!)
person.age = 32
person.city = "NYC"

# Remove a key (using delete method)
person.delete("email")
print(person);  # {name: "Alice", age: 32, city: "NYC"}

Hash Methods

.length / .len / .size

Returns the number of key-value pairs in the hash.

person = {name: "Alice", age: 30, city: "NYC"}
print(person.length);  # 3
print(person.len);     # 3
print(person.size);    # 3

empty = {}
print(empty.length);   # 0
.keys

Returns an array of all keys in the hash.

person = {name: "Alice", age: 30}
k = person.keys
print(k);  # ["name", "age"]
.values

Returns an array of all values in the hash.

person = {name: "Alice", age: 30}
v = person.values
print(v);  # ["Alice", 30]
.entries

Returns an array of [key, value] pairs.

person = {name: "Alice", age: 30}
e = person.entries
print(e);  # [["name", "Alice"], ["age", 30]]
.has_key(key)

Check if a key exists in the hash.

person = {name: "Alice", age: 30}

print(person.has_key("name"));   # true
print(person.has_key("email"));  # false
.delete(key)

Remove a key-value pair from the hash (modifies in place).

person = {name: "Alice", age: 30, city: "NYC"}
person.delete("age")
print(person);  # {name: "Alice", city: "NYC"}
.merge(hash)

Merge another hash. Values from the other hash take precedence. Returns a new hash.

defaults = {theme: "light", lang: "en"}
user_prefs = {theme: "dark"}

merged = defaults.merge(user_prefs)
print(merged);  # {theme: "dark", lang: "en"}
.clear

Remove all key-value pairs from the hash.

person = {name: "Alice", age: 30}
person.clear
print(person);    # {}
print(person.len);  # 0
.get(key, default?) / .fetch(key, default?)

Get value with optional default. .fetch() raises an error if key not found and no default provided.

scores = {Alice: 90, Bob: 85}

# get with default
print(scores.get("Alice"));        # 90
print(scores.get("Eve", 0));       # 0 (default)
print(scores.get("Eve"));          # null (no default)

# fetch - raises error if not found
print(scores.fetch("Alice"));      # 90
print(scores.fetch("Eve", 0));     # 0 (default)
# scores.fetch("Eve");            # Error: key not found
.dig(*keys)

Retrieve a nested value from a hash or array using a sequence of keys/indices. Returns null if any key is not found. Works with both string keys for hashes and integer indices for arrays.

data = {user: {profile: {name: "Alice"}}}

print(data.dig("user", "profile", "name"));  # "Alice"
print(data.dig("user", "settings", "theme"));  # null (path not found)

nested = {items: [10, 20, {value: 99}]}
print(nested.dig("items", 2, "value"));  # 99
print(nested.dig("items", 5));  # null (index out of bounds)
.map(def) / .filter(def) / .each(def)

Transform, filter, or iterate over hash entries. Functions receive (key, value) parameters.

scores = {Alice: 90, Bob: 85, Charlie: 95}

# map - transform entries, return new hash
curved = scores.map(|k, v| [k, v + 5])
# {Alice: 95, Bob: 90, Charlie: 100}

# Trailing block syntax (equivalent)
curved = scores.map |k, v| [k, v + 5] end

# filter - keep matching entries
high = scores.filter(|k, v| v >= 90)
# {Alice: 90, Charlie: 95}

# each - iterate for side effects
scores.each |k, v| print(k + ": " + str(v)) end
# Output: Alice: 90
#         Bob: 85
#         Charlie: 95
.transform_values(def) / .transform_keys(def)

Transform all values or keys. Returns a new hash.

scores = {Alice: 90, Bob: 85}

# Transform values
doubled = scores.transform_values(|v| v * 2)
# {Alice: 180, Bob: 170}

# Trailing block syntax
doubled = scores.transform_values |v| v * 2 end

# Transform keys
upper_keys = scores.transform_keys(|k| upcase(k))
# {ALICE: 90, BOB: 85}
.select(def) / .reject(def)

Select or reject entries based on condition. Functions receive (key, value) parameters.

scores = {Alice: 90, Bob: 85, Charlie: 95}

# select - keep where function returns true
high = scores.select(|k, v| v >= 90)
# {Alice: 90, Charlie: 95}

# reject - remove where function returns true
not_high = scores.reject(|k, v| v >= 90)
# {Bob: 85}

# Trailing block syntax
high = scores.select |k, v| v >= 90 end
.slice([keys]) / .except([keys])

Get subset with specified keys or without specified keys.

user = {
  name: "Alice",
  age: 30,
  email: "[email protected]",
  city: "NYC",
  password: "secret123"
}

# slice - only these keys
basic = user.slice(["name", "email"])
# {name: "Alice", email: "[email protected]"}

# except - exclude these keys (good for removing sensitive data)
safe = user.except(["password"])
# {name: "Alice", age: 30, email: "[email protected]", city: "NYC"}
.invert

Swap keys and values. Returns a new hash.

scores = {"Alice": 90, "Bob": 85}
inverted = scores.invert
print(inverted)  # {90: "Alice", 85: "Bob"}

# Note: if values are not unique, later entries overwrite earlier ones
dupes = {"a": 1, "b": 1}
print(dupes.invert)  # {1: "b"}
.compact

Remove entries with null values.

user = {
  "name": "Alice",
  "age": null,
  "email": "[email protected]",
  "phone": null,
}
cleaned = user.compact
print(cleaned)  # {"name": "Alice", "email": "[email protected]"}
.dig(key, ...)

Navigate nested hashes safely. Returns null if any key is not found (no error raised).

.shift

Remove and return the first key-value pair as [key, value] array. Returns null if empty. Mutates the original hash.

h = {"a": 1, "b": 2}
pair = h.shift
# pair = ["a", 1], h = {"b": 2}

empty = {}
empty.shift  # null
.flatten

Convert hash to array of [key, value] sub-arrays. Equivalent to .to_array.

h = {"a": 1, "b": 2}
flat = h.flatten
# [["a", 1], ["b", 2]]
.values_at(*keys)

Returns array of values for given keys. Returns null for missing keys.

h = {"a": 1, "b": 2, "c": 3}
h.values_at("a", "c")  # [1, 3]
h.values_at("a", "z")  # [1, null]
.key(value)

Returns the first key for a given value (inverse lookup). Returns null if not found.

h = {"a": 1, "b": 2, "c": 3}
h.key(2)   # "b"
h.key(99)  # null
.has_value?(value) / .value?(value)

Check if the hash contains a given value. Returns Bool.

h = {"a": 1, "b": 2}
h.has_value?(2)   # true
h.has_value?(99)  # false
h.value?(2)       # true (alias)
.to_h

Returns self (identity for hashes).

h = {"a": 1}
h2 = h.to_h
# h2 == h (same entries)
.each_key(fn)

Iterate over keys only. Returns the hash for chaining.

h = {"a": 1, "b": 2, "c": 3}
keys = []
h.each_key(|k| keys.push(k))
# keys = ["a", "b", "c"]
.each_value(fn)

Iterate over values only. Returns the hash for chaining.

h = {"a": 1, "b": 2, "c": 3}
vals = []
h.each_value(|v| vals.push(v))
# vals = [1, 2, 3]
.keep_if(fn) / .delete_if(fn)

.keep_if() keeps entries where block returns truthy (like select). .delete_if() removes entries where block returns truthy (like reject). Both return a new hash.

h = {"a": 1, "b": 2, "c": 3}

kept = h.keep_if(|k, v| v >= 2)
# {"b": 2, "c": 3}

removed = h.delete_if(|k, v| v >= 2)
# {"a": 1}
.update(other)

Alias for .merge(). Merges entries from another hash into a new hash.

h1 = {"a": 1}
h2 = h1.update({"b": 2})
# h2 = {"a": 1, "b": 2}
.all?(fn) / .any?(fn)

Test all or any entries with a predicate. Block receives (key, value). Returns Bool.

h = {"a": 2, "b": 4, "c": 6}
h.all?(|k, v| v % 2 == 0)  # true (all even)
h.any?(|k, v| v == 4)       # true (4 exists)
h.any?(|k, v| v > 10)       # false
.assoc(key) / .rassoc(value)

.assoc(key) returns [key, value] pair or null if key not found. .rassoc(value) returns [key, value] for matching value or null.

h = {"a": 1, "b": 2}
h.assoc("b")    # ["b", 2]
h.assoc("z")    # null
h.rassoc(1)     # ["a", 1]
h.rassoc(99)    # null
.fetch_values(*keys)

Returns array of values for given keys. Raises an error if any key is missing (unlike values_at which returns null).

h = {"a": 1, "b": 2}
h.fetch_values("a", "b")  # [1, 2]
# h.fetch_values("a", "z")  # Error: key not found

Common Patterns

Counting Occurrences
words = ["apple", "banana", "apple", "cherry", "banana", "apple"]

# Count occurrences with `??=` to seed missing keys, then `+=`
counts = {}
words.each do |w|
  counts[w] ??= 0
  counts[w] += 1
end
print(counts)  # {"apple": 3, "banana": 2, "cherry": 1}
Grouping Data
people = [
  {"name": "Alice",   "department": "Engineering"},
  {"name": "Bob",     "department": "Sales"},
  {"name": "Charlie", "department": "Engineering"},
]

# Group names by department — `??= []` seeds the bucket on first hit
by_dept = {}
people.each do |p|
  by_dept[p["department"]] ??= []
  by_dept[p["department"]].push(p["name"])
end
print(by_dept)
# {"Engineering": ["Alice", "Charlie"], "Sales": ["Bob"]}
Configuration Objects
# Default configuration
defaults = {
  host: "localhost",
  port: 3000,
  debug: false,
  timeout: 30
}

# User-provided config (partial)
user_config = {port: 8080, debug: true}

# Merge with defaults
config = defaults.merge(user_config)
# {host: "localhost", port: 8080, debug: true, timeout: 30}

# Safe access with defaults — `.get(key)` returns null for missing keys,
# so combine with `??` to fall back to a default.
timeout = config.get("timeout") ?? 60
retries = config.get("retries") ?? 3   # `retries` not set, so 3

Hash Class Methods

Hash literals are automatically wrapped in a Hash class instance that provides methods for manipulation and transformation. Each hash value has access to these methods via dot notation.

to_string()

Returns a formatted string representation of the hash. Called automatically in REPL.

h = {name: "Alice", age: 30}
# In REPL, displays: {name => "Alice", age => 30}
# Equivalent to: h.to_string
to_json

Serializes the hash to a JSON string.

h = {name: "Alice", age: 30, active: true}
h.to_json  # '{"name":"Alice","age":30,"active":true}'

nested = {user: {name: "Bob"}, tags: [1, 2]}
nested.to_json  # '{"user":{"name":"Bob"},"tags":[1,2]}'
length() / len() / size()

Returns the number of key-value pairs in the hash.

h = Hash.new
h.set("a", 1)
h.set("b", 2)
h.length  # 2
get(key) / set(key, value)

Gets or sets values by key using method calls.

h = Hash.new()h.set("name", "Alice")h.get("name");           # "Alice"
h.set("age", 30)h.get("age");            # 30
has_key(key)

Returns true if the hash contains the specified key.

h = Hash.new()h.set("key", "value")h.has_key("key");    # true
h.has_key("missing"); # false
keys() / values()

Returns arrays of all keys or all values in the hash.

h = Hash.new
h.set("a", 1)
h.set("b", 2)
k = h.keys     # ["a", "b"]
v = h.values   # [1, 2]
Hash.new()

Creates a new empty Hash instance.

h = Hash.new
h.length  # 0
.class / .inspect / .nil? / .is_a? / .blank? / .present?

Type introspection methods available on all types. Empty hashes are blank.

h = {name: "Alice"}
h.class          # "hash"
h.inspect        # "{\"name\": \"Alice\"}"
h.nil?           # false
h.is_a?("hash")  # true
h.blank?         # false
h.present?       # true
{}.blank?        # true