Skip to content

Instantly share code, notes, and snippets.

@DNNX
Last active November 19, 2015 21:19
Show Gist options
  • Save DNNX/da2aaafdfd71e6ff0ffd to your computer and use it in GitHub Desktop.
Save DNNX/da2aaafdfd71e6ff0ffd to your computer and use it in GitHub Desktop.
Maybe yes, Maybe no

This is another wrong, incomplete implementation of Maybe data type in Ruby. This gist is a full-blown gem. In order to use it, just add the following line to your Gemfile.

gem 'maybe', gist: 'da2aaafdfd71e6ff0ffd'

But WHY?

To my knowledge, all existing gems are too fat and do not provide the ability to configure what values should be treated as Just and what values are Nothing. Some treat [] as Nothing, some treat only nil as Nothing, some rely on truthiness (this is the most reasonable choice in my opinion). In my particular use case, I needed to treat obj as Nothing iff obj.present? is true. Obviously, hard-coding this Railsism into the gem is not an option, so I pass the Nothingness predicate into Maybe data constructor (pardon my French).

Enough talking, show me the code!

User = Struct.new(:twitter_account)
TwitterAccount = Struct.new(:number_of_followers)

def has_many_followers?(user)
  Maybe(user)
    .fmap(&:twitter_account)
    .fmap(&:number_of_followers)
    .fmap{ |n| n > 1 }
    .from_just { false }
end

has_many_followers?(nil)
# => false
has_many_followers?(User.new(nil))
# => false
has_many_followers?(User.new(TwitterAccount.new(nil)))
# => false
has_many_followers?(User.new(TwitterAccount.new(0)))
# => false
has_many_followers?(User.new(TwitterAccount.new(9001)))
# => true

### Ok, cool, but what if we want to use `present?` or `empty?`?

def greet(username)
  Maybe(username, &:present?)
    .fmap {|x| "Hello, #{x}!"}
    .from_just { 'Nothing to see here, move along' }
end

greet('Simon')
# => "Hello, Simon!"
greet('')
# => "Nothing to see here, move along"
greet(false)
# => "Nothing to see here, move along"

Wait, but your Maybe violates THE FUNCTOR LAWS!

It does indeed.

Maybe(1, &:odd?).fmap(&:succ).fmap(&:pred).from_just{'???'}
# => "???"
Maybe(1, &:odd?).fmap{|x| x.succ.pred}.from_just{'???'}
# => 1

But who cares?

Inspiration

Gem::Specification.new do |s|
s.name = 'maybe'
s.version = '0.0.1'
s.platform = Gem::Platform::RUBY
s.author = 'Viktar Basharymau'
s.email = 'viktar.basharymau@gmail.com'
s.summary = 'Maybe yes, maybe no'
s.description = 'Incomplete implementation of Maybe (a.k.a. Some and Option)'
s.files = ['maybe.rb']
s.test_file = 'maybe_test.rb'
s.require_path = '.'
end
def Maybe(value, &is_just)
is_just ||= :itself.to_proc
if is_just.(value)
Just.new(value, is_just)
else
Nothing
end
end
Just = Struct.new(:value, :is_just) do
def fmap
Maybe(yield(value), &is_just)
end
def from_just
value
end
end
module Nothing
def self.fmap
self
end
def self.from_just
yield
end
end
require 'minitest/autorun'
require 'minitest/spec'
describe 'Maybe' do
describe "long chain of successful fmap calls" do
it 'should return the last value' do
assert_equal :d,
Maybe({a: {b: {c: :d}}})
.fmap { |h| h[:a] }
.fmap { |h| h[:b] }
.fmap { |h| h[:c] }
.from_just { raise 'Should never get there' }
end
end
describe "long chain of fmap calls which breaks in the middle" do
it 'should return the last value' do
assert_equal 'default value',
Maybe({a: {b: {c: :d}}})
.fmap { |h| h[:a] }
.fmap { |h| h['OH NO'] }
.fmap { |h| h[:c] }
.from_just { 'default value' }
end
end
describe "custom is_just" do
it 'uses custom is_just in order to determine "Just-ness"' do
assert_equal 'Time for devaluation!',
Maybe(17_800) { |x| x <= 20_000 }
.fmap { |x| x * 1.04 } # 4% / month
.fmap { |x| x * 1.04 }
.fmap { |x| x * 1.04 }
.from_just { 'Time for devaluation!' }
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment