vissen-parameterized 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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