├── .github └── workflows │ └── ci.yml ├── .gitignore ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── lib ├── memoit.rb └── memoit │ └── version.rb ├── memoit.gemspec └── spec └── memoit_spec.rb /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Ruby CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | test: 9 | 10 | runs-on: ubuntu-latest 11 | 12 | strategy: 13 | matrix: 14 | ruby-version: 15 | - 3.1 16 | - "3.0" 17 | - 2.7 18 | - jruby-9.3 19 | 20 | steps: 21 | - uses: actions/checkout@v3 22 | - name: Set up Ruby ${{ matrix.ruby-version }} 23 | uses: ruby/setup-ruby@v1 24 | with: 25 | ruby-version: ${{ matrix.ruby-version }} 26 | bundler-cache: true # runs 'bundle install' and caches installed gems automatically 27 | - name: Run tests 28 | run: bundle exec rake 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | *.bundle 11 | *.so 12 | *.o 13 | *.a 14 | mkmf.log 15 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in memoit.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Jonas Nicklas 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Memoit 2 | 3 | [![Gem Version](https://badge.fury.io/rb/memoit.svg)](http://badge.fury.io/rb/memoit) 4 | [![Build Status](https://github.com/jnicklas/memoit/actions/workflows/ci.yml/badge.svg)](https://github.com/jnicklas/memoit/actions/workflows/ci.yml) 5 | 6 | Memoizes methods. 7 | 8 | ## Installation 9 | 10 | Add this line to your application's Gemfile: 11 | 12 | ```ruby 13 | gem 'memoit' 14 | ``` 15 | 16 | ## Usage 17 | 18 | ``` ruby 19 | class Foo 20 | memoize def bar(value) 21 | expensive_calculation(value) 22 | end 23 | 24 | memoize_class_method def self.baz(value) 25 | expensive_calculation(value) 26 | end 27 | end 28 | ``` 29 | 30 | ## Is it any good? 31 | 32 | [Yes](https://news.ycombinator.com/item?id=3067434). 33 | 34 | ## Development 35 | 36 | ```sh 37 | gem install bundler 38 | bundle install 39 | rspec 40 | ``` 41 | 42 | ## License 43 | 44 | [MIT](LICENSE.txt) 45 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task default: :spec 7 | -------------------------------------------------------------------------------- /lib/memoit.rb: -------------------------------------------------------------------------------- 1 | require "memoit/version" 2 | 3 | module Memoit 4 | 5 | # Memoize the method with the given name. 6 | # 7 | # @example 8 | # class Foo 9 | # memoize def bar(value) 10 | # expensive_calculation(value) 11 | # end 12 | # end 13 | def memoize(name) 14 | ivar_method_name = name.to_s.sub("?", "__questionmark").sub("!", "__bang") 15 | ivar_name = "@_memo_#{ivar_method_name}".to_sym 16 | mod = Module.new do 17 | define_method(name) do |*args, **kwargs, &block| 18 | return super(*args, **kwargs, &block) if block 19 | cache = instance_variable_get(ivar_name) || instance_variable_set(ivar_name, {}) 20 | cache.fetch([args, kwargs].hash) { |hash| cache[hash] = super(*args, **kwargs) } 21 | end 22 | end 23 | prepend mod 24 | name 25 | end 26 | 27 | # Memoize the class method with the given name. 28 | # 29 | # @example 30 | # class Foo 31 | # memoize_class_method def self.bar(value) 32 | # expensive_calculation(value) 33 | # end 34 | # end 35 | def memoize_class_method(name) 36 | singleton_class.memoize(name) 37 | end 38 | end 39 | 40 | Module.send(:include, Memoit) 41 | -------------------------------------------------------------------------------- /lib/memoit/version.rb: -------------------------------------------------------------------------------- 1 | module Memoit 2 | VERSION = "0.4.0" 3 | end 4 | -------------------------------------------------------------------------------- /memoit.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'memoit/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "memoit" 8 | spec.version = Memoit::VERSION 9 | spec.authors = ["Jonas Nicklas"] 10 | spec.email = ["jonas.nicklas@gmail.com"] 11 | spec.summary = %q{Memoize is back!} 12 | spec.homepage = "" 13 | spec.license = "MIT" 14 | 15 | spec.files = `git ls-files -z`.split("\x0") 16 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 17 | spec.require_paths = ["lib"] 18 | 19 | spec.required_ruby_version = ">= 2.1.0" 20 | 21 | spec.add_development_dependency "bundler", "~> 2.3" 22 | spec.add_development_dependency "rake", "~> 13.0" 23 | spec.add_development_dependency "rspec", "~> 3.10" 24 | end 25 | -------------------------------------------------------------------------------- /spec/memoit_spec.rb: -------------------------------------------------------------------------------- 1 | require "memoit" 2 | 3 | describe Memoit do 4 | let(:klass) do 5 | Class.new do 6 | memoize_class_method def self.foo 7 | rand 8 | end 9 | 10 | memoize def foo 11 | rand 12 | end 13 | 14 | memoize def bar(*values) 15 | rand 16 | end 17 | 18 | memoize def baz(pos = nil, hash = {}, kwak: nil) 19 | rand 20 | end 21 | 22 | memoize def qux(pos = nil, kwak: nil) 23 | rand 24 | end 25 | 26 | memoize def quux(kwak: nil) 27 | rand 28 | end 29 | 30 | memoize def corge(hash = {}, kwak: nil) 31 | rand 32 | end 33 | 34 | memoize def falsy 35 | foo 36 | false 37 | end 38 | 39 | memoize def query? 40 | rand 41 | end 42 | 43 | memoize def bang! 44 | rand 45 | end 46 | 47 | memoize def ☃ 48 | rand 49 | end 50 | end 51 | end 52 | let(:instance) { klass.new } 53 | 54 | describe ".memoize" do 55 | it "caches result" do 56 | expect(instance.foo).to eq(instance.foo) 57 | end 58 | 59 | it "caches results for different parameters" do 60 | a = Object.new 61 | expect(instance.bar(1)).to eq(instance.bar(1)) 62 | expect(instance.bar(2)).to eq(instance.bar(2)) 63 | expect(instance.bar(a, 1, :foo, "bar")).to eq(instance.bar(a, 1, :foo, "bar")) 64 | expect(instance.bar(2)).not_to eq(instance.bar(1)) 65 | expect(instance.bar(a, 1, :foo, "bar")).not_to eq(instance.bar(Object.new, 1, :foo, "bar")) 66 | end 67 | 68 | it "caches results when positional, hash and keyword arguments are used" do 69 | a = Object.new 70 | expect(instance.baz(a, { hash_key: "hash_value" }, kwak: "kwav")).to eq(instance.baz(a, { hash_key: "hash_value" }, kwak: "kwav")) 71 | end 72 | 73 | it "caches results when positional and keyword arguments are used" do 74 | a = Object.new 75 | expect(instance.qux(a, kwak: "kwav")).to eq(instance.qux(a, kwak: "kwav")) 76 | end 77 | 78 | it "caches results when keyword arguments are used" do 79 | expect(instance.quux(kwak: "kwav")).to eq(instance.quux(kwak: "kwav")) 80 | end 81 | 82 | it "ignores cache if keyword arguments differ" do 83 | expect(instance.quux(kwak: "1")).not_to eq(instance.quux(kwak: "2")) 84 | end 85 | 86 | it "caches results when hash and keyword arguments are used" do 87 | expect(instance.corge({ hash_key: "hash_value" }, kwak: "kwav")).to eq(instance.corge({ hash_key: "hash_value" }, kwak: "kwav")) 88 | end 89 | 90 | it "ignores cache when block given" do 91 | expect(instance.foo { }).not_to eq(instance.foo { }) 92 | end 93 | 94 | it "caches falsy values" do 95 | expect(instance).to receive(:foo).once 96 | expect(instance.falsy).to eq(instance.falsy) 97 | end 98 | 99 | it "handles question-mark methods" do 100 | expect(instance.query?).to eq(instance.query?) 101 | end 102 | 103 | it "handles bang methods" do 104 | expect(instance.bang!).to eq(instance.bang!) 105 | end 106 | 107 | it "handles non-ASCII-name methods" do 108 | expect(instance.☃).to eq(instance.☃) 109 | end 110 | 111 | it "returns the name of the method" do 112 | name = nil 113 | Class.new do 114 | name = memoize def blah 115 | end 116 | end 117 | expect(name).to eq(:blah) 118 | end 119 | 120 | it "works in a mixin" do 121 | mod = Module.new do 122 | memoize def cname 123 | self.class.name 124 | end 125 | end 126 | 127 | Foo = Class.new do 128 | include mod 129 | end 130 | 131 | Bar = Class.new do 132 | include mod 133 | end 134 | 135 | expect(Foo.new.cname).to eq("Foo") 136 | expect(Bar.new.cname).to eq("Bar") 137 | end 138 | end 139 | 140 | describe ".memoize_class_method" do 141 | it "caches result" do 142 | expect(klass.foo).to eq(klass.foo) 143 | end 144 | end 145 | end 146 | --------------------------------------------------------------------------------