vissen-parameterized 0.1.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.
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vissen
4
+ module Parameterized
5
+ # The scope exists to protect parameterized objects from being bound to
6
+ # parameters with a different lifetime than their own.
7
+ #
8
+ # Each scope is bound to a `Conditional` and as long as it, as well as the
9
+ # conditionals of all the parents return false, the scope is considered to
10
+ # be alive.
11
+ #
12
+ # By checking that one scope is included in another, it is possible to
13
+ # guarantee that values belonging to the first scope are safe to use.
14
+ class Scope
15
+ # @return [Scope] the parent of this scope.
16
+ attr_reader :parent
17
+
18
+ # A scope is considered dead once its conditional returns true, or if the
19
+ # parent scope is also dead.
20
+ #
21
+ # @return [true] if the conditional is met.
22
+ # @return [false] otherwise.
23
+ def dead?
24
+ @conditional.met? || parent.dead?
25
+ end
26
+
27
+ # The inverse of `#dead?`
28
+ #
29
+ # @see dead?
30
+ #
31
+ # @return [false] if the conditional is met.
32
+ # @return [true] otherwise.
33
+ def alive?
34
+ !dead?
35
+ end
36
+
37
+ # Forces the conditional to return true, irregardless of its actual state.
38
+ #
39
+ # @return [nil]
40
+ def kill!
41
+ @conditional.force!
42
+ end
43
+
44
+ # Checks if the given object is included, either in this scope or in any
45
+ # of the parent scopes.
46
+ #
47
+ # @param obj [#scope] the object to scope check.
48
+ # @return [true] if the object either shares this scope or a parent scope.
49
+ # @return [false] otherwise.
50
+ def include?(obj)
51
+ include_scope? obj.scope
52
+ end
53
+
54
+ alias === include?
55
+
56
+ # Creates a new scope that is a direct descendent of this one.
57
+ #
58
+ # @param conditional [Conditional] the conditional to use for the new
59
+ # scope.
60
+ # @return [Scope] a new child scope.
61
+ def create_scope(conditional)
62
+ Scope.new self, conditional
63
+ end
64
+
65
+ # Checks if the given scope is included in the scope hierarchy of this
66
+ # one.
67
+ #
68
+ # @param other [Scope, Object] the scope to check.
69
+ # @return [true] if the given scope is equal to this one, or one of the
70
+ # parents.
71
+ def include_scope?(other)
72
+ equal?(other) || @parent.include_scope?(other)
73
+ end
74
+
75
+ protected
76
+
77
+ # Creates a new scope. This method is protected to avoid erroneous parent
78
+ # structures and new top level scopes should instead be created using
79
+ # `GlobalScope.instance.create_scope`.
80
+ #
81
+ # @raise [TypeError] if the conditional does not respond to `#met?`.
82
+ # @raise [ScopeError] if the conditional is out of scope.
83
+ def initialize(parent, conditional)
84
+ raise TypeError unless conditional.respond_to? :met?
85
+ @parent = parent
86
+
87
+ raise ScopeError unless include? conditional
88
+ @conditional = conditional
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vissen
4
+ module Parameterized
5
+ # The scope error signals that a parameter was bound out of scope.
6
+ class ScopeError < Error
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vissen
4
+ module Parameterized
5
+ module Value
6
+ # Bool values are stored as booleans internally.
7
+ #
8
+ # === Usage
9
+ #
10
+ # bool = Bool.new true
11
+ # bool.value # => true
12
+ #
13
+ class Bool
14
+ include Value
15
+
16
+ # @return [true, false] see Value
17
+ DEFAULT = false
18
+
19
+ # @param new_value [Object] the new value.
20
+ # @return see Value#write
21
+ def write(new_value)
22
+ super new_value ? true : false
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vissen
4
+ module Parameterized
5
+ module Value
6
+ # Int values are stored as Fixnums internally.
7
+ #
8
+ # === Usage
9
+ #
10
+ # int = Int.new 42
11
+ # int.value # => 42
12
+ #
13
+ class Int
14
+ include Value
15
+
16
+ # @return [Fixnum] see Value
17
+ DEFAULT = 0
18
+
19
+ # @raise [TypeError] if the given object cannot be coerced into an
20
+ # integer.
21
+ #
22
+ # @param new_value [#to_i] the new value.
23
+ # @return see Value#write
24
+ def write(new_value)
25
+ super Integer(new_value)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vissen
4
+ module Parameterized
5
+ module Value
6
+ # Real values are stored as floats internally.
7
+ #
8
+ # === Usage
9
+ #
10
+ # real = Real.new 42
11
+ # real.value # => 42.0
12
+ #
13
+ class Real
14
+ include Value
15
+
16
+ # @return [Float] see Value
17
+ DEFAULT = 0.0
18
+
19
+ # @raise [TypeError] if the given object cannot be coerced into a
20
+ # float.
21
+ #
22
+ # @param new_value [#to_f] the new value.
23
+ # @return see Value#write
24
+ def write(new_value)
25
+ super Float(new_value)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vissen
4
+ module Parameterized
5
+ module Value
6
+ # Vector type values are stored internally as arrays of floats.
7
+ #
8
+ # === Usage
9
+ #
10
+ # vec = Vec[1, 0.2]
11
+ # vec.value # => [1.0, 0.2]
12
+ #
13
+ class Vec
14
+ include Value
15
+
16
+ # @return [Array<Float>] the default value that will be used when `.new`
17
+ # is called without arguments, or with nil.
18
+ DEFAULT = [0.0, 0.0].freeze
19
+
20
+ # @param initial_value [Array<#to_f>] the initial value to use.
21
+ def initialize(initial_value = nil)
22
+ @value = DEFAULT.dup
23
+ taint!
24
+
25
+ write initial_value if initial_value
26
+ end
27
+
28
+ # @raise [TypeError] if the given value does not respond to `#[]`.
29
+ # @raise [TypeError] if the elements of the given value does not cannot
30
+ # be coerced into floats.
31
+ #
32
+ # @param new_value [Array<#to_f>] the new values to write.
33
+ # @return [nil]
34
+ def write(new_value)
35
+ return if @value == new_value
36
+
37
+ @value[0] = Float(new_value[0])
38
+ @value[1] = Float(new_value[1])
39
+ taint!
40
+ nil
41
+ rescue NoMethodError
42
+ raise TypeError, 'The given object must support #[]'
43
+ end
44
+
45
+ class << self
46
+ # @param a [#to_f] the first vector component.
47
+ # @param b [#to_f] the second vector component.
48
+ # @return [Vec] a new Vec instance.
49
+ def [](a, b)
50
+ new [a, b]
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vissen
4
+ module Parameterized
5
+ # The Value module implements the basic functionallity of a value type.
6
+ # Class implementations are encouraged to override #write to provide type
7
+ # checking or coercion.
8
+ #
9
+ # === Usage
10
+ # The following example implements an integer type by calling #to_i on
11
+ # objects before they are written.
12
+ #
13
+ # class Int
14
+ # include Value
15
+ # DEFAULT = 0
16
+ #
17
+ # def write(new_value)
18
+ # super new_value.to_i
19
+ # end
20
+ # end
21
+ #
22
+ module Value
23
+ # @return [Object] the internal value object.
24
+ attr_reader :value
25
+
26
+ # @return [Object] the default value that will be used when `.new` is
27
+ # called without arguments, or with nil.
28
+ DEFAULT = nil
29
+
30
+ # @param value [Object] the initial value to use. Ignored if nil.
31
+ def initialize(value = nil)
32
+ @value = nil
33
+ @tainted = true
34
+
35
+ write(value.nil? ? self.class::DEFAULT : value)
36
+ end
37
+
38
+ # Updates the internally stored value. The object will be marked as
39
+ # tainted if the new value differs from the previous.
40
+ #
41
+ # @param new_value [Object] the new value to write.
42
+ # @return [true] if the value was changed.
43
+ # @return [false] otherwise.
44
+ def write(new_value)
45
+ return false if new_value == @value
46
+ @value = new_value
47
+ taint!
48
+ true
49
+ end
50
+
51
+ # @return [true] if the value has been written to since the last call to
52
+ # `#untaint!`.
53
+ # @return [false] otherwise.
54
+ def tainted?
55
+ @tainted
56
+ end
57
+
58
+ # Marks the value as untainted.
59
+ #
60
+ # @return [false]
61
+ def untaint!
62
+ @tainted = false
63
+ end
64
+
65
+ # Values are always considered part of the global scope.
66
+ #
67
+ # @return [Scope] the scope of the value.
68
+ def scope
69
+ GlobalScope.instance
70
+ end
71
+
72
+ # @return [Array<Module>] an array of the modules and classes that include
73
+ # the `Value` module.
74
+ def self.types
75
+ @types
76
+ end
77
+
78
+ # Converts a class name to a string.
79
+ #
80
+ # Vissen::Parameterized::Value::Real -> "real"
81
+ #
82
+ # @param klass [Class] the class to canonicalize.
83
+ # @return [String] a string version of the class name.
84
+ def self.canonicalize(klass)
85
+ klass.name
86
+ .split('::').last
87
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
88
+ .downcase
89
+ end
90
+
91
+ # @return [String] the value formated as a string, with an appended '*' if
92
+ # the value is tainted.
93
+ def to_s
94
+ base = @value.to_s
95
+ tainted? ? base + '*' : base
96
+ end
97
+
98
+ protected
99
+
100
+ def taint!
101
+ @tainted = true
102
+ end
103
+
104
+ # @param mod [Module]
105
+ def self.included(mod)
106
+ (@types ||= []) << mod
107
+ end
108
+
109
+ private_class_method :included
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vissen
4
+ module Parameterized
5
+ # The version number of the Parameterized library.
6
+ #
7
+ # @return [String] a semantic version number.
8
+ VERSION = '0.1.0'
9
+ end
10
+ end
@@ -0,0 +1,201 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+ require 'singleton'
5
+
6
+ require 'vissen/parameterized/version'
7
+ require 'vissen/parameterized/error'
8
+ require 'vissen/parameterized/scope_error'
9
+ require 'vissen/parameterized/accessor'
10
+ require 'vissen/parameterized/scope'
11
+ require 'vissen/parameterized/global_scope'
12
+ require 'vissen/parameterized/value'
13
+ require 'vissen/parameterized/value/bool'
14
+ require 'vissen/parameterized/value/int'
15
+ require 'vissen/parameterized/value/real'
16
+ require 'vissen/parameterized/value/vec'
17
+ require 'vissen/parameterized/parameter'
18
+ require 'vissen/parameterized/dsl'
19
+ require 'vissen/parameterized/graph'
20
+
21
+ module Vissen
22
+ # A parameterized object should have
23
+ # - a set of parameters,
24
+ # - a (possibly expensive) function that transforms the parameters to an
25
+ # output, and
26
+ # - an output value.
27
+ module Parameterized
28
+ extend Forwardable
29
+
30
+ INSPECT_FORMAT = '#<%<name>s:0x%016<object_id>x (%<params>s) -> %<type>s>'
31
+ private_constant :INSPECT_FORMAT
32
+
33
+ # @!method value
34
+ # @return [Object] the output value.
35
+ def_delegators :@_value, :value, :to_s
36
+
37
+ # @!method returns_a?(value_klass)
38
+ # Checks if the parameterized object returns a value of the given value
39
+ # class.
40
+ #
41
+ # @param value_klass [Class] the class to test.
42
+ # @return [true] if the output value is of the given class.
43
+ # @return [false] otherwise.
44
+ def_delegator :@_value, :is_a?, :returns_a?
45
+
46
+ # Forwards all arguments to super.
47
+ #
48
+ # @param args [Array<Object>] the arguments to forward to super.
49
+ # @param parameters [Hash<Symbol, Parameter>] the input parameters.
50
+ # @param output [Value] the output value object.
51
+ # @param scope [Scope] the scope of the object.
52
+ # @param setup [Hash<Symbol, Object>] the initial setup.
53
+ def initialize(*args,
54
+ parameters:,
55
+ output:,
56
+ scope: GlobalScope.instance,
57
+ setup: {})
58
+ @_accessor = Accessor.new parameters
59
+ @_params = parameters
60
+ @_scope = scope
61
+ @_value = output
62
+ @_checked = false
63
+
64
+ load_initial setup
65
+
66
+ super(*args)
67
+ end
68
+
69
+ # @raise [NotImplementedError] if not implemented by descendent.
70
+ #
71
+ # @param _parameters [Accessor] the parameters of the parameterized object.
72
+ # @return [Object] an object compatible with the output value type should be
73
+ # returned.
74
+ def call(_parameters)
75
+ raise NotImplementedError
76
+ end
77
+
78
+ # Marks the output value and all input parameters as untainted.
79
+ #
80
+ # @return [false]
81
+ def untaint!
82
+ # ASUMPTION: if the value has not been taint checked
83
+ # there should be no untainted values in
84
+ # this part of the graph. This does not
85
+ # hold initially.
86
+ return unless @_checked
87
+ @_checked = false
88
+
89
+ @_params.each { |_, param| param.untaint! }
90
+ @_value.untaint!
91
+ end
92
+
93
+ # Checks if the output value of the parameterized object has changed. If any
94
+ # of the input parameters have changed since last calling `#untaint!` the
95
+ # `#call` method will be evaluated in order to determine the state of the
96
+ # output value.
97
+ #
98
+ # Note that `#call` is only evaluated once after the object has been
99
+ # untainted. Subsequent calls to `#tainted?` will refer to the result of the
100
+ # first operation.
101
+ #
102
+ # @return [true] if the output value has changed since last calling
103
+ # `#untaint!`.
104
+ # @return [false] otherwise.
105
+ def tainted?
106
+ return @_value.tainted? if @_checked
107
+ @_checked = true
108
+
109
+ params_tainted =
110
+ @_params.reduce(false) do |a, (_, param)|
111
+ param.tainted? || a
112
+ end
113
+
114
+ return false unless params_tainted
115
+
116
+ @_value.write call(@_accessor)
117
+ end
118
+
119
+ # @return [true] if the parameterized object has the given parameter.
120
+ # @return [false] otherwise.
121
+ def parameter?(key)
122
+ @_params.key? key
123
+ end
124
+
125
+ # Binds a parameter to a target value.
126
+ #
127
+ # @see Parameter#bind
128
+ # @raise [KeyError] if the parameter is not found.
129
+ # @raise [ScopeError] if the parameter is out of scope.
130
+ #
131
+ # @param param [Symbol] the parameter to bind.
132
+ # @param target [#value] the value object to bind to.
133
+ # @return [Parameter] the parameter that was bound.
134
+ def bind(param, target)
135
+ raise ScopeError unless scope.include? target
136
+ @_params.fetch(param).bind target
137
+ end
138
+
139
+ # Sets the constant value of a parameter.
140
+ #
141
+ # @see Parameter#set
142
+ # @raise [KeyError] if the parameter is not found.
143
+ #
144
+ # @param param [Symbol] the parameter to bind.
145
+ # @param value [Object] the value to set.
146
+ # @return [Parameter] the parameter that was set.
147
+ def set(param, value)
148
+ @_params.fetch(param).set value
149
+ end
150
+
151
+ # @return [Accessor] a proxy object that provides access to parameters via
152
+ # method calls instead of hash lookups.
153
+ def parameters
154
+ @_accessor
155
+ end
156
+
157
+ alias params parameters
158
+
159
+ # @return [Scope] the scope to which the parameterized object belongs.
160
+ def scope
161
+ @_scope
162
+ end
163
+
164
+ # Produces a readable string representation of the parameterized object.
165
+ #
166
+ # @return [String] a string representation.
167
+ def inspect
168
+ format INSPECT_FORMAT, name: self.class.name,
169
+ object_id: object_id,
170
+ params: params_with_types,
171
+ type: Value.canonicalize(@_value.class)
172
+ end
173
+
174
+ # Iterates over the parameterized objects currently bound to the parameters.
175
+ #
176
+ # @return [Enumerable] if no block is given.
177
+ def each_parameterized
178
+ return to_enum(__callee__) unless block_given?
179
+ @_params.each do |_, param|
180
+ next if param.constant?
181
+ target = param.target
182
+ yield target if target.is_a? Parameterized
183
+ end
184
+ end
185
+
186
+ private
187
+
188
+ def load_initial(setup)
189
+ setup.each do |key, value|
190
+ @_params.fetch(key)
191
+ .send(value.respond_to?(:value) ? :bind : :set, value)
192
+ end
193
+ end
194
+
195
+ def params_with_types
196
+ @_params.map { |k, v| "#{k}:#{Value.canonicalize(v.type)}" }.join(', ')
197
+ end
198
+ end
199
+ end
200
+
201
+ require 'vissen/parameterized/conditional'
@@ -0,0 +1,39 @@
1
+
2
+ # frozen_string_literal: true
3
+
4
+ lib = File.expand_path('lib', __dir__)
5
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
6
+ require 'vissen/parameterized/version'
7
+
8
+ Gem::Specification.new do |spec|
9
+ spec.name = 'vissen-parameterized'
10
+ spec.version = Vissen::Parameterized::VERSION
11
+ spec.authors = ['Sebastian Lindberg']
12
+ spec.email = ['seb.lindberg@gmail.com']
13
+
14
+ spec.summary = 'Parameterized creates a dependency graph for pure ' \
15
+ 'functions.'
16
+ spec.description = 'This utility library gives objects the ability to ' \
17
+ 'declare input dependencies, a transformation of ' \
18
+ 'those inputs and an output value. Forcing ' \
19
+ 'dependencies to be acyclic, the library can always ' \
20
+ 'find a valid update order of all the transformations.'
21
+ spec.homepage = 'https://github.com/midi-visualizer/vissen-parameterized'
22
+ spec.license = 'MIT'
23
+
24
+ spec.metadata['yard.run'] = 'yri'
25
+
26
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
27
+ f.match(%r{^(test|spec|features)/})
28
+ end
29
+ spec.bindir = 'exe'
30
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
31
+ spec.require_paths = ['lib']
32
+
33
+ spec.add_development_dependency 'bundler', '~> 1.16'
34
+ spec.add_development_dependency 'minitest', '~> 5.0'
35
+ spec.add_development_dependency 'rake', '~> 10.0'
36
+ spec.add_development_dependency 'rubocop', '~> 0.52'
37
+ spec.add_development_dependency 'simplecov', '~> 0.16'
38
+ spec.add_development_dependency 'yard', '~> 0.9'
39
+ end