Class: Errgonomic::OptionalHash

Inherits:
Object
  • Object
show all
Includes:
OptionalDig
Defined in:
lib/errgonomic/optional_hash.rb

Overview

A companion to Hash whose lookups return Options, composed around a plain Hash rather than subclassing it. Subclassing cannot keep Option semantics: since Ruby 3, most Hash methods return plain Hash instances, so wrapped behavior silently drops off in pipelines. Composition with a small, closed API keeps the semantics honest; reach the plain Hash back with to_h.

Lookups follow key presence, not value truthiness, so a key holding nil is Some(nil). This matches Option presence semantics: the discriminant tells you whether the key was there, and the inner value is yours to judge.

Instance Method Summary collapse

Constructor Details

#initialize(hash = {}) ⇒ OptionalHash

Returns a new instance of OptionalHash.



19
20
21
22
23
24
25
26
# File 'lib/errgonomic/optional_hash.rb', line 19

def initialize(hash = {})
  unless hash.is_a?(::Hash)
    raise Errgonomic::TypeMismatchError,
          "OptionalHash wraps a Hash, got #{hash.class}"
  end

  @hash = hash
end

Instance Method Details

#==(other) ⇒ Object

Equal to another OptionalHash wrapping an equal hash; never equal to a plain Hash, mirroring how Some(x) is never equal to x.

Examples:

{ a: 1 }.into_optional == { a: 1 }.into_optional # => true
{ a: 1 }.into_optional == { a: 2 }.into_optional # => false
{ a: 1 }.into_optional == { a: 1 } # => false


119
120
121
# File 'lib/errgonomic/optional_hash.rb', line 119

def ==(other)
  other.is_a?(OptionalHash) && inner == other.inner
end

#[](key) ⇒ Object

Retrieve the value for a key, wrapped in an Option. A present key with a nil value is Some(nil); only a missing key is None.

Examples:

h = { color: :blue, shade: nil }.into_optional
h[:color] # => Some(:blue)
h[:shade] # => Some(nil)
h[:smell] # => None()


36
37
38
39
40
# File 'lib/errgonomic/optional_hash.rb', line 36

def [](key)
  return None() unless @hash.key?(key)

  Some(@hash[key])
end

#[]=(key, value) ⇒ Object

Write through to the underlying hash.

Examples:

h = {}.into_optional
h[:color] = :blue
h[:color] # => Some(:blue)


48
49
50
# File 'lib/errgonomic/optional_hash.rb', line 48

def []=(key, value)
  @hash[key] = value
end

#dig(key, *rest) ⇒ Object

Like Hash#dig, but every step checks presence, so the result distinguishes an absent path (None) from a present nil (Some(nil)), which Hash#dig conflates. Walks nested Hashes, Arrays, and OptionalHashes; digging into anything else raises, pedantically, where Hash#dig would raise TypeError.

Examples:

h = { person: { name: 'Ada', middle_name: nil } }.into_optional
h.dig(:person, :name) # => Some("Ada")
h.dig(:person, :middle_name) # => Some(nil)
h.dig(:person, :nickname) # => None()
h.dig(:company, :name) # => None()

arrays participate, with bounds checked

h = { people: [{ name: 'Ada' }] }.into_optional
h.dig(:people, 0, :name) # => Some("Ada")
h.dig(:people, 1, :name) # => None()

a nested wrapper walks the rest of the path itself

h = { person: { name: 'Ada' }.into_optional }.into_optional
h.dig(:person, :name) # => Some("Ada")
h.dig(:person, :nickname) # => None()

digging into a non-collection is an error, not a None

h = { name: 'Ada' }.into_optional
h.dig(:name, :length) # => raise Errgonomic::TypeMismatchError, "cannot dig into String"


78
79
80
# File 'lib/errgonomic/optional_hash.rb', line 78

def dig(key, *rest)
  optional_dig(@hash, [key, *rest])
end

#empty?Boolean

Examples:

{}.into_optional.empty? # => true
{ a: 1 }.into_optional.empty? # => false

Returns:

  • (Boolean)


93
94
95
# File 'lib/errgonomic/optional_hash.rb', line 93

def empty?
  @hash.empty?
end

#eql?(other) ⇒ Boolean

Examples:

{ a: 1 }.into_optional.eql?({ a: 1 }.into_optional) # => true
h = { a: 1 }.into_optional
{ h => :hit }[{ a: 1 }.into_optional] # => :hit

Returns:

  • (Boolean)


127
128
129
# File 'lib/errgonomic/optional_hash.rb', line 127

def eql?(other)
  other.is_a?(OptionalHash) && inner.eql?(other.inner)
end

#hashObject



131
132
133
# File 'lib/errgonomic/optional_hash.rb', line 131

def hash
  [self.class, inner].hash
end

#inspectObject

Examples:

{}.into_optional.inspect # => "OptionalHash({})"


137
138
139
# File 'lib/errgonomic/optional_hash.rb', line 137

def inspect
  "OptionalHash(#{@hash.inspect})"
end

#key?(key) ⇒ Boolean

Examples:

h = { shade: nil }.into_optional
h.key?(:shade) # => true
h.key?(:color) # => false

Returns:

  • (Boolean)


86
87
88
# File 'lib/errgonomic/optional_hash.rb', line 86

def key?(key)
  @hash.key?(key)
end

#sizeObject

Examples:

{ a: 1 }.into_optional.size # => 1


99
100
101
# File 'lib/errgonomic/optional_hash.rb', line 99

def size
  @hash.size
end

#to_hObject

The escape hatch back to a plain Hash: a shallow copy, so hash-shaped code cannot mutate the wrapped state behind the Option semantics.

Examples:

{ a: 1 }.into_optional.to_h # => { a: 1 }


108
109
110
# File 'lib/errgonomic/optional_hash.rb', line 108

def to_h
  @hash.dup
end