typisch 0.1.5

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,80 @@
1
+ # TODO: have this work with whichever of these classes
2
+ # end up getting required, without having to require them
3
+ # upfront
4
+ require 'rational'
5
+ require 'bigdecimal'
6
+ require 'complex'
7
+
8
+ module Typisch
9
+ # This is aiming to be a nice numeric tower like those of Scheme etc:
10
+ # Integral < Rational < Real < Complex
11
+ #
12
+ # In these kinds of numeric tower, type and degree of precision is treated as a separate
13
+ # orthogonal concern; for now I've not treated precision at all here, although
14
+ # support could be added, eg to allow a distinction between
15
+ # - fixed precision binary floating point (Float)
16
+ # - arbitrary precision decimal floating point (BigDecimal)
17
+ # - fixed size integer (Fixnum)
18
+ # - arbitrary size integer (Bignum)
19
+ # - etc
20
+ # There are quite a few ways to classify numeric types, so I've stuck with just the
21
+ # most basic mathematical numeric tower classification for now.
22
+ class Type::Numeric < Type::Constructor
23
+
24
+ def initialize(type, *valid_implementation_classes)
25
+ @type = type
26
+ @valid_implementation_classes = valid_implementation_classes
27
+ end
28
+
29
+ attr_reader :valid_implementation_classes
30
+
31
+ # Note: these are based on how ruby 1.8.7 does it; 1.9 changes
32
+ # things slightly IIRC so may need tweaks to cope with this.
33
+ # Either way ruby's hierarchy of numeric types is slightly idiosyncratic:
34
+ complex = new('Complex', ::Numeric)
35
+ real = new('Real', ::Precision, ::BigDecimal, ::Rational)
36
+ rational = new('Rational', ::Rational, ::Integer)
37
+ integral = new('Integral', ::Integer)
38
+
39
+ Registry.register_global_type(:complex, complex)
40
+
41
+ Registry.register_global_type(:real, real)
42
+ Registry.register_global_type(:float, real) # aliasing this as :float too
43
+
44
+ Registry.register_global_type(:rational, rational)
45
+
46
+ Registry.register_global_type(:integral, integral)
47
+ Registry.register_global_type(:integer, integral) # aliasing this as :integer too
48
+
49
+ TOWER = [complex, real, rational, integral]
50
+
51
+ class << self
52
+ private :new
53
+
54
+ def top_type(*)
55
+ TOWER.first
56
+ end
57
+
58
+ def check_subtype(x, y)
59
+ x.index_in_tower >= y.index_in_tower
60
+ end
61
+ end
62
+
63
+ def to_s(*)
64
+ @name.inspect
65
+ end
66
+
67
+ def tag
68
+ @type
69
+ end
70
+
71
+ def index_in_tower
72
+ TOWER.index {|t| t.equal?(self)}
73
+ end
74
+
75
+ def shallow_check_type(instance)
76
+ case instance when *@valid_implementation_classes then true else false end
77
+ end
78
+ alias :check_type :shallow_check_type
79
+ end
80
+ end
@@ -0,0 +1,72 @@
1
+ class Typisch::Type
2
+ class Object < Constructor
3
+ class << self
4
+ def top_type(*)
5
+ new("Object")
6
+ end
7
+
8
+ def check_subtype(x, y, &recursively_check_subtype)
9
+ return false unless x.class_or_module <= y.class_or_module
10
+ y.property_names_to_types.all? do |y_propname, y_type|
11
+ x_type = x[y_propname] and recursively_check_subtype[x_type, y_type]
12
+ end
13
+ end
14
+ end
15
+
16
+ def initialize(tag, property_names_to_types={})
17
+ @tag = tag
18
+ raise ArgumentError, "expected String tag name for first argument" unless tag.is_a?(::String) && !tag.empty?
19
+ @property_names_to_types = property_names_to_types
20
+ end
21
+
22
+ attr_reader :tag, :property_names_to_types
23
+
24
+ def class_or_module
25
+ tag.split('::').inject(::Object) {|a,b| a.const_get(b)}
26
+ end
27
+
28
+ def property_names
29
+ @property_names_to_types.keys
30
+ end
31
+
32
+ def subexpression_types
33
+ @property_names_to_types.values
34
+ end
35
+
36
+ def [](property_name)
37
+ @property_names_to_types[property_name]
38
+ end
39
+
40
+ # For now, will only accept classes of object where the properties are available
41
+ # via attr_reader-style getter methods. TODO: maybe make allowances for objects
42
+ # which want to type-check via hash-style property access too.
43
+ def check_type(instance, &recursively_check_type)
44
+ instance.is_a?(class_or_module) &&
45
+ @property_names_to_types.all? do |prop_name, type|
46
+ instance.respond_to?(prop_name) &&
47
+ recursively_check_type[type, instance.send(prop_name)]
48
+ end
49
+ end
50
+
51
+ def shallow_check_type(instance)
52
+ instance.is_a?(class_or_module)
53
+ end
54
+
55
+ def to_string(depth, indent)
56
+ next_indent = "#{indent} "
57
+ pairs = @property_names_to_types.map {|n,t| "#{n.inspect} => #{t.to_s(depth+1, "#{indent} ")}"}
58
+ tag = @tag == "Object" ? '' : "#{@tag},"
59
+ "object(#{tag}\n#{next_indent}#{pairs.join(",\n#{next_indent}")}\n#{indent})"
60
+ end
61
+
62
+ def canonicalize!
63
+ @property_names_to_types.keys.each do |name|
64
+ @property_names_to_types[name] = @property_names_to_types[name].target
65
+ end
66
+ end
67
+
68
+ def property_annotations(property_name)
69
+ (annotations[:properties] ||= {})[property_name] ||= {}
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,18 @@
1
+ module Typisch
2
+
3
+ class << self
4
+ # Finds a minimal set of upper bounds amongst the given set of items
5
+ # from a partially-ordered set, which together cover the whole set.
6
+ #
7
+ # In the worst case this will just return the whole set.
8
+ def find_minimal_set_of_upper_bounds(*items)
9
+ result = []
10
+ items.each do |item|
11
+ next if result.any? {|other| item <= other}
12
+ result.delete_if {|other| other <= item}
13
+ result << item
14
+ end
15
+ result
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,146 @@
1
+ module Typisch
2
+ # A registry is a glorified hash lookup of types by name
3
+ #
4
+ # - provide a concise way of referring to more complex types
5
+ # - help with the wiring up of recursive types
6
+ # -
7
+ #
8
+ class Registry
9
+ attr_reader :types_by_name, :types_by_class, :types_by_class_and_version
10
+
11
+ def initialize(&block)
12
+ @types_by_name = GLOBALS.dup
13
+ @pending_canonicalization = {}
14
+ @types_by_class = {}
15
+ @types_by_class_and_version = {}
16
+ register(&block) if block
17
+ end
18
+
19
+ def [](name, version=nil)
20
+ name = :"#{name}" if name.is_a?(::Module)
21
+ name = :"#{name}__#{version}" if version
22
+ @types_by_name[name] ||= Type::NamedPlaceholder.new(name, self)
23
+ end
24
+
25
+ def register_type(name, type, &callback_on_canonicalization)
26
+ case @types_by_name[name]
27
+ when Type::NamedPlaceholder
28
+ @types_by_name[name].send(:target=, type)
29
+ when NilClass
30
+ else
31
+ raise Error, "type already registered with name #{name.inspect}"
32
+ end
33
+ type.send(:name=, name) unless type.name
34
+ @types_by_name[name] = type
35
+ @pending_canonicalization[name] = [type, callback_on_canonicalization]
36
+ end
37
+ alias :[]= :register_type
38
+
39
+ # While loading, we'll register various types in this hash of types
40
+ # (boolean, string, ...) which we want to be included in all registries
41
+ GLOBALS = {}
42
+ def self.register_global_type(name, type)
43
+ type.send(:name=, name) unless type.name
44
+ GLOBALS[name] = type
45
+ end
46
+
47
+ # All registering of types in a registry needs to be done inside one of these
48
+ # blocks; it ensures that the any forward references or cyclic references are
49
+ # resolved (via canonicalize!-ing every type in the type graph) once you've
50
+ # finished registering types.
51
+ #
52
+ # This also ensures that any uses of recursion are valid / well-founded, and
53
+ # does any other necessary validation of the type graph you've declared which
54
+ # isn't possible to do upfront.
55
+ #
56
+ # You can nest register blocks without ill-effect; it will only try to
57
+ # resolve forward references etc once the outermost block has exited.
58
+ #
59
+ # Note, this is all very much non-threadsafe, wouldn't be hard to make it so
60
+ # (probably just slap a big mutex around it) but not sure why exactly you'd
61
+ # want multi-threaded type registration anyway to anyway so leaving as-is for now.
62
+ def register(&block)
63
+ if @registering_types
64
+ DSLContext.new(self).instance_eval(&block)
65
+ else
66
+ start_registering_types!
67
+ DSLContext.new(self).instance_eval(&block)
68
+ stop_registering_types!
69
+ end
70
+ end
71
+
72
+ def start_registering_types!
73
+ @registering_types = true
74
+ end
75
+
76
+ def stop_registering_types!
77
+ @registering_types = false
78
+
79
+ types = @pending_canonicalization.values.map {|t,c| t}
80
+ each_type_in_graph(*types) {|t| t.canonicalize!}
81
+ @pending_canonicalization.each {|name,(type,callback)| callback.call if callback}
82
+ @pending_canonicalization = {}
83
+ end
84
+
85
+ def each_type_in_graph(*types)
86
+ seen_so_far = {}
87
+ while (type = types.pop)
88
+ next if seen_so_far[type]
89
+ seen_so_far[type] = true
90
+ yield type
91
+ types.push(*type.subexpression_types)
92
+ end
93
+ end
94
+
95
+ # Allow you to dup and merge registries
96
+
97
+ def initialize_copy(other)
98
+ @types_by_name = @types_by_name.dup
99
+ end
100
+
101
+ def merge(other)
102
+ dup.merge!(other)
103
+ end
104
+
105
+ def merge!(other)
106
+ @types_by_name.merge!(other.types_by_name)
107
+ end
108
+
109
+ def to_s
110
+ pairs = @types_by_name.map do |n,t|
111
+ next if GLOBALS[n]
112
+ "r.register #{n.inspect}, #{t.to_s(0, ' ')}"
113
+ end.compact
114
+ "Typisch::Registry.new do |r|\n #{pairs.join("\n ")}\nend"
115
+ end
116
+
117
+ def register_type_for_class(klass, type)
118
+ @types_by_class[klass] = type
119
+ end
120
+
121
+ def register_version_type_for_class(klass, version, type)
122
+ @types_by_class_and_version[[klass, version]] = type
123
+ end
124
+ end
125
+
126
+ # We set up a global registry which you can use if you like, either
127
+ # via Typisch.global_registry or via the convenience aliases
128
+ # Typisch.[] and Typisch.register.
129
+ #
130
+ # Or, you can make your own registry if you don't want to share a
131
+ # global registry with other code using this library. (recommended
132
+ # if writing modular code / library code which uses this).
133
+
134
+ def self.global_registry
135
+ @global_registry ||= Registry.new
136
+ end
137
+
138
+ def self.register(&block)
139
+ global_registry.register(&block)
140
+ end
141
+
142
+ def self.[](name)
143
+ global_registry[name]
144
+ end
145
+
146
+ end
@@ -0,0 +1,107 @@
1
+ class Typisch::Type
2
+ # A Sequence is an ordered collection of items, all of a given type.
3
+ #
4
+ # For now if you want an unordered collection, you just have to treat it
5
+ # as an ordered collection with arbitrary order; if you want a map/hash you
6
+ # just treat it as a sequence of tuples. TODO: would be nice to
7
+ # have more of a hierarchy of collection types here, eg OrderedSequence < Set.
8
+ #
9
+ #
10
+ # (ordered) Sequences support 'slice types', which are a kind of structural
11
+ # supertype for sequences. Their use is primarily in specifying partial
12
+ # serializations or partial type-checking for large sequences.
13
+ #
14
+ # Eg sequence(:integer, :slice => 0...10)
15
+ #
16
+ # This is saying: "A sequence of ints, which may be of any known length, but
17
+ # where I only care (to validate, serialize, ...) at most the first 10 items".
18
+ #
19
+ # Eg sequence(:integer, :slice => 0...10, :total_length => false)
20
+ #
21
+ # This is saying: "A sequence of ints, which may be of any known or unknown length,
22
+ # but where I only care about (validating, serializing, ...) at most the first 10 items,
23
+ # and I don't care about (validating, serializing...) the total length of the collection
24
+ class Sequence < Constructor
25
+ class << self
26
+ def top_type(overall_top)
27
+ new(overall_top, :slice => (0...0), :total_length => false)
28
+ end
29
+
30
+ def check_subtype(x, y, &recursively_check_subtype)
31
+ recursively_check_subtype[x.type, y.type] && (
32
+ !x.slice ||
33
+ (y.slice && (
34
+ x.slice.begin <= y.slice.begin &&
35
+ x.slice.end >= y.slice.end &&
36
+ (x.total_length || !y.total_length)
37
+ ))
38
+ )
39
+ end
40
+ end
41
+
42
+ def initialize(type, options={})
43
+ @type = type
44
+ if options[:slice]
45
+ @slice = options[:slice]
46
+ @slice = (@slice.begin...@slice.end+1) unless @slice.exclude_end?
47
+ @total_length = options[:total_length] != false
48
+ end
49
+ end
50
+
51
+ attr_reader :slice, :total_length
52
+
53
+ def with_options(options)
54
+ self.class.new(@type, {:slice => @slice, :total_length => @total_length}.merge!(options))
55
+ end
56
+
57
+ def subexpression_types
58
+ [@type]
59
+ end
60
+
61
+ def check_type(instance, &recursively_check_type)
62
+ shallow_check_type(instance) && if @slice
63
+ (instance[@slice] || []).all? {|i| recursively_check_type[@type, i]} &&
64
+ (!@total_length || ::Integer === instance.length)
65
+ else
66
+ instance.all? {|i| recursively_check_type[@type, i]}
67
+ end
68
+ end
69
+
70
+ # I tried allowing any Enumerable, but this resulted in allowing String and a bunch
71
+ # of other things which sort of expose a vaguely-array-like interface but not really
72
+ # in a way that's helpful for typing purposes. E.g. String in 1.8.7 exposes Enumerable
73
+ # over its *lines*, but an array-like interface over its *characters*, sometimes as
74
+ # strings, sometimes as ascii char codes. So not consistent at all.
75
+ #
76
+ # Any other classes added here must expose Enumerable, but also .length and slices
77
+ # via [] (at least if you want them to work with slice types).
78
+ #
79
+ # For now allowing Hashes too so they can be typed as a sequence of tuples, although should
80
+ # really only be typed as a set of tuples as there's no ordering or support for slices.
81
+ VALID_IMPLEMENTATION_CLASSES = [::Array, ::Hash]
82
+
83
+ def shallow_check_type(instance)
84
+ case instance when *VALID_IMPLEMENTATION_CLASSES then true else false end
85
+ end
86
+
87
+ def tag
88
+ "Sequence"
89
+ end
90
+
91
+ attr_reader :type
92
+
93
+ def to_string(depth, indent)
94
+ result = "sequence(#{@type.to_s(depth+1, indent)}"
95
+ if @slice
96
+ result << ", :slice => #{@slice}"
97
+ result << ", :total_length => false" unless @total_length
98
+ end
99
+ result << ")"
100
+ end
101
+
102
+ def canonicalize!
103
+ @type = @type.target
104
+ end
105
+ end
106
+
107
+ end
@@ -0,0 +1,80 @@
1
+ require 'json'
2
+
3
+ module Typisch
4
+ class JSONSerializer
5
+ def initialize(type, options={})
6
+ @type = type
7
+ @options = {}
8
+ @type_tag_key = (options[:type_tag_key] || '__class__').freeze
9
+ @class_to_type_tag = options[:class_to_type_tag]
10
+ @type_tag_to_class = options[:type_tag_to_class] || (@class_to_type_tag && @class_to_type_tag.invert)
11
+ end
12
+
13
+ def class_to_type_tag(klass)
14
+ @class_to_type_tag ? @class_to_type_tag[klass] : klass.to_s
15
+ end
16
+
17
+ def serialize(value)
18
+ serialize_to_jsonable(value).to_json
19
+ end
20
+
21
+ def serialize_already_encountered_pair(value, type, existing_serialization)
22
+ raise SerializationError, "cyclic object / type graph when serializing"
23
+ end
24
+
25
+ # http://work.tinou.com/2009/06/the-expression-problem-and-other-mysteries-of-life.html
26
+ def serialize_to_jsonable(value, type=@type, existing_serializations={})
27
+ existing = existing_serializations[[type, value]]
28
+ return serialize_already_encountered_pair(value, type, existing) if existing
29
+
30
+ result = case type
31
+ when Type::Date
32
+ value.to_s
33
+
34
+ when Type::Time
35
+ value.iso8601
36
+
37
+ when Type::Sequence
38
+ if type.slice
39
+ slice = value[type.slice]
40
+ existing_serializations[[type, value]] = result = {
41
+ @type_tag_key => class_to_type_tag(value.class),
42
+ 'range_start' => type.slice.begin
43
+ }
44
+ result['items'] = slice.map {|v| serialize_to_jsonable(v, type.type, existing_serializations)} if slice
45
+ result['total_items'] = value.length if type.total_length
46
+ result
47
+ else
48
+ result = existing_serializations[[type, value]] = []
49
+ value.each {|v| result << serialize_to_jsonable(v, type.type, existing_serializations)}
50
+ result
51
+ end
52
+
53
+ when Type::Tuple
54
+ result = existing_serializations[[type, value]] = []
55
+ type.types.zip(value).each {|t,v| result << serialize_to_jsonable(v,t,existing_serializations)}
56
+ result
57
+
58
+ when Type::Object
59
+ result = existing_serializations[[type, value]] = {@type_tag_key => class_to_type_tag(value.class)}
60
+ type.property_names_to_types.each do |prop_name, type|
61
+ result[prop_name.to_s] = serialize_to_jsonable(value.send(prop_name), type, existing_serializations)
62
+ end
63
+ result
64
+
65
+ when Type::Union
66
+ type = type.alternative_types.find {|t| t.shallow_check_type(value)}
67
+ raise SerializationError, "No types in union #{type} matched #{value.inspect}, could not serialize" unless type
68
+ serialize_to_jsonable(value, type, existing_serializations)
69
+
70
+ when Type::Constructor # Numeric, Null, String, Boolean etc
71
+ value
72
+
73
+ else
74
+ raise SerializationError, "Type #{type} not supported for serialization of #{value.inspect}"
75
+ end
76
+
77
+ result
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,69 @@
1
+ module Typisch
2
+ # String types support refinement types, specifying a set of allowed values,
3
+ # or a maximum length.
4
+ #
5
+ # About ruby Symbols: these are a pain in the arse.
6
+ # For now I'm allowing them to type-check interchangably with Strings.
7
+ # Since Typisch isn't specifically designed for Ruby's quirks but for more general
8
+ # data interchange, I don't think Symbol should have a special priviledged type
9
+ # of its own.
10
+ #
11
+ # Nevertheless if we ever allow custom type tags on String types (as we do for
12
+ # Object types at the moment) we could perhaps allow Symbol as a specially-tagged psuedo
13
+ # string like type. Although it's not a subclass of String, so hmm.
14
+ class Type::String < Type::Constructor
15
+ class << self
16
+ def tag
17
+ "String"
18
+ end
19
+
20
+ def top_type(*)
21
+ @top_type ||= new
22
+ end
23
+
24
+ def check_subtype(x, y)
25
+ x.equal?(y) || (
26
+ (x.max_length || Infinity) <= (y.max_length || Infinity) &&
27
+ (!y.values || (x.values && x.values.subset?(y.values)))
28
+ )
29
+ end
30
+ end
31
+
32
+ def initialize(refinements={})
33
+ @refinements = refinements
34
+ if @refinements[:values] && !@refinements[:values].is_a?(::Set)
35
+ @refinements[:values] = ::Set.new(refinements[:values])
36
+ end
37
+ end
38
+
39
+ Infinity = 1.0/0
40
+
41
+ def max_length
42
+ @refinements[:max_length]
43
+ end
44
+
45
+ def values
46
+ @refinements[:values]
47
+ end
48
+
49
+ def tag
50
+ self.class.tag
51
+ end
52
+
53
+ def to_s(*)
54
+ @name ? @name.inspect : "string(#{@refinements.inspect})"
55
+ end
56
+
57
+ def self.tag
58
+ "String"
59
+ end
60
+
61
+ def shallow_check_type(instance)
62
+ (::String === instance || ::Symbol === instance) &&
63
+ (!values || values.include?(instance.to_s)) &&
64
+ (!max_length || instance.to_s.length <= max_length)
65
+ end
66
+
67
+ Registry.register_global_type(:string, top_type)
68
+ end
69
+ end
@@ -0,0 +1,64 @@
1
+ class Typisch::Type
2
+ class << self
3
+ # The core of the subtyping algorithm, which copes with equi-recursive types.
4
+ #
5
+ # Actually quite simple on the face of it -- or at least, short.
6
+ #
7
+ # The crucial thing is that we allow a goal to be assumed without proof during
8
+ # the proving of its subgoals. Since (because of the potentially recursive nature
9
+ # of types) those subgoals may refer to types from the parent goal, we could otherwise
10
+ # run into infinite loops.
11
+ #
12
+ # How's that justified from a logic perspective? well, what we're doing is,
13
+ # we're just checking that the given subtyping judgement *isn't* provably
14
+ # *false* under the inference rules at hand. This allows a maximal consistent
15
+ # set of subtyping judgements to be made.
16
+ #
17
+ # Its dual, requiring that the judgement *is* provably *true* under the inference
18
+ # rules, would only allow a minimal set to be proven, and could get stuck
19
+ # searching forever for a proof of those judgements which are neither provably false
20
+ # nor provably true (namely, the awkward recursive ones).
21
+ #
22
+ # See Pierce on equi-recursive types and subtyping for the theory:
23
+ # http://www.cis.upenn.edu/~bcpierce/tapl/, it's an application of
24
+ # http://en.wikipedia.org/wiki/Knaster–Tarski_theorem to show that this
25
+ # is a least fixed point with respect to the adding of extra inferences
26
+ # to a set of subtyping judgements, if you note that those inference rules
27
+ # are monotonic. 'Corecursion' / 'coinduction' are also terms for what's
28
+ # going on here.
29
+ #
30
+ # TODO: for best performance, should we be going depth-first or breadth-first here?
31
+ #
32
+ # Also TODO: when subtype? succeeds (returns true), we can safely save the resulting
33
+ # set of judgements that were shown to be consistent, for use during future calls to
34
+ # subtype?. Memoization essentially.
35
+ def subtype?(x, y, may_assume_proven = {}, depth=0)
36
+ return true if may_assume_proven[[x,y]]
37
+ may_assume_proven[[x,y]] = true
38
+
39
+ result = check_subtype(x, y) do |u,v|
40
+ subtype?(u, v, may_assume_proven, depth+1)
41
+ end
42
+
43
+ result
44
+ end
45
+
46
+ private
47
+ def check_subtype(x, y, &recursively_check_subtype)
48
+ # Types are either union types, or constructor types. We deal with the unions first.
49
+ if Union === x
50
+ x.alternative_types.all? {|t| recursively_check_subtype[t, y]}
51
+ elsif Union === y
52
+ y.alternative_types.any? {|t| recursively_check_subtype[x, t]}
53
+ elsif x.type_lattice == y.type_lattice
54
+ # Hand over to that specific type_lattice in which both these Type::Constructor types
55
+ # live, in order to check subtyping goals which are specific to this lattice.
56
+ x.type_lattice.check_subtype(x, y, &recursively_check_subtype)
57
+ else
58
+ # Different Type::Constructor lattices are non-overlapping so we stop unless they're
59
+ # the same:
60
+ false
61
+ end
62
+ end
63
+ end
64
+ end