memonymous 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ spikes/*
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm use ruby-1.9.2-p180@memonymous --create --install
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in memonymous.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,13 @@
1
+ DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
2
+ Version 2, December 2004
3
+
4
+ Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
5
+
6
+ Everyone is permitted to copy and distribute verbatim or modified
7
+ copies of this license document, and changing it is allowed as long
8
+ as the name is changed.
9
+
10
+ DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
11
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
12
+
13
+ 0. You just DO WHAT THE FUCK YOU WANT TO.
@@ -0,0 +1,51 @@
1
+ # Memonymous: a Ruby memoization module that doesn't clutter up its includees.
2
+
3
+ ## License
4
+
5
+ Public domain (more or less). See the LICENSE file for more information.
6
+
7
+ ## Origin
8
+
9
+ Inspired by a recent Hangman puzzle at the Portland Ruby Brigade ([link](http://groups.google.com/group/pdxruby/browse_thread/thread/19d58126a268e89a/078efefc26ddcf7b)), I decided to try writing a memoization module that used `super` instead of mucking around with `alias_method_chain`.
10
+
11
+ Unfortunately, the only way I could think of to get this to work was to override the including class's .new method, so that all new instances were actually instances of a newly-created subclass...and that just seemed like a Really Bad Idea&trade;.
12
+
13
+ Fortunately, I did find an interesting workaround, and in the process learned a few new tricks that have been sitting right there in the Kernel and Module classes just waiting for me to notice them.
14
+
15
+ ## Usage:
16
+
17
+ <pre>
18
+ class FrankSinatra
19
+ attr_reader :call_count
20
+ def dont_be_afraid_you_can_call_me
21
+ @call_count ||= 0
22
+ @call_count += 1
23
+ end
24
+ end
25
+
26
+ class MemoizedFrankSinatra < FrankSinatra
27
+ include Memonymous
28
+ memoize :dont_be_afraid_you_can_call_me
29
+ end
30
+
31
+ naked_frank = FrankSinatra.new
32
+ 3.times do
33
+ naked_frank.dont_be_afraid_you_can_call_me
34
+ end
35
+ p naked_frank.call_count # => 3
36
+
37
+ memoized_frank = MemoizedFrankSinatra.new
38
+ 3.times do
39
+ memoized_frank.dont_be_afraid_you_can_call_me
40
+ end
41
+ p memoized_frank.call_count # => 1
42
+ </pre>
43
+
44
+ ## Technique
45
+
46
+ I was actually quite amused by this one. Because Ruby doesn't let you change the superclass of a Class object, I couldn't insert new methods above the includee in the hierarchy. Instead, when .memoize is called, it asks Ruby for an UnboundMethod object for each method it's supposed to memoize and hangs onto it in a class instance variable. Then it defines its own replacement for the same method.
47
+
48
+ Later, when you call the memoized method, we either look to see if we've already computed it (thus the 'memoization' part of the gem), or we grab the UnboundMethod, bind it to the caller, call it, and save the result. This process reminds me of the Wallace and Gromit film "The Wrong Trousers" -- see the comments in the Memonymous module for a link to a Google video search.
49
+
50
+ Thanks also to Jay Fields for the technique -- I had started down this path and was trying to figure it out, but came across a [2008 blog post](http://blog.jayfields.com/2008/04/alternatives-for-redefining-methods.html) with a conveniently-packaged code snippet.
51
+
@@ -0,0 +1 @@
1
+ require 'bundler/gem_tasks'
@@ -0,0 +1,41 @@
1
+ require "memonymous/version"
2
+
3
+ module Memonymous
4
+ def self.included(receiver)
5
+ receiver.extend ClassMethod
6
+ unless receiver.instance_variable_defined?('@memoized_methods')
7
+ receiver.instance_variable_set('@memoized_methods', {})
8
+ end
9
+ end
10
+
11
+ # Note the singular name!
12
+ # The point of this exercise is to memoize without cluttering up the function namespace.
13
+ module ClassMethod
14
+ def memoize(*methods)
15
+ Array(methods).flatten.map(&:to_sym).each do |mname|
16
+ @memoized_methods[mname] = instance_method(mname) # save the UnboundMethod for later
17
+ class_eval <<-RUBY
18
+ # apparently 'def' is faster than 'define_method', according to Several Random People on the Internet
19
+ def #{mname}(*args, &b)
20
+ ivar_name = '@#{mname}'
21
+
22
+ # The entire point: returning fast if we already know the answer
23
+ return instance_variable_get(ivar_name) if instance_variable_defined?(ivar_name)
24
+
25
+ # FIRST POST^H^H^H^H CALL!
26
+ # Grab the UnboundMethod, bind it, and call it.
27
+ # I like to think of this as the "Gromit's Train Track" approach to memoization.
28
+ # (See: http://www.google.com/search?q=wallace+and+gromit+train+chase if you don't get the reference.)
29
+ return instance_variable_set( ivar_name,
30
+ self \
31
+ .class \
32
+ .instance_variable_get('@memoized_methods')[:'#{mname}'] \
33
+ .bind(self) \
34
+ .call(*args, &b)
35
+ )
36
+ end
37
+ RUBY
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,3 @@
1
+ module Memonymous
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,20 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "memonymous/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "memonymous"
7
+ s.version = Memonymous::VERSION
8
+ s.authors = ["Sam Livingston-Gray"]
9
+ s.email = ["geeksam@gmail.com"]
10
+ s.homepage = ""
11
+ s.summary = %q{Yet another memoizing gem. This one doesn't pollute your method namespace.}
12
+ s.description = %q{VERY SIMPLISTIC memoization for functions (one memoized value per function; no regard for arguments). Uses UnboundMethods instead of method aliasing, so you don't see extra method names when you inspect your object.}
13
+
14
+ s.rubyforge_project = "memonymous"
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+ end
@@ -0,0 +1,57 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), *%w[test_helper]))
2
+
3
+ describe Memonymous do
4
+ before do
5
+ @memoize_this = Class.new do
6
+ attr_reader :call_count
7
+ def dont_be_afraid_you_can_call_me
8
+ @call_count ||= 0
9
+ @call_count += 1
10
+ end
11
+ end
12
+ end
13
+
14
+ it 'should add a .memoize method' do
15
+ refute @memoize_this.respond_to?(:memoize), "Method found before it should be there"
16
+ @memoize_this.send(:include, Memonymous)
17
+ assert @memoize_this.respond_to?(:memoize), "Method not found after it should've been added"
18
+ end
19
+
20
+ it 'should ONLY add a .memoize method' do
21
+ old_klass_methods = @memoize_this.methods.sort.dup
22
+ old_inst_methods = @memoize_this.instance_methods.sort.dup
23
+
24
+ @memoize_this.send(:include, Memonymous)
25
+
26
+ new_klass_methods = @memoize_this.methods.sort.dup
27
+ new_inst_methods = @memoize_this.instance_methods.sort.dup
28
+
29
+ assert_equal [:memoize], new_klass_methods - old_klass_methods
30
+ assert_equal [], new_inst_methods - old_inst_methods
31
+ end
32
+
33
+ describe 'when included into a class' do
34
+ before :each do
35
+ @memoize_this.send(:include, Memonymous)
36
+ end
37
+
38
+ it "should not memoize methods it hasn't been told about" do
39
+ foo = @memoize_this.new
40
+ foo.dont_be_afraid_you_can_call_me
41
+ foo.dont_be_afraid_you_can_call_me
42
+ assert_equal 2, foo.call_count
43
+ end
44
+
45
+ it 'should set up a @memoized_methods class instance variable' do
46
+ assert @memoize_this.instance_variables.map(&:to_sym).include?(:'@memoized_methods')
47
+ end
48
+
49
+ it 'should memoize methods it has been told about' do
50
+ @memoize_this.memoize(:dont_be_afraid_you_can_call_me)
51
+ foo = @memoize_this.new
52
+ foo.dont_be_afraid_you_can_call_me
53
+ foo.dont_be_afraid_you_can_call_me
54
+ assert_equal 1, foo.call_count
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,5 @@
1
+ require 'bundler/setup'
2
+ Bundler.setup(:default, :development)
3
+ require 'test/unit'
4
+ require 'minitest/spec'
5
+ require 'memonymous'
metadata ADDED
@@ -0,0 +1,68 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: memonymous
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 0.1.0
6
+ platform: ruby
7
+ authors:
8
+ - Sam Livingston-Gray
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2011-06-27 00:00:00 -07:00
14
+ default_executable:
15
+ dependencies: []
16
+
17
+ description: VERY SIMPLISTIC memoization for functions (one memoized value per function; no regard for arguments). Uses UnboundMethods instead of method aliasing, so you don't see extra method names when you inspect your object.
18
+ email:
19
+ - geeksam@gmail.com
20
+ executables: []
21
+
22
+ extensions: []
23
+
24
+ extra_rdoc_files: []
25
+
26
+ files:
27
+ - .gitignore
28
+ - .rvmrc
29
+ - Gemfile
30
+ - LICENSE
31
+ - README.md
32
+ - Rakefile
33
+ - lib/memonymous.rb
34
+ - lib/memonymous/version.rb
35
+ - memonymous.gemspec
36
+ - test/memonymous_test.rb
37
+ - test/test_helper.rb
38
+ has_rdoc: true
39
+ homepage: ""
40
+ licenses: []
41
+
42
+ post_install_message:
43
+ rdoc_options: []
44
+
45
+ require_paths:
46
+ - lib
47
+ required_ruby_version: !ruby/object:Gem::Requirement
48
+ none: false
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: "0"
53
+ required_rubygems_version: !ruby/object:Gem::Requirement
54
+ none: false
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: "0"
59
+ requirements: []
60
+
61
+ rubyforge_project: memonymous
62
+ rubygems_version: 1.6.0
63
+ signing_key:
64
+ specification_version: 3
65
+ summary: Yet another memoizing gem. This one doesn't pollute your method namespace.
66
+ test_files:
67
+ - test/memonymous_test.rb
68
+ - test/test_helper.rb