comp_tree 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. data/README +153 -0
  2. data/Rakefile +152 -0
  3. data/comp_tree.gemspec +38 -0
  4. data/contrib/quix/Rakefile +16 -0
  5. data/contrib/quix/install.rb +3 -0
  6. data/contrib/quix/lib/quix/builtin/dir/casefold_brackets.rb +7 -0
  7. data/contrib/quix/lib/quix/builtin/kernel/tap.rb +9 -0
  8. data/contrib/quix/lib/quix/builtin/module/include.rb +21 -0
  9. data/contrib/quix/lib/quix/builtin/module/private.rb +41 -0
  10. data/contrib/quix/lib/quix/config.rb +37 -0
  11. data/contrib/quix/lib/quix/cygwin.rb +60 -0
  12. data/contrib/quix/lib/quix/diagnostic.rb +44 -0
  13. data/contrib/quix/lib/quix/enumerable.rb +33 -0
  14. data/contrib/quix/lib/quix/fileutils.rb +37 -0
  15. data/contrib/quix/lib/quix/hash_struct.rb +27 -0
  16. data/contrib/quix/lib/quix/kernel.rb +61 -0
  17. data/contrib/quix/lib/quix/lazy_struct.rb +55 -0
  18. data/contrib/quix/lib/quix/simple_installer.rb +87 -0
  19. data/contrib/quix/lib/quix/string.rb +38 -0
  20. data/contrib/quix/lib/quix/subpackager.rb +52 -0
  21. data/contrib/quix/lib/quix/thread_local.rb +32 -0
  22. data/contrib/quix/lib/quix/vars.rb +138 -0
  23. data/contrib/quix/lib/quix.rb +32 -0
  24. data/contrib/quix/test/all.rb +12 -0
  25. data/contrib/quix/test/test_deps.rb +25 -0
  26. data/contrib/quix/test/test_include.rb +47 -0
  27. data/contrib/quix/test/test_private.rb +86 -0
  28. data/contrib/quix/test/test_root.rb +19 -0
  29. data/contrib/quix/test/test_struct.rb +48 -0
  30. data/contrib/quix/test/test_vars.rb +187 -0
  31. data/install.rb +3 -0
  32. data/lib/comp_tree/algorithm.rb +210 -0
  33. data/lib/comp_tree/bucket_ipc.rb +151 -0
  34. data/lib/comp_tree/driver.rb +267 -0
  35. data/lib/comp_tree/error.rb +27 -0
  36. data/lib/comp_tree/node.rb +165 -0
  37. data/lib/comp_tree/quix/builtin/kernel/tap.rb +33 -0
  38. data/lib/comp_tree/quix/diagnostic.rb +68 -0
  39. data/lib/comp_tree/quix/kernel.rb +85 -0
  40. data/lib/comp_tree/retriable_fork.rb +42 -0
  41. data/lib/comp_tree/task_node.rb +22 -0
  42. data/lib/comp_tree.rb +23 -0
  43. data/test/all.rb +12 -0
  44. data/test/test_bucketipc.rb +72 -0
  45. data/test/test_circular.rb +36 -0
  46. data/test/test_comp_tree.rb +364 -0
  47. data/test/test_exception.rb +97 -0
  48. metadata +120 -0
data/README ADDED
@@ -0,0 +1,153 @@
1
+
2
+ = CompTree -- Parallel Computation Tree
3
+
4
+ == Synopsis
5
+
6
+ require 'comp_tree'
7
+
8
+ CompTree::Driver.new { |driver|
9
+
10
+ # Define a function named 'area' taking these three arguments.
11
+ driver.define_area(:width, :height, :offset) { |width, height, offset|
12
+ width*height - offset
13
+ }
14
+
15
+ # Define a function 'width' which takes a 'border' argument.
16
+ driver.define_width(:border) { |border|
17
+ 2 + border
18
+ }
19
+
20
+ # Ditto for 'height'.
21
+ driver.define_height(:border) { |border|
22
+ 3 + border
23
+ }
24
+
25
+ # Define a constant function 'border'.
26
+ driver.define_border {
27
+ 5
28
+ }
29
+
30
+ # Ditto for 'offset'.
31
+ driver.define_offset {
32
+ 7
33
+ }
34
+
35
+ # Compute the area using four parallel threads.
36
+ area = driver.compute(:area, :threads => 4)
37
+
38
+ # We've done this computation.
39
+ if area == (2 + 5)*(3 + 5) - 7
40
+ puts "It worked!"
41
+ else
42
+ puts "Send bug report to ..."
43
+ end
44
+ }
45
+
46
+ === Alternative Forms for Function Definitions
47
+
48
+ This form evals a lambda, saving you the repeat parameter list:
49
+
50
+ driver.define_area :width, :height, :offset, %{
51
+ width*height - offset
52
+ }
53
+
54
+ driver.define_width :border, %{
55
+ 2 + border
56
+ }
57
+
58
+ Notice the '<code>%</code>' before the brace. The lambda is created
59
+ just once, during the time of definition.
60
+
61
+ Finally there is the raw form which uses no +eval+ or
62
+ +method_missing+ tricks:
63
+
64
+ driver.define(:area, :width, :height, :offset) { |width, height, offset|
65
+ width*height - offset
66
+ }
67
+
68
+ driver.define(:width, :border) { |border|
69
+ 2 + border
70
+ }
71
+
72
+ == Important Notes
73
+
74
+ The user should have a basic understanding of <em>functional
75
+ programming</em> (see for example
76
+ http://en.wikipedia.org/wiki/Functional_programming) and the meaning
77
+ of <em>side effects</em>.
78
+
79
+ CompTree requires the user to adopt a functional style. Every
80
+ function you define must explicitly depend on the data it uses.
81
+
82
+ #
83
+ # BAD example: depending on state -- offset not listed as a parameter
84
+ #
85
+ driver.define_area(:width, :height) { |width, height|
86
+ width*height - offset
87
+ }
88
+
89
+ Unless <em>offset</em> is really a constant, the result of
90
+ <tt>driver.compute(:area, :num_threads => n)</tt> is not well-defined
91
+ for _n_ > 1.
92
+
93
+ Just as depending on some changeable state is bad, it is likewise bad
94
+ to affect a state (to produce a <em>side effect</em>).
95
+
96
+ #
97
+ # BAD example: affecting state
98
+ #
99
+ driver.define_area(:width, :height, :offset) { |width, height, offset|
100
+ ACCUMULATOR.add "more data"
101
+ width*height - offset
102
+ }
103
+
104
+ Given a tree where nodes are modifying _ACCUMULATOR_, the end state of
105
+ _ACCUMULATOR_ is not well-defined. Moreover if _ACCUMULATOR_ is not
106
+ thread-safe, the result will be even worse.
107
+
108
+ Note however it is OK affect a state as long as <em>no other function
109
+ depends on that state</em>. This is the principle under which
110
+ +comp_tree+ parallelizes Rake tasks (http://drake.rubyforge.org).
111
+
112
+ == Install
113
+
114
+ % gem install comp_tree
115
+
116
+ Or for the regular (non-gem) .tgz package,
117
+
118
+ % ruby install.rb [--uninstall]
119
+
120
+ == Links
121
+
122
+ * Download: http://rubyforge.org/frs/?group_id=6917
123
+ * Rubyforge home: http://rubyforge.org/projects/comptree
124
+ * Repository: http://github.com/quix/comp_tree
125
+
126
+ == Author
127
+
128
+ * James M. Lawrence <quixoticsycophant@gmail.com>
129
+
130
+ == License
131
+
132
+ Copyright (c) 2008 James M. Lawrence. All rights reserved.
133
+
134
+ Permission is hereby granted, free of charge, to any person
135
+ obtaining a copy of this software and associated documentation files
136
+ (the "Software"), to deal in the Software without restriction,
137
+ including without limitation the rights to use, copy, modify, merge,
138
+ publish, distribute, sublicense, and/or sell copies of the Software,
139
+ and to permit persons to whom the Software is furnished to do so,
140
+ subject to the following conditions:
141
+
142
+ The above copyright notice and this permission notice shall be
143
+ included in all copies or substantial portions of the Software.
144
+
145
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
146
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
147
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
148
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
149
+ BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
150
+ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
151
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
152
+ SOFTWARE.
153
+
data/Rakefile ADDED
@@ -0,0 +1,152 @@
1
+ $LOAD_PATH.unshift "contrib/quix/lib"
2
+
3
+ require 'rake/gempackagetask'
4
+ require 'rake/contrib/rubyforgepublisher'
5
+ require 'quix/subpackager'
6
+
7
+ $VERBOSE = nil
8
+ require 'rdoc/rdoc'
9
+ $VERBOSE = true
10
+
11
+ require 'fileutils'
12
+ include FileUtils
13
+
14
+ gemspec = eval(File.read("comp_tree.gemspec"))
15
+ package_name = gemspec.name
16
+ package_name_in_ruby = "CompTree"
17
+
18
+ # I would prefer "doc", but "html" is hard-coded for rubyforgepublisher
19
+ doc_dir = "html"
20
+
21
+ ######################################################################
22
+ # clean
23
+
24
+ task :clean => [:clobber, :clean_doc] do
25
+ end
26
+
27
+ task :clean_doc do
28
+ rm_rf(doc_dir)
29
+ end
30
+
31
+ ######################################################################
32
+ # test
33
+
34
+ task :test do
35
+ require 'test/all'
36
+ end
37
+
38
+ ######################################################################
39
+ # package
40
+
41
+ task :package => :clean
42
+
43
+ Rake::GemPackageTask.new(gemspec) { |t|
44
+ t.need_tar = true
45
+ }
46
+
47
+ ######################################################################
48
+ # doc
49
+
50
+ task :doc => :clean_doc do
51
+ files = %w(README) + %w(driver error node task_node).map { |name|
52
+ "lib/comp_tree/#{name}.rb"
53
+ }
54
+
55
+ options = [
56
+ "-o", doc_dir,
57
+ "--title", "comp_tree: #{gemspec.summary}",
58
+ "--main", "README"
59
+ ]
60
+
61
+ RDoc::RDoc.new.document(files + options)
62
+ end
63
+
64
+ ######################################################################
65
+ # repackage files from contrib/
66
+
67
+ task :generate_rb do
68
+ packages = {
69
+ :comp_tree => {
70
+ :name_in_ruby => "CompTree",
71
+ :lib_dir => "./lib",
72
+ :subpackages => {
73
+ :quix => {
74
+ :name_in_ruby => "Quix",
75
+ :sources => [
76
+ "diagnostic",
77
+ "kernel",
78
+ "builtin/kernel/tap",
79
+ ],
80
+ :lib_dir => "./contrib/quix/lib",
81
+ :ignore_root_rb => true,
82
+ },
83
+ },
84
+ },
85
+ }
86
+ Quix::Subpackager.run(packages)
87
+ end
88
+
89
+ ######################################################################
90
+ # git
91
+
92
+ def git(*args)
93
+ cmd = ["git"] + args
94
+ sh(*cmd)
95
+ end
96
+
97
+ task :init_contrib do
98
+ unless `git remote`.split.include? "quix"
99
+ git(*%w!remote add -f quix git@github.com:quix/quix.git!)
100
+ end
101
+ end
102
+
103
+ task :add_contrib_first_time => :init_contrib do
104
+ git(*%w!merge --squash -s ours --no-commit quix/master!)
105
+ git(*%w!read-tree --prefix=contrib/quix -u quix/master!)
106
+ git("commit", "-m", "add quix utils")
107
+ end
108
+
109
+ task :run_pull_contrib do
110
+ git(*%w!pull --no-commit -s subtree quix master!)
111
+ end
112
+
113
+ task :pull_contrib => [ :init_contrib, :run_pull_contrib, :generate_rb ]
114
+
115
+ ######################################################################
116
+ # publisher
117
+
118
+ task :publish => :doc do
119
+ Rake::RubyForgePublisher.new('comptree', 'quix').upload
120
+ end
121
+
122
+ ######################################################################
123
+ # release
124
+
125
+ task :prerelease => :clean do
126
+ rm_rf(doc_dir)
127
+ rm_rf("pkg")
128
+ unless `git status` =~ %r!nothing to commit \(working directory clean\)!
129
+ raise "Directory not clean"
130
+ end
131
+ end
132
+
133
+ task :finish_release do
134
+ %w(gem tgz).each_with_index { |ext, index|
135
+ sh("rubyforge",
136
+ (index == 0 ? "add_release" : "add_file"),
137
+ gemspec.rubyforge_project,
138
+ gemspec.rubyforge_project,
139
+ gemspec.version.to_s,
140
+ "pkg/#{gemspec.name}-#{gemspec.version}.#{ext}")
141
+ }
142
+ git("tag", gemspec.version.to_s)
143
+ git("push")
144
+ end
145
+
146
+ task :release =>
147
+ [
148
+ :prerelease,
149
+ :package,
150
+ :publish,
151
+ :finish_release,
152
+ ]
data/comp_tree.gemspec ADDED
@@ -0,0 +1,38 @@
1
+
2
+ Gem::Specification.new { |t|
3
+ t.author = "James M. Lawrence"
4
+ t.email = "quixoticsycophant@gmail.com"
5
+ t.summary = "Parallel Computation Tree"
6
+ t.name = "comp_tree"
7
+ t.rubyforge_project = "comptree"
8
+ t.homepage = "comptree.rubyforge.org"
9
+ t.version = "0.5.0"
10
+ t.description = "Build a computation tree and execute it with N " +
11
+ "parallel threads. Optionally fork computation nodes into new processes."
12
+
13
+ t.files = %w{README comp_tree.gemspec} +
14
+ Dir["./**/*.rb"] +
15
+ Dir["./**/Rakefile"]
16
+
17
+ rdoc_exclude = %w{
18
+ test
19
+ contrib
20
+ install
21
+ quix
22
+ fork
23
+ diagnostic
24
+ algorithm
25
+ bucket
26
+ comp_tree\.rb
27
+ }
28
+ t.has_rdoc = true
29
+ t.extra_rdoc_files = %w{README}
30
+ t.rdoc_options += [
31
+ "--main",
32
+ "README",
33
+ "--title",
34
+ "comp_tree: #{t.summary}",
35
+ ] + rdoc_exclude.inject(Array.new) { |acc, pattern|
36
+ acc + ["--exclude", pattern]
37
+ }
38
+ }
@@ -0,0 +1,16 @@
1
+ $LOAD_PATH.unshift "./lib"
2
+
3
+ require 'quix/simple_installer'
4
+ require 'quix/config'
5
+
6
+ task :test do
7
+ load './test/all.rb'
8
+ end
9
+
10
+ task :install do
11
+ Quix::SimpleInstaller.new.install
12
+ end
13
+
14
+ task :uninstall do
15
+ Quix::SimpleInstaller.new.uninstall
16
+ end
@@ -0,0 +1,3 @@
1
+ $LOAD_PATH.unshift "./lib"
2
+ require 'quix/simple_installer'
3
+ Quix::SimpleInstaller.new.run
@@ -0,0 +1,7 @@
1
+
2
+ class << Dir
3
+ remove_method :[]
4
+ def [](pattern)
5
+ Dir.glob(pattern, File::FNM_CASEFOLD)
6
+ end
7
+ end
@@ -0,0 +1,9 @@
1
+
2
+ unless respond_to? :tap
3
+ module Kernel
4
+ def tap
5
+ yield self
6
+ self
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,21 @@
1
+
2
+ if $DEBUG and !(defined?($NO_DEBUG_INCLUDE) and $NO_DEBUG_INCLUDE)
3
+ class Module
4
+ orig_include = instance_method(:include)
5
+ remove_method(:include)
6
+ define_method(:include) { |*mods|
7
+ mods.each { |mod|
8
+ if mod.class == Module
9
+ mod.instance_methods(true).each { |name|
10
+ if self.instance_methods(true).include?(name)
11
+ STDERR.puts("Note: replacing #{self.inspect}##{name} " +
12
+ "with #{mod.inspect}##{name}")
13
+ end
14
+ }
15
+ end
16
+ orig_include.bind(self).call(*mods)
17
+ }
18
+ }
19
+ end
20
+ end
21
+
@@ -0,0 +1,41 @@
1
+
2
+ class Module
3
+ alias_method :private__original, :private
4
+ def private(*args, &block)
5
+ private__original(*args)
6
+ if block
7
+ singleton_class = (class << self ; self ; end)
8
+ caller_self = block.binding.eval("self")
9
+ method_added__original =
10
+ if (t = method(:method_added)) and t.owner == singleton_class
11
+ t
12
+ else
13
+ nil
14
+ end
15
+ begin
16
+ singleton_class.instance_eval {
17
+ define_method(:method_added) { |name|
18
+ caller_self.instance_eval {
19
+ private__original(name.to_sym)
20
+ }
21
+ if t = method_added__original
22
+ t.call(name)
23
+ end
24
+ }
25
+ }
26
+ block.call
27
+ ensure
28
+ if t = method_added__original
29
+ t.owner.instance_eval {
30
+ define_method(:method_added, t)
31
+ }
32
+ else
33
+ singleton_class.instance_eval {
34
+ remove_method(:method_added)
35
+ }
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+
@@ -0,0 +1,37 @@
1
+
2
+ require 'rbconfig'
3
+
4
+ module Quix
5
+ module Config
6
+ CONFIG = ::Config::CONFIG
7
+
8
+ def ruby_executable
9
+ File.join(CONFIG["bindir"], CONFIG["RUBY_INSTALL_NAME"])
10
+ end
11
+
12
+ def version_gt(version) ; version_compare( :>, version) ; end
13
+ def version_lt(version) ; version_compare( :<, version) ; end
14
+ def version_eq(version) ; version_compare(:==, version) ; end
15
+ def version_ge(version) ; version_compare(:>=, version) ; end
16
+ def version_le(version) ; version_compare(:<=, version) ; end
17
+ def version_ne(version) ; version_compare(:"!=", version) ; end
18
+
19
+ def version_compare(op, version)
20
+ major, minor, teeny =
21
+ version.split(".").map { |n| n.to_i }
22
+
23
+ this_major, this_minor, this_teeny =
24
+ %w(MAJOR MINOR TEENY).map { |v| CONFIG[v].to_i }
25
+
26
+ if this_major == major and this_minor == minor
27
+ this_teeny.send(op, teeny)
28
+ elsif this_major == major
29
+ this_minor.send(op, minor)
30
+ else
31
+ this_major.send(op, major)
32
+ end
33
+ end
34
+
35
+ extend self
36
+ end
37
+ end
@@ -0,0 +1,60 @@
1
+
2
+ unless RUBY_PLATFORM =~ %r!cygwin!
3
+ raise NotImplementedError, "cygwin-only module"
4
+ end
5
+
6
+ require 'fileutils'
7
+ require 'thread'
8
+
9
+ module Quix
10
+ module Cygwin
11
+ def run_batchfile(file, *args)
12
+ dos_pwd_env {
13
+ sh("cmd", "/c", dos_path(file), *args)
14
+ }
15
+ end
16
+
17
+ def normalize_path(path)
18
+ path.sub(%r!/+\Z!, "")
19
+ end
20
+
21
+ def unix2dos(string)
22
+ string.
23
+ gsub("\n", "\r\n").
24
+ gsub(%r!\r+!, "\r")
25
+ end
26
+
27
+ def dos_path(unix_path)
28
+ `cygpath -w #{normalize_path(unix_path)}`.chomp
29
+ end
30
+
31
+ def unix_path(dos_path)
32
+ escaped_path = dos_path.sub(%r!\\+\Z!, "").gsub("\\", "\\\\\\\\")
33
+ `cygpath #{escaped_path}`.chomp
34
+ end
35
+
36
+ def dos_pwd_env
37
+ Thread.exclusive {
38
+ orig = ENV["PWD"]
39
+ ENV["PWD"] = dos_path(Dir.pwd)
40
+ begin
41
+ yield
42
+ ensure
43
+ ENV["PWD"] = orig
44
+ end
45
+ }
46
+ end
47
+
48
+ def avoid_dll(file)
49
+ temp_file = file + ".avoiding-link"
50
+ FileUtils.mv(file, temp_file)
51
+ begin
52
+ yield
53
+ ensure
54
+ FileUtils.mv(temp_file, file)
55
+ end
56
+ end
57
+
58
+ extend self
59
+ end
60
+ end
@@ -0,0 +1,44 @@
1
+
2
+ require 'quix/builtin/kernel/tap'
3
+
4
+ module Quix
5
+ module Diagnostic
6
+ def show(desc = nil, stream = STDOUT, &block)
7
+ if desc
8
+ stream.puts(desc)
9
+ end
10
+ if block
11
+ expression = block.call
12
+ eval(expression, block.binding).tap { |result|
13
+ stream.printf("%-16s => %s\n", expression, result.inspect)
14
+ }
15
+ end
16
+ end
17
+
18
+ if $DEBUG
19
+ def debug
20
+ yield
21
+ end
22
+
23
+ def debugging?
24
+ true
25
+ end
26
+
27
+ def trace(desc = nil, &block)
28
+ if desc
29
+ show("#{desc}.".sub(%r!\.\.+\Z!, ""), STDERR, &block)
30
+ else
31
+ show(nil, STDERR, &block)
32
+ end
33
+ end
34
+ else
35
+ # non-$DEBUG
36
+ def debug ; end
37
+ def debugging? ; end
38
+ def trace(*args) ; end
39
+ end
40
+
41
+ extend self
42
+ end
43
+ end
44
+
@@ -0,0 +1,33 @@
1
+
2
+ require 'quix/builtin/kernel/tap'
3
+
4
+ module Quix
5
+ module Enumerable
6
+ def inject_with_index(*args)
7
+ index = 0
8
+ inject(*args) { |acc, elem|
9
+ yield(acc, elem, index).tap {
10
+ index += 1
11
+ }
12
+ }
13
+ end
14
+
15
+ def map_with_index
16
+ Array.new.tap { |result|
17
+ each_with_index { |elem, index|
18
+ result << yield(elem, index)
19
+ }
20
+ }
21
+ end
22
+
23
+ def select_with_index
24
+ Array.new.tap { |result|
25
+ each_with_index { |elem, index|
26
+ if yield(elem, index)
27
+ result << elem
28
+ end
29
+ }
30
+ }
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,37 @@
1
+
2
+ require 'tmpdir'
3
+ require 'quix/builtin/kernel/tap'
4
+
5
+ module Quix
6
+ module FileUtils
7
+ def rename_file(file, new_name)
8
+ #
9
+ # For case-insensitive systems, we must move the file elsewhere
10
+ # before changing case.
11
+ #
12
+ temp = File.join(Dir.tmpdir, File.basename(file))
13
+ ::FileUtils.mv(file, temp)
14
+ begin
15
+ ::FileUtils.mv(temp, new_name)
16
+ rescue
17
+ ::FileUtils.mv(temp, file)
18
+ raise
19
+ end
20
+ end
21
+
22
+ def replace_file(file)
23
+ old_contents = File.read(file)
24
+ yield(old_contents).tap { |new_contents|
25
+ File.open(file, "w") { |output|
26
+ output.print(new_contents)
27
+ }
28
+ }
29
+ end
30
+
31
+ def stem(file)
32
+ file.sub(%r!#{File.extname(file)}\Z!, "")
33
+ end
34
+
35
+ extend self
36
+ end
37
+ end
@@ -0,0 +1,27 @@
1
+
2
+ require 'quix/builtin/kernel/tap'
3
+ require 'ostruct'
4
+
5
+ module Quix
6
+ class HashStruct < OpenStruct
7
+ def method_missing(sym, *args, &block)
8
+ if table.respond_to? sym
9
+ table.send(sym, *args, &block)
10
+ else
11
+ super
12
+ end
13
+ end
14
+
15
+ class << self
16
+ def recursive_new(hash)
17
+ new.tap { |s|
18
+ hash.each_pair { |key, value|
19
+ s.send(
20
+ :"#{key}=",
21
+ value.is_a?(Hash) ? recursive_new(value) : value)
22
+ }
23
+ }
24
+ end
25
+ end
26
+ end
27
+ end