typisch 0.1.5
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +55 -0
- data/lib/typisch/boolean.rb +13 -0
- data/lib/typisch/constructor.rb +112 -0
- data/lib/typisch/datetime.rb +41 -0
- data/lib/typisch/dsl.rb +242 -0
- data/lib/typisch/errors.rb +11 -0
- data/lib/typisch/meta.rb +57 -0
- data/lib/typisch/named_placeholder.rb +67 -0
- data/lib/typisch/null.rb +17 -0
- data/lib/typisch/numeric.rb +80 -0
- data/lib/typisch/object.rb +72 -0
- data/lib/typisch/poset_algorithms.rb +18 -0
- data/lib/typisch/registry.rb +146 -0
- data/lib/typisch/sequence.rb +107 -0
- data/lib/typisch/serialization.rb +80 -0
- data/lib/typisch/string.rb +69 -0
- data/lib/typisch/subtyping.rb +64 -0
- data/lib/typisch/tuple.rb +74 -0
- data/lib/typisch/type.rb +138 -0
- data/lib/typisch/type_checking.rb +12 -0
- data/lib/typisch/typed.rb +133 -0
- data/lib/typisch/union.rb +75 -0
- data/lib/typisch/version.rb +3 -0
- data/lib/typisch.rb +34 -0
- metadata +166 -0
@@ -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
|