typisch 0.1.5

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