Skip to content

Instantly share code, notes, and snippets.

@Manfred
Last active September 23, 2016 19:13
Show Gist options
  • Save Manfred/3575f1e45c898f826a7e2f22ff59a9ee to your computer and use it in GitHub Desktop.
Save Manfred/3575f1e45c898f826a7e2f22ff59a9ee to your computer and use it in GitHub Desktop.
An often used memoization idiom causes problems in Ruby.
# Run this example with $ ruby -W w.rb
class Gallery
attr_reader :disabled
def initialize(disabled:)
@disabled = disabled
end
def images
@images ||= []
end
def enabled?
@enabled ||= !disabled
end
end
class Camera
attr_reader :disabled
def initialize(disabled:)
@disabled = disabled
end
def images
if defined?(@images)
@images
else
@images = []
end
end
def enabled?
if defined?(@enabled)
@enabled
else
@enabled != disabled
end
end
end
require 'minitest/autorun'
class Test < Minitest::Test
def test_gallery_enabled
assert Gallery.new(disabled: false).enabled?
end
def test_gallery_disabled
refute Gallery.new(disabled: true).enabled?
end
def test_camera_enabled
assert Camera.new(disabled: false).enabled?
end
def test_camera_disabled
refute Camera.new(disabled: true).enabled?
end
end

An often used memoization idiom causes warnings in Ruby

These warnings are there to help you find typos in your code; in this case it's an instance variable. When you write @diabled you will see a warning about it not being initialized, which helps you track down weird bugs.

Using the idiom is also not possible for boolean values because @a ||= false will always re-evaluate the right part of the expression. Remember that it's actually:

@a = @a || false

Now @a becomes false. So in the second iteration of this statement you evaluate this

@a = false || false

Now assume that the right side part of the expression takes 20 hours to compute.

@Manfred
Copy link
Author

Manfred commented Sep 23, 2016

I snuck in a little bug here to test if you're actually paying attention. Good luck finding it. Tweet at @thijs for for your prize.

@clupprich
Copy link

clupprich commented Sep 23, 2016

I like to replace the else with an early bail-out, because I find it easier to read:

def enabled?
  return @enabled if defined?(@enabled)

  @enabled = !disabled
end

Kudos to @skmetz and @avdi!

@jballanc
Copy link

Not sure if this is the "bug" you mentioned, but I'd just like to point out this common misconception in Ruby. This:

@a ||= false

actually expands to this:

@a || @a = false

The end result is the same, however.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment