├── .gitignore ├── .rspec ├── .ruby-version ├── .travis.yml ├── Gemfile ├── LICENCE ├── README.md ├── Rakefile ├── benchmark.rb ├── example └── foo.rb ├── lib ├── memoist2.rb └── memoist2 │ └── version.rb ├── memoist2.gemspec └── spec ├── memoist2_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | coverage 6 | InstalledFiles 7 | lib/bundler/man 8 | pkg 9 | rdoc 10 | spec/reports 11 | test/tmp 12 | test/version_tmp 13 | tmp 14 | 15 | Gemfile.lock 16 | # YARD artifacts 17 | .yardoc 18 | _yardoc 19 | doc/ -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format progress 3 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.4 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.0 4 | - 2.1 5 | - 2.2 6 | - 2.3 7 | - 2.4 8 | - ruby-head 9 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | gemspec 3 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Matthew Rudy Jacobs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | memoist2 2 | ======== 3 | 4 | [![Build Status](https://travis-ci.org/matthewrudy/memoist2.png?branch=master)](https://travis-ci.org/matthewrudy/memoist2) 5 | 6 | Simple Memoization for Ruby 2.0+. 7 | 8 | ### Differences between Memoist & Memoist2 9 | 10 | Unlike [Memoist], this is **not** a drop-in replacement for old ActiveSupport::Memoizable. Memoist will still work just fine for that if you're using Ruby 2.0+. This project, on the other hand, is just me playing using Module#prepend which makes this stuff very simple. 11 | 12 | **[Memoist]** 13 | * Works on all Rubies. 14 | * Is quite complicated. 15 | * Has the exact same api as `ActiveSupport::Memoizable`. 16 | 17 | **Memoist2** 18 | * Only works on `Ruby >= 2.0.0`. 19 | * Is deliberately much simpler. 20 | * Has a slightly different API that could totally change. 21 | 22 | [Memoist]: https://github.com/matthewrudy/memoist 23 | 24 | 25 | Example 26 | ------- 27 | 28 | Memoize an instance method 29 | 30 | class Foo 31 | include Memoist2 32 | 33 | def bar 34 | sleep 1 && 2**10 35 | end 36 | memoize :bar 37 | end 38 | 39 | Memoize a class method 40 | 41 | class Foo 42 | include Memoist2 43 | 44 | def self.bar 45 | # something expensive 46 | end 47 | memoize_class_method :bar 48 | end 49 | 50 | Licence 51 | ------- 52 | 53 | Licensed under the MIT licence. 54 | 55 | See the file LICENCE. 56 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rspec/core/rake_task' 2 | RSpec::Core::RakeTask.new(:spec) 3 | task :default => :spec 4 | 5 | require "bundler/gem_tasks" 6 | -------------------------------------------------------------------------------- /benchmark.rb: -------------------------------------------------------------------------------- 1 | <<-INTRO 2 | 3 | Question: 4 | Is Memoist2 performant? 5 | 6 | Answer: 7 | 8 | Raw is a little bit slower 9 | user system total real 10 | raw method 2.220000 0.000000 2.220000 ( 2.216027) 11 | memoist2 method 1.670000 0.000000 1.670000 ( 1.670678) 12 | manual memoized (||=) 1.280000 0.000000 1.280000 ( 1.286099) 13 | manual memoized (defined?) 1.380000 0.000000 1.380000 ( 1.372037) 14 | 15 | INTRO 16 | 17 | require 'benchmark' 18 | require 'memoist2' 19 | 20 | TIMES = 10_000_000 21 | 22 | class MyClass 23 | include Memoist2 24 | 25 | def raw 26 | (2**10)**2 # something mildly difficult 27 | end 28 | 29 | def memoized 30 | 1 31 | end 32 | memoize :memoized 33 | 34 | def variable_cache 35 | @variable_cache ||= 1 36 | end 37 | 38 | def defined_cache 39 | unless defined?(@defined_cache) 40 | @defined_cache = 1 41 | end 42 | @defined_cache 43 | end 44 | end 45 | 46 | INSTANCE = MyClass.new 47 | 48 | Benchmark.bm(30) do |x| 49 | x.report("raw method") do 50 | TIMES.times do 51 | INSTANCE.raw 52 | end 53 | end 54 | 55 | x.report("memoist2 method") do 56 | TIMES.times do 57 | INSTANCE.memoized 58 | end 59 | end 60 | 61 | x.report("manual memoized (||=)") do 62 | TIMES.times do 63 | INSTANCE.variable_cache 64 | end 65 | end 66 | 67 | x.report("manual memoized (defined?)") do 68 | TIMES.times do 69 | INSTANCE.defined_cache 70 | end 71 | end 72 | end -------------------------------------------------------------------------------- /example/foo.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../../lib/memoist2', __FILE__) 2 | 3 | class Foo 4 | extend Memoist2 5 | 6 | def string 7 | "string" 8 | end 9 | 10 | def string_memoized 11 | "string" 12 | end 13 | memoize :string_memoized 14 | 15 | def fixnum 16 | 42 17 | end 18 | 19 | def fixnum_memoized 20 | 42 21 | end 22 | memoize :fixnum_memoized 23 | end 24 | 25 | require 'benchmark' 26 | 27 | TIMES = 10_000_000 28 | 29 | Benchmark.bmbm do |x| 30 | 31 | x.report "string - memoized" do 32 | foo = Foo.new 33 | TIMES.times do 34 | foo.string_memoized 35 | end 36 | end 37 | 38 | x.report "string - unmemoized" do 39 | foo = Foo.new 40 | TIMES.times do 41 | foo.string 42 | end 43 | end 44 | 45 | x.report "fixnum - memoized" do 46 | foo = Foo.new 47 | TIMES.times do 48 | foo.fixnum_memoized 49 | end 50 | end 51 | 52 | x.report "fixnum - unmemoized" do 53 | foo = Foo.new 54 | TIMES.times do 55 | foo.fixnum 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/memoist2.rb: -------------------------------------------------------------------------------- 1 | require "memoist2/version" 2 | 3 | module Memoist2 4 | 5 | def self.memoized_ivar_for(symbol) 6 | "@_memoized_#{symbol.to_s.sub(/\?\Z/, '_query').sub(/!\Z/, '_bang')}".to_sym 7 | end 8 | 9 | module ClassMethods 10 | 11 | def memoize(*method_names) 12 | method_names.each do |method_name| 13 | memoized_ivar = Memoist2.memoized_ivar_for(method_name) 14 | memoized_module = Module.new do 15 | module_eval <<-EVAL 16 | def #{method_name} 17 | unless #{memoized_ivar} 18 | #{memoized_ivar} = [super] 19 | end 20 | #{memoized_ivar}[0] 21 | end 22 | 23 | def self.to_s 24 | "Memoist2::MemoizedMethod(#{method_name})" 25 | end 26 | def self.inspect; to_s; end 27 | EVAL 28 | end 29 | prepend memoized_module 30 | end 31 | end 32 | 33 | def memoize_class_method(*method_names) 34 | singleton_class.class_eval do 35 | include Memoist2 unless ancestors.include?(Memoist2) 36 | memoize *method_names 37 | end 38 | end 39 | end 40 | 41 | def self.included(klass) 42 | klass.extend(ClassMethods) 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/memoist2/version.rb: -------------------------------------------------------------------------------- 1 | module Memoist2 2 | VERSION = "0.3.0" 3 | end 4 | -------------------------------------------------------------------------------- /memoist2.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'memoist2/version' 5 | 6 | 7 | Gem::Specification.new do |s| 8 | # Basic details 9 | s.name = "memoist2" 10 | s.version = Memoist2::VERSION 11 | s.authors = ["Matthew Rudy Jacobs"] 12 | s.email = ["matthewrudyjacobs@gmail.com"] 13 | 14 | s.summary = "Really simple memoization for ruby 2.0" 15 | s.homepage = "https://github.com/matthewrudy/memoist2" 16 | s.license = "MIT" 17 | 18 | # Only works with Ruby 2 19 | s.required_ruby_version = '>= 2.0.0' 20 | 21 | # Add any extra files to include in the gem 22 | s.files = `git ls-files`.split 23 | s.require_paths = ["lib"] 24 | s.test_files = s.files.grep(%r{^(test|spec|features)/}) 25 | 26 | # Dependencies to run tests 27 | s.add_development_dependency("rspec") 28 | s.add_development_dependency("rake") 29 | end 30 | -------------------------------------------------------------------------------- /spec/memoist2_spec.rb: -------------------------------------------------------------------------------- 1 | require 'memoist2' 2 | 3 | describe Memoist2 do 4 | 5 | # used as base class for examples 6 | class Counter 7 | def initialize 8 | @counter = 0 9 | end 10 | attr_reader :counter 11 | 12 | def count! 13 | @counter += 1 14 | end 15 | end 16 | 17 | describe "nil values" do 18 | subject do 19 | Class.new(Counter) do 20 | include Memoist2 21 | 22 | def nilly 23 | count! 24 | nil 25 | end 26 | memoize :nilly 27 | end.new 28 | end 29 | 30 | it "returns the expected value" do 31 | 5.times do 32 | expect(subject.nilly).to be nil 33 | end 34 | end 35 | 36 | it "memoizes correctly" do 37 | expect do 38 | 5.times{ subject.nilly } 39 | end.to change{ subject.counter }.by(1) 40 | end 41 | end 42 | 43 | describe "instance methods" do 44 | subject do 45 | Class.new(Counter) do 46 | include Memoist2 47 | 48 | def foo 49 | count! 50 | :bar 51 | end 52 | memoize :foo 53 | end.new 54 | end 55 | 56 | it "returns the expected value" do 57 | 5.times do 58 | expect(subject.foo).to be :bar 59 | end 60 | end 61 | 62 | it "memoizes correctly" do 63 | expect do 64 | 5.times{ subject.foo } 65 | end.to change{ subject.counter }.by(1) 66 | end 67 | 68 | it "can memoize multiple methods simultaneously" do 69 | subject = Class.new(Counter) do 70 | include Memoist2 71 | 72 | def a 73 | count! && :a 74 | end 75 | 76 | def b 77 | count! && :b 78 | end 79 | 80 | memoize :a, :b 81 | end.new 82 | 83 | 5.times{ subject.a } 84 | 5.times{ subject.b } 85 | 86 | expect(subject.counter).to be 2 87 | end 88 | end 89 | 90 | describe "class methods" do 91 | describe "using metaclass" do 92 | subject do 93 | Class.new do 94 | class << self 95 | include Memoist2 96 | 97 | def foo 98 | @counter ||= 0 99 | @counter += 1 100 | end 101 | memoize :foo 102 | end 103 | end 104 | end 105 | 106 | it "works" do 107 | 5.times do 108 | expect(subject.foo).to be 1 109 | end 110 | end 111 | end 112 | 113 | describe "using memoize_class_method" do 114 | subject do 115 | Class.new do 116 | include Memoist2 117 | 118 | def self.foo 119 | @counter ||= 0 120 | @counter += 1 121 | end 122 | memoize_class_method :foo 123 | end 124 | end 125 | 126 | it "works" do 127 | 5.times do 128 | expect(subject.foo).to be 1 129 | end 130 | end 131 | end 132 | end 133 | 134 | describe "punctuated methods" do 135 | subject do 136 | Class.new do 137 | include Memoist2 138 | 139 | def question? 140 | @question_calls ||= 0 141 | @question_calls += 1 142 | end 143 | memoize :question? 144 | 145 | def bang! 146 | @bang_calls ||= 0 147 | @bang_calls += 1 148 | end 149 | memoize :bang! 150 | 151 | end.new 152 | end 153 | 154 | it "can memoize question methods" do 155 | 5.times do 156 | expect(subject.question?).to be 1 157 | end 158 | end 159 | 160 | it "can memoize bang methods" do 161 | 5.times do 162 | expect(subject.bang!).to be 1 163 | end 164 | end 165 | end 166 | end 167 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # This file was generated by the `rspec --init` command. Conventionally, all 2 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. 3 | # Require this file using `require "spec_helper"` to ensure that it is only 4 | # loaded once. 5 | # 6 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 7 | RSpec.configure do |config| 8 | config.treat_symbols_as_metadata_keys_with_true_values = true 9 | config.run_all_when_everything_filtered = true 10 | config.filter_run :focus 11 | 12 | # Run specs in random order to surface order dependencies. If you find an 13 | # order dependency and want to debug it, you can fix the order by providing 14 | # the seed, which is printed after each run. 15 | # --seed 1234 16 | config.order = 'random' 17 | end 18 | --------------------------------------------------------------------------------