Skip to content

Commit

Permalink
First commit.
Browse files Browse the repository at this point in the history
  • Loading branch information
Dan Yoder committed Jun 7, 2008
0 parents commit b8e904e
Show file tree
Hide file tree
Showing 9 changed files with 235 additions and 0 deletions.
63 changes: 63 additions & 0 deletions Rakefile
@@ -0,0 +1,63 @@
require 'rubygems'
require 'date'
require 'rake/rdoctask'
require 'rake/testtask'
require 'fileutils'

Gem::manage_gems
include FileUtils

require 'rake/gempackagetask'

spec = Gem::Specification.new do |s|
s.platform = Gem::Platform::RUBY
s.required_ruby_version = '>= 1.8.6'
s.name = "functor"
# s.rubyforge_project = 'autocode'
s.version = "0.2"
s.authors = ["Dan Yoder"]
s.email = 'dan@zeraweb.com'
s.homepage = 'http://dev.zeraweb.com/'
s.summary = "Pattern-based dispatch for Ruby, inspired by Topher Cyll's multi."
s.files = Dir['*/**/*']
#s.bindir = 'bin'
#s.executables = []
s.require_path = "lib"
s.has_rdoc = true
end

task :package => :clean do
Gem::Builder.new(spec).build
end

task :clean do
system 'rm -rf *.gem'
end

task :install => :package do
system 'sudo gem install ./*.gem'
end

desc "create .gemspec file (useful for github)"
task :gemspec do
filename = "#{spec.name}.gemspec"
File.open(filename, "w") do |f|
f.puts spec.to_ruby
end
end

#desc 'Publish rdoc to RubyForge.'
#task :publish do
# # `rsync -avz --delete doc/rdoc/ dyoder67@rubyforge.org:/var/www/gforge-projects/autocode/`
# `scp -r doc/rdoc dyoder67@rubyforge.org:/var/www/gforge-projects/autocode/`
#end

Rake::RDocTask.new do |rdoc|
rdoc.rdoc_dir = 'doc/rdoc'
rdoc.options << '--line-numbers'
rdoc.rdoc_files.add([ 'README', 'HISTORY', 'lib/*.rb' ])
end

Rake::TestTask.new(:test) do |t|
t.test_files = FileList["test/*.rb"].exclude("test/helpers.rb")
end
2 changes: 2 additions & 0 deletions doc/HISTORY
@@ -0,0 +1,2 @@
0.1 - Initial implemention of Functor class.
0.2 - Added method dispatch, to_proc support, tests.
36 changes: 36 additions & 0 deletions doc/README
@@ -0,0 +1,36 @@
Functor provides pattern-based function and method dispatch for Ruby. To use it in a class:

class Repeater
attr_accessor :times
include Functor::Method
functor( :repeat, Integer ) { |x| x * @times }
functor( :repeat, String ) { |s| [].fill( s, 0..@times ).join(' ') }
end

r = Repeater.new
r.times = 5
r.repeat( 5 ) # => 25
r.repeat( "-" ) # => "- - - - -"
r.repeat( 7.3 ) # => RuntimeError!

Warning: This defines a class instance variable @functors behind the scenes as a side-effect. Also, functors won't inherit properly at this point.

You can also define Functor objects directly:

fib ||= Functor.new do
given( 0 ) { 0 }
given( 1 ) { 1 }
given( Integer ) { |n| self.call( n - 1 ) + self.call( n - 2 ) }
end

You can use functors directly with functions taking a block like this:

[ *0..10 ].map( &fib ) # => [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

You can explicitly bind self using #bind:

fun.bind( obj )

which is actually how the method dispatch is implemented.

Arguments are matched first using === and then ==, so anything that supports these methods can be matched against.
18 changes: 18 additions & 0 deletions functor.gemspec
@@ -0,0 +1,18 @@
Gem::Specification.new do |s|
s.name = %q{functor}
s.version = "0.2"

s.specification_version = 2 if s.respond_to? :specification_version=

s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
s.authors = ["Dan Yoder"]
s.date = %q{2008-06-07}
s.email = %q{dan@zeraweb.com}
s.files = ["doc/HISTORY", "doc/README", "lib/functor.rb", "lib/object.rb", "test/fib.rb", "test/functor.rb", "test/helpers.rb"]
s.has_rdoc = true
s.homepage = %q{http://dev.zeraweb.com/}
s.require_paths = ["lib"]
s.required_ruby_version = Gem::Requirement.new(">= 1.8.6")
s.rubygems_version = %q{1.0.1}
s.summary = %q{Pattern-based dispatch for Ruby, inspired by Topher Cyll's multi.}
end
41 changes: 41 additions & 0 deletions lib/functor.rb
@@ -0,0 +1,41 @@
require 'lib/object'
class Functor

module Method

def self.included( k )
k.module_eval { @functors = {} }
def k.functor( name, *args, &block )
unless @functors[name]
@functors[name] = Functor.new
eval("def #{name}( *args, &block ) ; self.class.module_eval { @functors[ :#{name} ] }.bind( self ).call( *args, &block ) ; end" )
end
@functors[name].given( *args, &block )
end
end
end

def initialize( &block ) ; @patterns = []; instance_eval(&block) if block_given? ; end

def given( *pattern, &block ) ; @patterns.push [ pattern, block ] ; self ; end

def bind( object ) ; @object = object ; self ; end

def call( *args, &block )
args.push( block ) if block_given?
pattern, action = @patterns.find { |pattern, action| match?( args, pattern ) }
raise "argument mismatch for argument(s): #{args.inspect}." unless action
@object ? @object.instance_exec( *args, &action ) : action.call( *args )
end

def to_proc
lambda { |*args| self.call(*args) }
end

private

def match?( args, pattern )
pattern.zip(args).all? { |x,y| x === y or x == y } if pattern.length == args.length
end

end
17 changes: 17 additions & 0 deletions lib/object.rb
@@ -0,0 +1,17 @@
class Object
# This is an extremely powerful little function that will be built-in to Ruby 1.9.
# This version is from Mauricio Fernandez via ruby-talk. Works like instance_eval
# except that you can pass parameters to the block. This means you can define a block
# intended for use with instance_eval, pass it to another method, which can then
# invoke with parameters. This is used quite a bit by the Waves::Mapping code.
def instance_exec(*args, &block)
mname = "__instance_exec_#{Thread.current.object_id.abs}"
class << self; self end.class_eval{ define_method(mname, &block) }
begin
ret = send(mname, *args)
ensure
class << self; self end.class_eval{ undef_method(mname) } rescue nil
end
ret
end
end
16 changes: 16 additions & 0 deletions test/fib.rb
@@ -0,0 +1,16 @@
require 'test/helpers'

fib ||= Functor.new do
given( 0 ) { 0 }
given( 1 ) { 1 }
given( Integer ) { |n| self.call( n - 1 ) + self.call( n - 2 ) }
end

describe "Dispatch on a functor object should" do

specify "be able to implement the Fibonacci function" do
[*0..10].map( &fib ).should == [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
end

end

28 changes: 28 additions & 0 deletions test/functor.rb
@@ -0,0 +1,28 @@
require 'test/helpers'

class Repeater
attr_accessor :times
include Functor::Method
functor( :repeat, Integer ) { |x| x * @times }
functor( :repeat, String ) { |s| [].fill( s, 0, @times ).join(' ') }
end

describe "Dispatch on instance method should" do

before do
@r = Repeater.new
@r.times = 5
end

specify "invoke different methods with object scope based on arguments" do
@r.repeat( 5 ).should == 25
@r.repeat( "-" ).should == '- - - - -'
end

specify "should raise an exception if there is no matching value" do
lambda { @r.repeat( 7.3) }.should.raise
end

end


14 changes: 14 additions & 0 deletions test/helpers.rb
@@ -0,0 +1,14 @@
require 'rubygems'
%w{ bacon }.each { |dep| require dep }
Bacon.summary_on_exit

module Kernel
private
def specification(name, &block) Bacon::Context.new(name, &block) end
end

Bacon::Context.instance_eval do
alias_method :specify, :it
end

require 'lib/functor'

0 comments on commit b8e904e

Please sign in to comment.