philiprehberger-maybe
Optional/Maybe monad with safe chaining and pattern matching
Requirements
- Ruby >= 3.1
Installation
Add to your Gemfile:
gem "philiprehberger-maybe"
Or install directly:
gem install philiprehberger-maybe
Usage
require "philiprehberger/maybe"
result = Philiprehberger::Maybe.wrap(42)
result.value # => 42
result.some? # => true
none = Philiprehberger::Maybe.wrap(nil)
none.none? # => true
Safe Chaining
result = Philiprehberger::Maybe.wrap(user)
.map { |u| u.address }
.map { |a| a.city }
.or_else('Unknown')
Pattern Matching
case Philiprehberger::Maybe.wrap(value)
in { some: true, value: Integer => v }
puts "Got integer: #{v}"
in { none: true }
puts 'No value'
end
Filtering
Philiprehberger::Maybe.wrap(18)
.filter { |v| v >= 21 }
.or_else(0)
.value # => 0
Digging into Nested Structures
data = { user: { address: { city: 'Vienna' } } }
Philiprehberger::Maybe.wrap(data)
.dig(:user, :address, :city)
.value # => 'Vienna'
Flat Map
Philiprehberger::Maybe.wrap(5)
.flat_map { |v| Philiprehberger::Maybe.wrap(v > 0 ? v : nil) }
.value # => 5
Combining with Zip
a = Philiprehberger::Maybe.wrap(1)
b = Philiprehberger::Maybe.wrap(2)
a.zip(b).value # => [1, 2]
c = Philiprehberger::Maybe.wrap(nil)
a.zip(c).none? # => true
Recovery
Philiprehberger::Maybe.wrap(nil)
.recover { 'default' }
.value # => 'default'
Side Effects with Tap
Philiprehberger::Maybe.wrap(42)
.tap { |v| puts "Got: #{v}" }
.map { |v| v * 2 }
.value # => 84
Enumerable Support
Philiprehberger::Maybe.wrap(42).to_a # => [42]
Philiprehberger::Maybe.wrap(nil).to_a # => []
Philiprehberger::Maybe.wrap(5)
.select { |v| v > 3 } # => [5]
Utility Methods
a = Philiprehberger::Maybe.wrap(1)
b = Philiprehberger::Maybe.wrap(2)
Philiprehberger::Maybe.all?(a, b) # => true
c = Philiprehberger::Maybe.wrap(nil)
Philiprehberger::Maybe.first_some(c, a) # => Some(1)
Constructing from Conditions
Philiprehberger::Maybe.from_bool(user.admin?, user)
# => Some(user) when admin, None otherwise
Philiprehberger::Maybe.from_bool(cache.valid?) { cache.fetch }
# => Some(value) if the gate passes, None otherwise (block only runs when truthy)
Philiprehberger::Maybe.try { JSON.parse(input) }
# => Some(parsed) on success, None on any StandardError
Philiprehberger::Maybe.try(KeyError) { ENV.fetch('MISSING') }
# => None — only KeyError is caught; other exceptions propagate
Rejecting Values
Philiprehberger::Maybe.wrap(user)
.reject(&:deleted?)
.value # => nil when user.deleted?, user otherwise
Flattening Nested Maybes
outer = Philiprehberger::Maybe.wrap(Philiprehberger::Maybe.wrap(42))
outer.flatten.value # => 42
API
Maybe
| Method | Description |
|---|---|
.wrap(value) |
Wrap a value in Some (non-nil) or None (nil) |
.all?(*maybes) |
Return true if all arguments are Some |
.first_some(*maybes) |
Return the first Some, or None if all are None |
.from_bool(condition, value = nil, &block) |
Some when condition is truthy (and value/block result is non-nil), else None |
.try(*error_classes, &block) |
Run block; return Some on success, None on caught errors or nil (defaults to StandardError) |
Maybe::Some
| Method | Description |
|---|---|
#value |
Return the wrapped value |
#some? |
Return true |
#none? |
Return false |
| `#map { \ | v\ |
| `#flat_map { \ | v\ |
| `#filter { \ | v\ |
| `#reject { \ | v\ |
#flatten |
Flatten one level — Some(Some(x)) → Some(x), Some(None) → None |
#contains?(value) |
True if the wrapped value equals the argument |
#present? |
Return true (alias for #some?) |
#or_else(default) |
Return self (ignores default) |
#or_raise(error, msg) |
Return the value |
#dig(*keys) |
Dig into nested hashes/arrays |
#zip(*others) |
Combine Maybes; Some array if all Some, else None |
| `#tap { \ | v\ |
| `#each { \ | v\ |
#deconstruct_keys(keys) |
Pattern matching support |
Maybe::None
| Method | Description |
|---|---|
#value |
Return nil |
#some? |
Return false |
#none? |
Return true |
| `#map { \ | v\ |
| `#flat_map { \ | v\ |
| `#filter { \ | v\ |
| `#reject { \ | v\ |
#flatten |
Return None (no-op) |
#contains?(value) |
Always false |
#present? |
Return false (alias for #some?) |
#or_else(default) |
Return default wrapped in Maybe |
#or_raise(error, msg) |
Raise the specified error |
#dig(*keys) |
Return None |
#recover { } |
Convert None to Some via block |
#zip(*others) |
Return None (always) |
| `#tap { \ | v\ |
| `#each { \ | v\ |
#deconstruct_keys(keys) |
Pattern matching support |
Development
bundle install
bundle exec rspec
bundle exec rubocop
Support
If you find this project useful: