comp_tree 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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