stunted 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
data/.gitignore ADDED
@@ -0,0 +1,8 @@
1
+ *.gem
2
+ .bundle
3
+ .yardoc
4
+ coverage
5
+ doc
6
+ Gemfile.lock
7
+ rdoc
8
+ pkg/*
data/.rvmrc ADDED
@@ -0,0 +1,2 @@
1
+ rvm_install_on_use_flag=1
2
+ rvm --create use ruby-1.9.2
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in stunted.gemspec
4
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,30 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ stunted (0.0.1)
5
+ hamster
6
+
7
+ GEM
8
+ remote: http://rubygems.org/
9
+ specs:
10
+ assert2 (0.5.5)
11
+ git (1.2.5)
12
+ hamster (0.4.2)
13
+ jeweler (1.6.4)
14
+ bundler (~> 1.0)
15
+ git (>= 1.2.5)
16
+ rake
17
+ rake (0.9.2.2)
18
+ rcov (0.9.11)
19
+ shoulda (2.11.3)
20
+
21
+ PLATFORMS
22
+ ruby
23
+
24
+ DEPENDENCIES
25
+ assert2
26
+ bundler (~> 1.0.0)
27
+ jeweler (~> 1.6.4)
28
+ rcov
29
+ shoulda
30
+ stunted!
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2012 Brian Marick
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,11 @@
1
+ Stunted is a
2
+ [not-framework](http://www.artima.com/weblogs/viewpost.jsp?thread=8826)
3
+ to make it both possible and more pleasing to program in a
4
+ somewhat functional style in Ruby. It also contains examples
5
+ of functional style.
6
+
7
+ In the future, it will build on top of, or bring together, gems
8
+ written by other people.
9
+
10
+ Documentation is in the [wiki](https://github.com/marick/stunted/wiki).
11
+ There is a mailing list for [functional programming in Ruby](http://groups.google.com/group/rubyfoopers).
data/README.rdoc ADDED
@@ -0,0 +1,19 @@
1
+ = stunted
2
+
3
+ Description goes here.
4
+
5
+ == Contributing to stunted
6
+
7
+ * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
8
+ * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
9
+ * Fork the project
10
+ * Start a feature/bugfix branch
11
+ * Commit and push until you are happy with your contribution
12
+ * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
13
+ * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
14
+
15
+ == Copyright
16
+
17
+ Copyright (c) 2012 Brian Marick. See LICENSE.txt for
18
+ further details.
19
+
data/Rakefile ADDED
@@ -0,0 +1,28 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ require 'rake/testtask'
4
+ Rake::TestTask.new(:test) do |test|
5
+ test.libs << 'lib' << 'test'
6
+ test.pattern = 'test/**/*test*.rb'
7
+ test.verbose = true
8
+ end
9
+
10
+ require 'rcov/rcovtask'
11
+ Rcov::RcovTask.new do |test|
12
+ test.libs << 'test'
13
+ test.pattern = 'test/**/test_*.rb'
14
+ test.verbose = true
15
+ test.rcov_opts << '--exclude "gems/*"'
16
+ end
17
+
18
+ task :default => :test
19
+
20
+ require 'rdoc/task'
21
+ Rake::RDocTask.new do |rdoc|
22
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
23
+
24
+ rdoc.rdoc_dir = 'rdoc'
25
+ rdoc.title = "stunted #{version}"
26
+ rdoc.rdoc_files.include('README*')
27
+ rdoc.rdoc_files.include('lib/**/*.rb')
28
+ end
@@ -0,0 +1,59 @@
1
+ $:.unshift("../lib")
2
+ require "stunted"
3
+
4
+ # A boring class that holds a value and can produce
5
+ # new objects with an incremented value
6
+ class ValueHolder
7
+ attr_reader :value
8
+
9
+ def initialize(value)
10
+ @value = value
11
+ end
12
+
13
+ def bumped
14
+ self.class.new(value + 1)
15
+ end
16
+ end
17
+
18
+ puts ValueHolder.new(0).bumped.bumped.value # 2
19
+
20
+ # Let's let this object be sent functions as if they were messages.
21
+ class ValueHolder
22
+ include Stunted::Chainable
23
+ end
24
+
25
+ puts ValueHolder.new(0).
26
+ bumped.
27
+ defsend(-> { self.class.new(@value + 1000) }).
28
+ defsend(-> { self.class.new(value + 330) }).
29
+ value # 1331
30
+
31
+
32
+ # The need for parentheses above is annoying, but can be avoided with blocks.
33
+ puts ValueHolder.new(0).
34
+ bumped.
35
+ defsend { self.class.new(@value + 1000) }.
36
+ defsend { self.class.new(value + 330) }.
37
+ value # 1331
38
+
39
+
40
+ # Here's an example of using higher-order functions
41
+
42
+ make_adder =
43
+ lambda do | addend |
44
+ -> { self.class.new(@value + addend) }
45
+ end
46
+
47
+ puts ValueHolder.new(0).
48
+ bumped.
49
+ defsend(make_adder.(33)).
50
+ value # 34
51
+
52
+ # You can pass in extra arguments
53
+
54
+ INCREMENT_VALUE = 200
55
+
56
+ puts ValueHolder.new(0).
57
+ defsend(-> addend { self.class.new(@value + addend) }, INCREMENT_VALUE).
58
+ defsend(INCREMENT_VALUE) { | addend | self.class.new(value + addend) }.
59
+ value # 400
@@ -0,0 +1,57 @@
1
+ $:.unshift("../lib")
2
+ require "stunted"
3
+
4
+ # A boring class that holds a value and can produce
5
+ # new objects with an incremented value
6
+ class ValueHolder
7
+ attr_reader :value
8
+
9
+ def initialize(value)
10
+ @value = value
11
+ end
12
+
13
+ def bumped
14
+ self.class.new(value + 1)
15
+ end
16
+ end
17
+
18
+ puts ValueHolder.new(0).bumped.bumped.value # 2
19
+
20
+ # Let's let this object be sent functions as if they were messages.
21
+ class ValueHolder
22
+ include Stunted::Chainable
23
+ end
24
+
25
+ puts ValueHolder.new(0).
26
+ bumped.
27
+ pass_to(-> _ { _.value + 1000 }). # I'm using _ to represent "self"
28
+ succ # 1002
29
+
30
+ # The need for parentheses above is annoying, but can be avoided with blocks.
31
+ puts ValueHolder.new(0).
32
+ bumped.
33
+ pass_to { | _ | _.value + 500 }.
34
+ succ # 502
35
+
36
+
37
+ # Here's an example of using higher-order functions
38
+
39
+ make_adder =
40
+ lambda do | addend |
41
+ -> _ { _.value + addend }
42
+ end
43
+
44
+ puts ValueHolder.new(0).
45
+ bumped.
46
+ pass_to(make_adder.(33)).
47
+ succ # 35
48
+
49
+
50
+ # You can pass in extra arguments
51
+
52
+ INCREMENT_VALUE = 200
53
+
54
+ puts ValueHolder.new(0).
55
+ pass_to(-> _, addend { _.class.new(_.value + addend) }, INCREMENT_VALUE).
56
+ pass_to(INCREMENT_VALUE) { | _, addend | _.class.new(_.value + addend) }.
57
+ value # 400
@@ -0,0 +1,33 @@
1
+ $:.unshift("../lib")
2
+ require "stunted"
3
+
4
+ # `defn` can be used to declare functions within modules. They are
5
+ # available with `include` or `extend`.
6
+
7
+
8
+ module FunctionProvider
9
+ extend Stunted::Defn
10
+
11
+ defn :add, -> a, b { a + b }
12
+
13
+ captured = 3
14
+ defn :add_captured, -> a { a + captured }
15
+ end
16
+
17
+ # Here's a local definition of a function:
18
+ add_local = -> a, b { a + b }
19
+ puts add_local.(1, 2) # 3
20
+
21
+ include FunctionProvider
22
+
23
+ puts add.(1, 2) # 3
24
+
25
+ puts add_captured.(1) # 4
26
+
27
+ some_local = 88
28
+ FunctionProvider.defn :captured_value, -> { some_local }
29
+
30
+ puts captured_value.() # 88
31
+ some_local = 99
32
+ puts captured_value.() # 99
33
+
@@ -0,0 +1,20 @@
1
+ $:.unshift("../lib")
2
+ require "stunted"
3
+
4
+ ## This example needs more work.
5
+
6
+ include Stunted
7
+
8
+ module Shape
9
+ def shape; "shape"; end
10
+ end
11
+
12
+ reservation_maker = FunctionalHash.make_maker(Shape)
13
+
14
+ reservation = reservation_maker.(:a => 1, :b => 2)
15
+
16
+ puts reservation.shape
17
+
18
+
19
+
20
+
data/lib/includer.rb ADDED
@@ -0,0 +1,40 @@
1
+
2
+
3
+ class FunctionalHash < Hash
4
+
5
+ def initialize(hash = {})
6
+ hash.each do | k, v |
7
+ self[k] = v
8
+ # Not important for this example
9
+ end
10
+ end
11
+
12
+ def merge(hash)
13
+ self.class.new(super(hash))
14
+ end
15
+
16
+
17
+ def become(mod)
18
+ klass = Class.new(FunctionalHash)
19
+ klass.send(:include, mod)
20
+ klass.new(self)
21
+ end
22
+
23
+ end
24
+
25
+
26
+ module TimesliceShaped
27
+ def timeslice_fun; "timeslice_fun!"; end
28
+ end
29
+
30
+ puts FunctionalHash.new.become(TimesliceShaped).timeslice_fun
31
+
32
+
33
+ data_full = FunctionalHash.new(id: 33, name: "Dawn")
34
+ function_full = data_full.become(TimesliceShaped)
35
+
36
+
37
+ puts FunctionalHash.new.become(TimesliceShaped).merge(:a => 3)
38
+ puts FunctionalHash.new.become(TimesliceShaped).merge(:a => 3).timeslice_fun
39
+
40
+
@@ -0,0 +1,40 @@
1
+ module Stunted
2
+
3
+ module Defn
4
+
5
+ # Note: if you use a block with defn, you get block semantics. In particular,
6
+ # don't try to return from such a block.
7
+ def defn(name, fn = nil, &block)
8
+ if fn
9
+ define_method(name) { fn }
10
+ else
11
+ define_method(name) { lambda(&block) } # Todo: why is this lambda rigamarole required?
12
+ end
13
+ module_function name if respond_to?(:module_function, true)
14
+ end
15
+ module_function :defn
16
+ public :defn
17
+ end
18
+
19
+ module Chainable
20
+
21
+ def pass_to(*args, &block)
22
+ if block_given?
23
+ lambda(&block).(self, *args)
24
+ else
25
+ fn = args.shift
26
+ fn.(self, *args)
27
+ end
28
+ end
29
+
30
+ def defsend(*args, &block)
31
+ if block_given?
32
+ instance_exec(*args, &block)
33
+ else
34
+ fn = args.shift
35
+ instance_exec(*args, &fn)
36
+ end
37
+ end
38
+ end
39
+
40
+ end
@@ -0,0 +1,123 @@
1
+ module Stunted
2
+
3
+ class FunctionalHash < Hash
4
+
5
+ def initialize(hash = {})
6
+ hash.each do | k, v |
7
+ self[k] = v
8
+ if k.is_a?(Symbol)
9
+ if self.respond_to?(k)
10
+ $stderr.puts("Warning: #{k.inspect} overrides existing Hash method.")
11
+ end
12
+ instance_eval("def #{k}; self[#{k.inspect}]; end")
13
+ end
14
+ end
15
+ end
16
+
17
+ def fetch(key, *args, &block)
18
+ current = super(key, *args, &block)
19
+ if current.is_a?(Proc)
20
+ if current.arity == 0
21
+ self[key] = current.()
22
+ else
23
+ self[key] = current.(self)
24
+ end
25
+ else
26
+ current
27
+ end
28
+ end
29
+
30
+ def merge(hash)
31
+ self.class.new(super(hash))
32
+ end
33
+ alias_method :+, :merge
34
+
35
+ def change_within(*args)
36
+ if (args.first.is_a? Hash)
37
+ merge(args.first)
38
+ else
39
+ key = args.first
40
+ rest = args[1..-1]
41
+ merge(key => fetch(key).change_within(*rest))
42
+ end
43
+ end
44
+
45
+ def remove(*keys)
46
+ new_hash = dup
47
+ keys.each { | key | new_hash.send(:delete, key) }
48
+ self.class.new(new_hash)
49
+ end
50
+
51
+ def remove_within(*args)
52
+ key = args.first
53
+ rest = args[1..-1]
54
+ if (args.count <= 1)
55
+ remove(key)
56
+ else
57
+ merge(key => fetch(key).remove_within(*rest))
58
+ end
59
+ end
60
+
61
+ def -(keys)
62
+ keys = [keys] unless keys.respond_to?(:first)
63
+ remove(*keys)
64
+ end
65
+
66
+ def [](key)
67
+ fetch(key, nil)
68
+ end
69
+
70
+ def only(*keys)
71
+ self.class[*keys.zip(self.values_at(*keys)).flatten(1)]
72
+ end
73
+
74
+ def self.shaped_class(*shapes)
75
+ klass = Class.new(self)
76
+ shapes.each do | mod |
77
+ klass.send(:include, mod)
78
+ end
79
+ klass
80
+ end
81
+
82
+ def self.make_maker(*shapes)
83
+ klass = shaped_class(*shapes)
84
+ ->(inits={}) { klass.new(inits) }
85
+ end
86
+
87
+ def become(*shapes)
88
+ self.class.shaped_class(*shapes).new(self)
89
+ end
90
+
91
+ def component(hash) # For now, just one pair
92
+ field = hash.keys.first
93
+ shape = hash.values.first
94
+ secondary_module = Module.new
95
+ shape.instance_methods.each do | meth |
96
+ defn = "def #{meth}(*args)
97
+ merge(#{field.inspect} => self.#{field}.#{meth}(*args))
98
+ end"
99
+ secondary_module.module_eval(defn)
100
+ end
101
+ merge(field => self[field].become(shape)).become(secondary_module)
102
+ end
103
+
104
+ private :[]=, :clear, :delete, :delete_if
105
+ end
106
+
107
+
108
+ module FHUtil
109
+ def F(hash = {})
110
+ FunctionalHash.new(hash)
111
+ end
112
+
113
+ def Fonly(tuples)
114
+ F(tuples.first)
115
+ end
116
+
117
+ def Fall(tuples)
118
+ tuples.map { | row | F(row) }
119
+ end
120
+ end
121
+
122
+
123
+ end
@@ -0,0 +1,3 @@
1
+ module Stunted
2
+ VERSION = "0.0.1"
3
+ end
data/lib/stunted.rb ADDED
@@ -0,0 +1,3 @@
1
+ require "stunted/version"
2
+ require "stunted/chaining"
3
+ require "stunted/functional-hash"
data/stunted.gemspec ADDED
@@ -0,0 +1,30 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "stunted/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "stunted"
7
+ s.homepage = "http://github.com/marick/stunted"
8
+ s.license = "MIT"
9
+ s.summary = %Q{Support code for functional programming}
10
+ s.description = %Q{Support code for functional programming}
11
+ s.email = "marick@exampler.com"
12
+ s.authors = ["Brian Marick"]
13
+ s.required_ruby_version = '>= 1.9.2'
14
+ s.version = Stunted::VERSION
15
+
16
+ s.rubyforge_project = "stunted"
17
+
18
+ s.files = `git ls-files`.split("\n")
19
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
20
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
21
+ s.require_paths = ["lib"]
22
+
23
+ s.add_runtime_dependency "hamster"
24
+
25
+ s.add_development_dependency "shoulda", ">= 0"
26
+ s.add_development_dependency "assert2"
27
+ s.add_development_dependency "bundler", "~> 1.0.0"
28
+ s.add_development_dependency "jeweler", "~> 1.6.4"
29
+ s.add_development_dependency "rcov", ">= 0"
30
+ end
@@ -0,0 +1,80 @@
1
+ require 'helper'
2
+
3
+ class TestChaining < Test::Unit::TestCase
4
+ extend Stunted::Defn
5
+
6
+ class ChainableValueHolder
7
+ extend Stunted::Defn
8
+ include Stunted::Chainable
9
+ def initialize(value)
10
+ @value = value
11
+ end
12
+
13
+ defn :some_function_that_refers_to_self, -> { @value }
14
+
15
+ attr_reader :value
16
+ end
17
+
18
+ def setup
19
+ @contains_three = ChainableValueHolder.new(3)
20
+ end
21
+
22
+ context "pass_to passes self as an explicit argument" do
23
+ should "to blocks" do
24
+ captured_addend = 33
25
+ result = @contains_three.pass_to { | _ | _.value + captured_addend }
26
+ assert { result == 36 }
27
+ end
28
+
29
+ should "to lambdas" do
30
+ captured_addend = 33
31
+ result = @contains_three.pass_to -> _ { _.value + captured_addend }
32
+ assert { result == 36 }
33
+ end
34
+
35
+ should "even with newly-generated functions" do
36
+ self.class.defn :add_maker do | addend |
37
+ -> _ { _.value + addend }
38
+ end
39
+
40
+ result = @contains_three.pass_to add_maker.(3)
41
+ assert { result == 6 }
42
+ end
43
+
44
+ should "allow extra arguments" do
45
+ result = @contains_three.pass_to(-> _, addend { _.value + addend }, 33)
46
+ assert { result == 36}
47
+
48
+ result = @contains_three.pass_to(33) { |_, addend | _.value + addend }
49
+ assert { result == 36}
50
+ end
51
+
52
+ end
53
+
54
+ context "defsend is like defining a temporary method and then sending to it" do
55
+ should "allow method defined with block" do
56
+ captured_addend = 33
57
+ assert { 36 == @contains_three.defsend { self.value + captured_addend } }
58
+ end
59
+
60
+ should "allow method defined with lambda" do
61
+ captured_addend = 33
62
+ result = @contains_three.defsend(-> {@value + captured_addend })
63
+ assert { result == 36 }
64
+ end
65
+
66
+ should "allow method to be defined elsewhere" do
67
+ captured_addend = 33
68
+ result = @contains_three.defsend(@contains_three.some_function_that_refers_to_self)
69
+ assert { result == 3 }
70
+ end
71
+
72
+ should "allow arguments" do
73
+ result = @contains_three.defsend(-> addend { @value + addend }, 33)
74
+ assert { result == 36}
75
+
76
+ result = @contains_three.defsend(33) { |addend | value + addend }
77
+ assert { result == 36}
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,42 @@
1
+ require 'helper'
2
+
3
+ class DefnTest < Test::Unit::TestCase
4
+
5
+ module FunctionProvider
6
+ extend Stunted::Defn
7
+
8
+ defn :add_from_lambda, -> a, b { a + b}
9
+ defn :higher_order_add_2, add_from_lambda.curry.(2)
10
+
11
+ defn :add_from_proc do | a, b |
12
+ a + b
13
+ end
14
+
15
+ defn(:add_from_proc_alternate_syntax) { | a, b | a + b }
16
+
17
+ addend = 3
18
+ defn :add_with_captured_var, -> a { a + addend }
19
+ end
20
+
21
+ include FunctionProvider
22
+
23
+ should "allow all functions to be called" do
24
+ assert { add_from_lambda.(1, 2) == 3 }
25
+ assert { higher_order_add_2.(3) == 5 }
26
+ assert { add_from_proc.(3, 4) == 7 }
27
+ assert { add_from_proc_alternate_syntax.(4, 5) == 9 }
28
+ assert { add_with_captured_var.(2) == 5 }
29
+ end
30
+
31
+
32
+ captured = 3
33
+ FunctionProvider.defn :add_some, -> a { a + captured }
34
+
35
+ should "allow definitions to contain captured variables" do
36
+ assert { add_some.(2) == 5 }
37
+ end
38
+
39
+ end
40
+
41
+
42
+
@@ -0,0 +1,267 @@
1
+ require 'helper'
2
+
3
+ class FunctionalHashTest < Test::Unit::TestCase
4
+ include Stunted
5
+ include Stunted::FHUtil
6
+
7
+
8
+ context "non-lazy behavior" do
9
+ setup do
10
+ @sut = FunctionalHash.new(:a => 1, 2 => "two")
11
+ end
12
+
13
+ should "lookup like regular hash" do
14
+ assert { @sut[:a] == 1 }
15
+ assert { @sut[2] == "two" }
16
+ end
17
+
18
+ should "also allow method-like lookup" do
19
+ assert { @sut.a == 1 }
20
+ end
21
+
22
+ should "be == to other hashes" do
23
+ assert { @sut == {:a => 1, 2 => "two"}}
24
+ end
25
+ end
26
+
27
+ context "lazy behavior" do
28
+ should "take procs as arguments and evaluate them on demand" do
29
+ @sut = FunctionalHash.new(:a => lambda { @global })
30
+ @global = 33
31
+ assert { @sut.a == 33 }
32
+ # And the value now remains
33
+ @global = 99999999
34
+ assert { @sut.a == 33 }
35
+ end
36
+
37
+ should "give an argument to the lambda if wants access to `self`." do
38
+ @sut = FunctionalHash.new(:a => lambda { |h| h.b + 1 }, :b => 2)
39
+ assert { @sut.a == 3 }
40
+ end
41
+ end
42
+
43
+ context "immutable behavior" do
44
+ should "prevent assignment, etc" do
45
+ # todo - more of these to come
46
+ @sut = FunctionalHash.new(:a => 3)
47
+ assert_raises(NoMethodError) { @sut[:a] = 5 }
48
+ assert_raises(NoMethodError) { @sut.clear }
49
+ assert_raises(NoMethodError) { @sut.delete }
50
+ assert_raises(NoMethodError) { @sut.delete_if }
51
+ end
52
+
53
+ should "create new object upon addition" do
54
+ sut = FunctionalHash.new(:a => 1)
55
+ added = sut + {:b => 3}
56
+ assert { added.is_a?(FunctionalHash) }
57
+ assert { added == {:a => 1, :b => 3} }
58
+ assert { added != sut }
59
+ assert { added.object_id != sut.object_id }
60
+ end
61
+
62
+ should "you can also use merge" do
63
+ sut = FunctionalHash.new(:a => 1)
64
+ added = sut.merge(:b => 3)
65
+ assert { added.is_a?(FunctionalHash) }
66
+ assert { added == {:a => 1, :b => 3} }
67
+ assert { added != sut }
68
+ assert { added.object_id != sut.object_id }
69
+ end
70
+
71
+ should "create new object upon removal" do
72
+ sut = FunctionalHash.new(:a => 1, :b => lambda {2}, :c => 3)
73
+ only_c = sut.remove(:a, :b)
74
+ assert { only_c.is_a?(FunctionalHash) }
75
+ assert { only_c == {:c => 3} }
76
+ assert { only_c != sut }
77
+ assert { only_c.object_id != sut.object_id }
78
+ end
79
+
80
+ should "can also use `-` with both one argument and an array" do
81
+ sut = FunctionalHash.new(:a => 1, :b => lambda {2}, :c => 3)
82
+ empty = sut - :a - [:b, :c]
83
+ assert { empty.is_a?(FunctionalHash) }
84
+ assert { empty == {} }
85
+ assert { empty != sut }
86
+ assert { empty.object_id != sut.object_id }
87
+ end
88
+
89
+ context "changing within" do
90
+ setup do
91
+ @hashlike = F(:val => 3,
92
+ :other => "other",
93
+ :nested => F(:val => 33,
94
+ :other => "other",
95
+ :nested => F(:val => 333,
96
+ :other => "other")))
97
+ end
98
+
99
+ should "act as + at zero level" do
100
+ actual = @hashlike.change_within(val: 4)
101
+ assert { actual.is_a?(FunctionalHash) }
102
+ assert { actual.val == 4 }
103
+ assert { actual.other == "other" }
104
+ end
105
+
106
+ should "allow nesting" do
107
+ actual = @hashlike.change_within(:nested, val => 44)
108
+ assert { actual.is_a?(FunctionalHash) }
109
+ assert { actual.val == 3 }
110
+ assert { actual.nested.val == 44 }
111
+ assert { actual.nested.other == "other" }
112
+ end
113
+
114
+ should "allow n levels of nesting" do
115
+ actual = @hashlike.change_within(:nested, :nested, val => 444)
116
+ assert { actual.is_a?(FunctionalHash) }
117
+ assert { actual.val == 3 }
118
+ assert { actual.nested.val == 33 }
119
+ assert { actual.nested.nested.val == 444 }
120
+ assert { actual.nested.nested.other == "other" }
121
+ end
122
+
123
+ end
124
+
125
+ context "changing within" do
126
+ setup do
127
+ @hashlike = F(:val => 3,
128
+ :other => "other",
129
+ :nested => F(:val => 33,
130
+ :other => "other",
131
+ :nested => F(:val => 333,
132
+ :other => "other")))
133
+ end
134
+
135
+ should "act as - at zero level" do
136
+ actual = @hashlike.remove_within(:val)
137
+ assert { actual.is_a?(FunctionalHash) }
138
+ assert { !actual.has_key?(:val) }
139
+ assert { actual.other == "other" }
140
+ end
141
+
142
+ should "allow nesting" do
143
+ actual = @hashlike.remove_within(:nested, :val)
144
+ assert { actual.is_a?(FunctionalHash) }
145
+ assert { actual.val == 3 }
146
+ assert { ! actual.nested.has_key?(:val) }
147
+ assert { actual.nested.other == "other" }
148
+ end
149
+
150
+ should "allow n levels of nesting" do
151
+ actual = @hashlike.remove_within(:nested, :nested, :val)
152
+ assert { actual.is_a?(FunctionalHash) }
153
+ assert { actual.val == 3 }
154
+ assert { actual.nested.val == 33 }
155
+ assert { ! actual.nested.nested.has_key?(:val) }
156
+ assert { actual.nested.nested.other == "other" }
157
+ end
158
+
159
+ end
160
+
161
+ context "making smaller hashes" do
162
+ should "be done with the `only` method" do
163
+ hashlike = F(:a => 1, :b => 2, :c => 3)
164
+ actual = hashlike.only(:a, :c)
165
+ assert { actual.is_a?(FunctionalHash) }
166
+ assert { actual == {:a => 1, :c => 3} }
167
+ end
168
+ end
169
+
170
+ context "shapes" do
171
+ context "becoming a particular shape" do
172
+
173
+ module OddShaped
174
+ def oddly
175
+ self.merge(:odd => "once")
176
+ end
177
+ end
178
+
179
+ module EvenShaped
180
+ def evenly
181
+ self.merge(:even => "once")
182
+ end
183
+ end
184
+
185
+ should "allow module methods to be called" do
186
+ hashlike = FunctionalHash.new.become(OddShaped, EvenShaped)
187
+ assert { hashlike.oddly.odd == "once" }
188
+ end
189
+
190
+ should "cause a copy of the extended object to contain the same functions" do
191
+ hashlike = FunctionalHash.new.become(OddShaped, EvenShaped)
192
+ assert { hashlike.oddly.evenly == {:odd => "once", :even => "once" } }
193
+ end
194
+
195
+ should "accumulate the functions from multiple `become` calls." do
196
+ hashlike = FunctionalHash.new.become(OddShaped).become(EvenShaped)
197
+ assert { hashlike.oddly.evenly == {:odd => "once", :even => "once" } }
198
+ end
199
+ end
200
+ end
201
+
202
+ context "containing a particular shape" do
203
+
204
+ module Timesliced
205
+ def shift_timeslice(days)
206
+ merge(:first_date => first_date + days,
207
+ :last_date => last_date + days)
208
+ end
209
+ end
210
+
211
+ class ::Fixnum
212
+ def days
213
+ self
214
+ end
215
+
216
+ def week
217
+ self * 7
218
+ end
219
+ end
220
+
221
+ should "allow spaces of subcomponents to be described" do
222
+ original =
223
+ F(name: "fred",
224
+ timeslice: F(first_date: Date.new(2001, 1, 1),
225
+ last_date: Date.new(2001, 2, 2))).
226
+ component(:timeslice => Timesliced)
227
+
228
+ shifted =
229
+ original.
230
+ shift_timeslice(1.week).
231
+ shift_timeslice(2.days)
232
+
233
+ assert { shifted.timeslice.first_date == original.timeslice.first_date + 9 }
234
+ assert { shifted.timeslice.last_date == original.timeslice.last_date + 9 }
235
+ end
236
+ end
237
+
238
+ context "maker functions" do
239
+ module Round
240
+ def round; "round!"; end
241
+ end
242
+
243
+ module Cylindrical
244
+ def cylindrical; "cylinder"; end
245
+ end
246
+
247
+ reservation_maker = FunctionalHash.make_maker(Round, Cylindrical)
248
+ reservation = reservation_maker.(a: 1, b: 2)
249
+
250
+ should "make regular immutable hash" do
251
+ assert { reservation.a == 1 }
252
+ end
253
+
254
+ should "include shape functions" do
255
+ assert { reservation.round == "round!" }
256
+ assert { reservation.cylindrical == "cylinder" }
257
+ end
258
+
259
+ should "include shape functions in hashes created from this hash" do
260
+ assert { reservation.merge(c: 3).round == "round!" }
261
+ end
262
+
263
+ end
264
+ end
265
+ end
266
+
267
+
data/test/helper.rb ADDED
@@ -0,0 +1,19 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ begin
4
+ Bundler.setup(:default, :development)
5
+ rescue Bundler::BundlerError => e
6
+ $stderr.puts e.message
7
+ $stderr.puts "Run `bundle install` to install missing gems"
8
+ exit e.status_code
9
+ end
10
+ require 'test/unit'
11
+ require 'shoulda'
12
+ require 'assert2'
13
+
14
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
15
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
16
+ require 'stunted'
17
+
18
+ class Test::Unit::TestCase
19
+ end
metadata ADDED
@@ -0,0 +1,138 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: stunted
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Brian Marick
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-01-19 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: hamster
16
+ requirement: &2156591000 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *2156591000
25
+ - !ruby/object:Gem::Dependency
26
+ name: shoulda
27
+ requirement: &2156590360 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: *2156590360
36
+ - !ruby/object:Gem::Dependency
37
+ name: assert2
38
+ requirement: &2156395500 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ type: :development
45
+ prerelease: false
46
+ version_requirements: *2156395500
47
+ - !ruby/object:Gem::Dependency
48
+ name: bundler
49
+ requirement: &2156394280 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: 1.0.0
55
+ type: :development
56
+ prerelease: false
57
+ version_requirements: *2156394280
58
+ - !ruby/object:Gem::Dependency
59
+ name: jeweler
60
+ requirement: &2156393420 !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ~>
64
+ - !ruby/object:Gem::Version
65
+ version: 1.6.4
66
+ type: :development
67
+ prerelease: false
68
+ version_requirements: *2156393420
69
+ - !ruby/object:Gem::Dependency
70
+ name: rcov
71
+ requirement: &2156392760 !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ! '>='
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: *2156392760
80
+ description: Support code for functional programming
81
+ email: marick@exampler.com
82
+ executables: []
83
+ extensions: []
84
+ extra_rdoc_files: []
85
+ files:
86
+ - .document
87
+ - .gitignore
88
+ - .rvmrc
89
+ - Gemfile
90
+ - Gemfile.lock
91
+ - LICENSE.txt
92
+ - README.md
93
+ - README.rdoc
94
+ - Rakefile
95
+ - examples/chaining-of-function-with-IMPLICIT-self.rb
96
+ - examples/chaining-of-function-with-explicit-passing-of-self.rb
97
+ - examples/defining-functions-in-modules.rb
98
+ - examples/functional-hashes-with-shapes.rb
99
+ - lib/includer.rb
100
+ - lib/stunted.rb
101
+ - lib/stunted/chaining.rb
102
+ - lib/stunted/functional-hash.rb
103
+ - lib/stunted/version.rb
104
+ - stunted.gemspec
105
+ - test/chainable-tests.rb
106
+ - test/defn-tests.rb
107
+ - test/functional-hash-test.rb
108
+ - test/helper.rb
109
+ homepage: http://github.com/marick/stunted
110
+ licenses:
111
+ - MIT
112
+ post_install_message:
113
+ rdoc_options: []
114
+ require_paths:
115
+ - lib
116
+ required_ruby_version: !ruby/object:Gem::Requirement
117
+ none: false
118
+ requirements:
119
+ - - ! '>='
120
+ - !ruby/object:Gem::Version
121
+ version: 1.9.2
122
+ required_rubygems_version: !ruby/object:Gem::Requirement
123
+ none: false
124
+ requirements:
125
+ - - ! '>='
126
+ - !ruby/object:Gem::Version
127
+ version: '0'
128
+ requirements: []
129
+ rubyforge_project: stunted
130
+ rubygems_version: 1.8.10
131
+ signing_key:
132
+ specification_version: 3
133
+ summary: Support code for functional programming
134
+ test_files:
135
+ - test/chainable-tests.rb
136
+ - test/defn-tests.rb
137
+ - test/functional-hash-test.rb
138
+ - test/helper.rb