├── .gitignore ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── lib ├── method_missing.rb └── method_missing │ ├── ap_method.rb │ ├── cons_method.rb │ ├── method_extension.rb │ └── version.rb ├── method_missing.gemspec └── spec ├── method_missing ├── ap_method_spec.rb └── cons_method_spec.rb ├── method_missing_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | .bundle 3 | Gemfile.lock 4 | pkg/* 5 | *swp 6 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | # Specify your gem's dependencies in method_missing.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c)2011, Mike Burns 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above 12 | copyright notice, this list of conditions and the following 13 | disclaimer in the documentation and/or other materials provided 14 | with the distribution. 15 | 16 | * Neither the name of Mike Burns nor the names of other 17 | contributors may be used to endorse or promote products derived 18 | from this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | method\_missing 2 | ============== 3 | 4 | The Method gem you've been waiting for. 5 | 6 | The method\_missing gem brings the functional tools you need to Ruby: method composition, sequencing, and repeating. These are useful for when you must combine methods, apply different methods to the same argument, or "grow" a method. 7 | 8 | I'll explain, but first: 9 | 10 | Installing 11 | ---------- 12 | 13 | gem install method_missing 14 | 15 | Usage: Composing 16 | ---------------- 17 | 18 | This is the classic. In algebra class you learned that `f(g(x))` can also be written `(f . g)(x)`. This is handy because now you have this `(f . g)` object that you can pass to integrals and whatnot. 19 | 20 | So in Ruby, using the method\_missing gem: 21 | 22 | def escape_everything(text) 23 | everything_escaper.call(text) 24 | end 25 | 26 | def everything_escaper 27 | method(:html_escape) * method(:escape_javascript) * method(:json_escape) 28 | end 29 | 30 | And more algebraically: 31 | 32 | (f * g).call(x) == f.call(g.call(x)) 33 | 34 | Usage: Sequencing 35 | ----------------- 36 | 37 | This doesn't come up as often but when it does, oh boy, does it ever! This is useful for when you have a bunch of methods to apply to the same argument. For example, using the method\_missing gem: 38 | 39 | def valid_options?(options_checker, options) 40 | options_checker.at_most_one( 41 | (method(:sort_mod_time) / method(:sort_access_time)).call(options)) && 42 | options_checker.at_most_one( 43 | (method(:sort_file_size) / method(:sort_mod_time)).call(options)) 44 | end 45 | 46 | Again, more algebraically: 47 | 48 | (f / g / h).call(x) == [f.call(x), g.call(x), h.call(x)] 49 | 50 | Usage: Repeating 51 | ---------------- 52 | 53 | This one comes up the least in most people's day-to-day life. It's the concept of applying a method to its output, repeatedly. Here's a contrived example, using the method\_missing gem: 54 | 55 | def church_encoding(n) 56 | if n.zero? 57 | lambda{|f,x| x} 58 | else 59 | lambda{|f,x| (f ^ n).call(x)} 60 | end 61 | end 62 | 63 | Algebraically: 64 | 65 | (f ^ 3).call(4) == f.call(f.call(f.call(4))) 66 | 67 | Usage: Combining 68 | ---------------- 69 | 70 | These are objects which can be combined! Here's an example, also contrived, that turns an object into a number, *n*, then adds four to *n* and multiplies *n* by six, then sums those results, using primitive methods that add one to a number and multiply a number by two: 71 | 72 | def interesting_math(o) 73 | four_and_six.call(o).sum 74 | end 75 | 76 | def four_and_six 77 | ((add1 ^ 4) / (mul2 ^ 3)) * method(:to_i) 78 | end 79 | 80 | To show this algebraically: 81 | 82 | (((add1 ^ 4) / (mul2 ^ 3)) * method(:to_i)).call(o) == [ 83 | add1.call(add1.call(add1.call(add1.call(o.to_i)))), 84 | mul2.call(mul2.call(mul2.call(o.to_i))) 85 | ] 86 | 87 | Copyright 88 | --------- 89 | Copyright 2017 Mike Burns 90 | -------------------------------------------------------------------------------- /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/method_missing.rb: -------------------------------------------------------------------------------- 1 | require "method_missing/version" 2 | require 'method_missing/method_extension' 3 | -------------------------------------------------------------------------------- /lib/method_missing/ap_method.rb: -------------------------------------------------------------------------------- 1 | require 'method_missing/cons_method' 2 | 3 | class ApMethod 4 | def initialize(methods) 5 | @methods = methods 6 | end 7 | 8 | def *(f) 9 | ApMethod.new(@methods.map {|m| ConsMethod.new(m, f)}) 10 | end 11 | 12 | def /(f) 13 | ApMethod.new(@methods + [f]) 14 | end 15 | 16 | def ^(power) 17 | ApMethod.new(@methods.map {|m| m ^ power}) 18 | end 19 | 20 | 21 | def call(*x) 22 | @methods.map{|m| m.call(*x)} 23 | end 24 | 25 | def to_proc 26 | lambda {|*x| 27 | @methods.map {|m| m.call(*x) } 28 | } 29 | end 30 | 31 | def inspect 32 | "#" 33 | end 34 | 35 | def arity 36 | @methods.first.arity 37 | end 38 | 39 | def [](*x) 40 | call(*x) 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/method_missing/cons_method.rb: -------------------------------------------------------------------------------- 1 | require 'method_missing/ap_method' 2 | 3 | class ConsMethod 4 | def initialize(f,g) 5 | @f = f 6 | @g = g 7 | end 8 | 9 | def *(h) 10 | ConsMethod.new(self, h) 11 | end 12 | 13 | def /(h) 14 | ApMethod.new([@f, @g, h]) 15 | end 16 | 17 | def ^(n) 18 | if n < 2 19 | self 20 | else 21 | ConsMethod.new(self, self ^ (n-1)) 22 | end 23 | end 24 | 25 | 26 | def owner 27 | @g.owner 28 | end 29 | 30 | def receiver 31 | @g.receiver 32 | end 33 | 34 | def to_proc 35 | Proc.new {|x| @f.call(*@g.call(*x)) } 36 | end 37 | 38 | def inspect 39 | "#" 40 | end 41 | 42 | def arity 43 | @g.arity 44 | end 45 | 46 | def call(x) 47 | @f.call(*@g.call(*x)) 48 | end 49 | 50 | def [](*x) 51 | call(*x) 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/method_missing/method_extension.rb: -------------------------------------------------------------------------------- 1 | require 'method_missing/cons_method' 2 | require 'method_missing/ap_method' 3 | 4 | class Method 5 | def *(g) 6 | ConsMethod.new(self,g) 7 | end 8 | 9 | def /(g) 10 | ApMethod.new([self, g]) 11 | end 12 | 13 | def ^(power) 14 | if power < 2 15 | self 16 | else 17 | ConsMethod.new(self, self ^ (power-1)) 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/method_missing/version.rb: -------------------------------------------------------------------------------- 1 | module MethodMissing 2 | VERSION = "0.0.1" 3 | end 4 | -------------------------------------------------------------------------------- /method_missing.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path("../lib", __FILE__) 3 | require "method_missing/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "method_missing" 7 | s.version = MethodMissing::VERSION 8 | s.authors = ["Mike Burns"] 9 | s.email = ["mike@mike-burns.com"] 10 | s.homepage = "http://github.com/mike-burns/method_missing" 11 | s.license = 'BSD' 12 | s.summary = %q{Compose, sequence, and repeat Ruby methods.} 13 | s.description = %q{ 14 | The methods on methods that you've been missing. 15 | 16 | This gem adds the #* #/ and #^ methods so you can compose, sequence, and 17 | repeat methods. 18 | 19 | By composing methods you can express that one method calls another more 20 | obviously. 21 | 22 | By sequencing methods you can express that a series of methods have the 23 | same argument more succinctly. 24 | 25 | By repeating a method you can compose it with itself as needed, to build 26 | upon itself. 27 | } 28 | 29 | s.rubyforge_project = "method_missing" 30 | 31 | s.files = `git ls-files`.split("\n") 32 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 33 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 34 | s.require_paths = ["lib"] 35 | 36 | s.add_development_dependency('rspec') 37 | s.add_development_dependency('rake') 38 | end 39 | -------------------------------------------------------------------------------- /spec/method_missing/ap_method_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'method_missing/ap_method' 3 | require 'method_missing/method_extension' 4 | 5 | describe ApMethod do 6 | def add3_m(x) 7 | x + 3 8 | end 9 | def mul2_m(x) 10 | x * 2 11 | end 12 | def sub1_m(x) 13 | x - 1 14 | end 15 | def div4_m(x) 16 | x / 4 17 | end 18 | 19 | let(:sub1) { method(:sub1_m) } 20 | let(:mul2) { method(:mul2_m) } 21 | let(:add3) { method(:add3_m) } 22 | let(:div4) { method(:div4_m) } 23 | 24 | subject { ApMethod.new([sub1, mul2, div4]) } 25 | 26 | it "composes" do 27 | (subject * add3).call(4).should == 28 | [sub1.call(add3.call(4)), 29 | mul2.call(add3.call(4)), 30 | div4.call(add3.call(4))] 31 | end 32 | 33 | it "sequences" do 34 | result = [sub1.call(4), mul2.call(4), div4.call(4), add3.call(4)] 35 | (subject / add3).call(4).should == result 36 | end 37 | 38 | it "repeats" do 39 | power = 3 40 | argument = 4 41 | result = [ 42 | (sub1 ^ power).call(argument), 43 | (mul2 ^ power).call(argument), 44 | (div4 ^ power).call(argument)] 45 | (subject ^ power).call(argument).should == result 46 | end 47 | 48 | context 'the Method interface' do 49 | it "is callable" do 50 | subject.call(4).should == [sub1.call(4), mul2.call(4), div4.call(4)] 51 | end 52 | 53 | it "has #[] as an alias for #call" do 54 | subject[4].should == [sub1.call(4), mul2.call(4), div4.call(4)] 55 | end 56 | 57 | it "knows the arity of the method" do 58 | subject.arity.should == 1 59 | end 60 | 61 | it "inspects into something nice" do 62 | expected = "#" 63 | subject.inspect.should == expected 64 | end 65 | 66 | it "converts itself to a Proc" do 67 | subject.to_proc.should be_a(Proc) 68 | subject.to_proc.call(4).should == [sub1.call(4), mul2.call(4), div4.call(4)] 69 | end 70 | 71 | it "is unable to know the receiver" do 72 | expect { subject.receiver }.to raise_error 73 | end 74 | 75 | it "is unable to know the method owner" do 76 | expect { subject.owner }.to raise_error 77 | end 78 | 79 | it "is unable to know its name" do 80 | expect { subject.name }.to raise_error 81 | end 82 | 83 | it "is unable to unbind" do 84 | expect { subject.unbind }.to raise_error 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /spec/method_missing/cons_method_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'method_missing/cons_method' 3 | 4 | describe ConsMethod do 5 | def add3_m(x) 6 | x + 3 7 | end 8 | def mul2_m(x) 9 | x * 2 10 | end 11 | def sub1_m(x) 12 | x - 1 13 | end 14 | 15 | let(:sub1) { method(:sub1_m) } 16 | let(:mul2) { method(:mul2_m) } 17 | let(:add3) { method(:add3_m) } 18 | 19 | subject { ConsMethod.new(sub1, mul2) } 20 | 21 | it "composes" do 22 | (subject * add3).call(4).should == sub1.call(mul2.call(add3.call(4))) 23 | end 24 | 25 | it "sequences" do 26 | (subject / add3).call(4).should == [sub1.call(4), mul2.call(4), add3.call(4)] 27 | end 28 | 29 | it "repeats" do 30 | result = sub1.call(mul2.call( 31 | sub1.call(mul2.call( 32 | sub1.call(mul2.call(4)))))) 33 | (subject ^ 3).call(4).should == result 34 | end 35 | 36 | context 'the Method interface' do 37 | it "is callable" do 38 | subject.call(4).should == sub1.call(mul2.call(4)) 39 | end 40 | 41 | it "has #[] as an alias for #call" do 42 | subject[4].should == sub1.call(mul2.call(4)) 43 | end 44 | 45 | it "knows the arity of the method" do 46 | subject.arity.should == 1 47 | end 48 | 49 | it "inspects into something nice" do 50 | expected = "#" 51 | subject.inspect.should == expected 52 | end 53 | 54 | it "converts itself to a Proc" do 55 | subject.to_proc.should be_a(Proc) 56 | subject.to_proc.call(4).should == sub1.call(mul2.call(4)) 57 | end 58 | 59 | it "knows the receiver" do 60 | subject.receiver.inspect.should == mul2.receiver.inspect 61 | end 62 | 63 | it "knows the method owner" do 64 | subject.owner.should == mul2.owner 65 | end 66 | 67 | it "is unable to know its name" do 68 | expect { subject.name }.to raise_error 69 | end 70 | 71 | it "is unable to unbind" do 72 | expect { subject.unbind }.to raise_error 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /spec/method_missing_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'method_missing' 3 | 4 | describe Method do 5 | def sub1_m(x) 6 | x - 1 7 | end 8 | def mul2_m(x) 9 | x * 2 10 | end 11 | 12 | let(:sub1) { method(:sub1_m) } 13 | let(:mul2) { method(:mul2_m) } 14 | 15 | it "adds a composition operator to a method" do 16 | (sub1 * mul2).call(3).should == sub1.call(mul2.call(3)) 17 | end 18 | 19 | it "adds a sequencing operator to a method" do 20 | (sub1 / mul2).call(3).should == [sub1.call(3), mul2.call(3)] 21 | end 22 | 23 | it "adds a repeat operator to a method" do 24 | (mul2 ^ 3).call(3).should == mul2.call(mul2.call(mul2.call(3))) 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rspec' 2 | --------------------------------------------------------------------------------