Skip to content

Record

What is a Record?

A Record is a named data object — a Hash with a declared structure and a readable name. Where a plain hash is anonymous, a Record prints as Point(x: 3, y: 4) and carries its type name everywhere it goes.

Records are ideal when your data has a consistent shape and a meaningful name matters — domain objects, configuration structures, function return values.

record Point(x, y)

pt = Point(3, 4)
puts pt           # Point(x: 3, y: 4)
puts pt["x"]      # 3
puts pt["__type__"]  # Point

Defining a Record

record Point(x, y)
record Employee(name, dept, salary)
record Config(host, port, db, timeout)
record RGB(r, g, b)

record takes a name and a list of field names. Field names become the hash keys.


Creating Instances

Call the record name as a function, passing values in order:

record Point(x, y)
record Person(name, age, role)

pt   = Point(10, 20)
alice = Person("Alice", 30, "Engineer")
origin = Point(0, 0)

puts pt      # Point(x: 10, y: 20)
puts alice   # Person(name: Alice, age: 30, role: Engineer)

Accessing Fields

Records are Hashes. Access fields with string keys:

record Employee(name, dept, salary)

emp = Employee("Alice", "Engineering", 95000)

puts emp["name"]     # Alice
puts emp["dept"]     # Engineering
puts emp["salary"]   # 95000

# The type name is stored under __type__
puts emp["__type__"]   # Employee

Records are Hashes

Every hash method and iterator works on records — they are not special objects, just hashes with a __type__ key and a readable puts representation.

record Point(x, y)
pt = Point(3, 4)

puts pt.keys     # [__type__, x, y]
puts pt.values   # [Point, 3, 4]
puts pt.size     # 3

# Merge — creates a plain hash (type info preserved)
moved = pt | {"x" => 10}
puts moved       # {__type__: Point, x: 10, y: 4}

# dig
record Config(db)
db_config = Config({host: "localhost", port: 5432})
puts db_config.dig("db", "host")   # localhost

Records in Collections

Because records are hashes, all vector and hash iterators work on them naturally:

record Person(name, age, dept)

people = [
  Person("Alice", 32, "Engineering"),
  Person("Bob",   25, "Marketing"),
  Person("Carol", 35, "Engineering"),
  Person("Dave",  28, "Marketing"),
  Person("Eve",   41, "Engineering")
]

# select — filter by field
engineers = people.select do |p|
  p["dept"] == "Engineering"
end
puts engineers.length   # 3

# map — transform field values
names = people.map do |p|
  p["name"]
end
puts names   # [Alice, Bob, Carol, Dave, Eve]

# sort_by
by_age = people.sort_by do |p|
  p["age"]
end
puts by_age.map do |p| "#{p["name"]} (#{p["age"]})" end
# [Bob (25), Dave (28), Alice (32), Carol (35), Eve (41)]

# group_by
by_dept = people.group_by do |p|
  p["dept"]
end
by_dept.each do |dept, members|
  puts "#{dept}: #{members.map do |m| m["name"] end.join(", ")}"
end
# Engineering: Alice, Carol, Eve
# Marketing: Bob, Dave

# each_with_object — build a salary index
salary_index = people.each_with_object({}) do |person, h|
  h[person["name"]] = person["salary"]
end

Records vs Plain Hashes

Hash Record
Literal syntax {name: "Alice"} Person("Alice", 30)
puts output {name: Alice, age: 30} Person(name: Alice, age: 30)
Type name available No r["__type__"]
Field order guaranteed Yes (insertion order) Yes (definition order)
All hash methods Yes Yes
All iterators Yes Yes
Best for Arbitrary/dynamic data Structured domain objects

Use a plain hash when the shape is dynamic or comes from external data (JSON, CSV, DB rows). Use a Record when your code defines the shape and the name carries meaning.


Modifying Records

Records are mutable — field values can be changed by key assignment:

record Config(host, port, debug)

cfg = Config("localhost", 5432, false)
puts cfg   # Config(host: localhost, port: 5432, debug: false)

cfg["debug"] = true
cfg["port"]  = 5433
puts cfg   # Config(host: localhost, port: 5433, debug: true)

Pattern: Factory Function

Wrap record creation in a function for validation or default values:

record User(name, email, role, active)

def make_user(name, email, role = "viewer")
  raise "Name required" if name == nil or name == ""
  raise "Email required" if email == nil or email == ""
  User(name, email, role, true)
end

admin = make_user("Alice", "alice@example.com", "admin")
guest = make_user("Bob",   "bob@example.com")

puts admin   # User(name: Alice, email: alice@example.com, role: admin, active: true)
puts guest   # User(name: Bob, email: bob@example.com, role: viewer, active: true)

Pattern: Records as Return Values

Records make multi-value returns self-documenting:

record Stats(count, mean, stdev, min, max)

def describe(data)
  Stats(
    data.length,
    round(mean(data), 2),
    round(stdev(data), 2),
    min(data),
    max(data)
  )
end

result = describe([23, 45, 12, 67, 34, 89])
puts result
# Stats(count: 6, mean: 45.0, stdev: 26.74, min: 12, max: 89)

puts result["mean"]    # 45.0
puts result["min"]     # 12

Compared to returning a plain vector [6, 45.0, 26.74, 12, 89], the record is self-describing — field names are the documentation.


Quick Reference

# Define
record Point(x, y)
record Person(name, age, role)

# Create
pt     = Point(3, 4)
person = Person("Alice", 30, "Engineer")

# Access
pt["x"]             # field value
pt["__type__"]      # "Point"

# Inspect
pt.keys             # [__type__, x, y]
pt.values           # [Point, 3, 4]
pt.has_key?("x")    # true

# Modify
pt["x"] = 10

# Merge
pt | {"x" => 99}    # {__type__: Point, x: 99, y: 4}

# In collections — all vector/hash iterators work
people.select do |p| p["dept"] == "Engineering" end
people.sort_by  do |p| p["age"] end
people.group_by do |p| p["dept"] end
people.map      do |p| p["name"] end