Created
April 13, 2015 13:11
-
-
Save Whitespace/5d3150847540ef3ea779 to your computer and use it in GitHub Desktop.
Gradual Type Checking for Ruby
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# inspired by http://gogotanaka.github.io/rubype.github.io/ | |
module Types | |
def self.included(base) | |
class << base | |
def typesig method, signature_hash | |
arg_types, ret_type = signature_hash.to_a[0] | |
self.send(:alias_method, "__#{method}".to_sym, method) | |
define_method method do |*args| | |
failures = args.map.with_index do |arg, i| | |
case arg_types[i] | |
when Any | |
when Symbol | |
if !arg.respond_to?(arg_types[i]) | |
"Expected argument #{i} of #{self.class}\##{method} to have method \##{arg_types[i]} but got \"#{args[i]}\" instead" | |
end | |
when Class | |
if !arg.is_a?(arg_types[i]) | |
"Expected argument #{i} of #{self.class}\##{method} to be #{arg_types[i]} but got \"#{args[i].class}\" instead" | |
end | |
end | |
end.compact | |
unless failures.empty? | |
raise ArgumentTypeError, failures.join("\n") | |
else | |
ret_val = send("__#{method}", *args) | |
case ret_type | |
when Any || ret_type | |
ret_val | |
else | |
raise ReturnTypeError, "Expected #{self.class}\##{method} to return #{ret_type} but got \"#{ret_val.class}\" instead" | |
end | |
end | |
end | |
end | |
end | |
end | |
class Any | |
def self.===(klass) | |
true | |
end | |
end | |
class ArgumentTypeError < TypeError; end | |
class ReturnTypeError < TypeError; end | |
end | |
# ex1: Assert class of args and return | |
class MyClass | |
include Types | |
def sum(x, y) | |
x + y | |
end | |
typesig :sum, [Numeric, Numeric] => Numeric | |
def wrong_sum(x, y) | |
'string' | |
end | |
typesig :wrong_sum, [Numeric, Numeric] => Numeric | |
end | |
puts MyClass.new.sum(1, 2) | |
# puts MyClass.new.sum(1, 'string') | |
# puts MyClass.new.wrong_sum(1, 2) | |
# ex2: Assert object has specified method | |
class MyClass | |
include Types | |
def sum(x, y) | |
x.to_i + y | |
end | |
typesig :sum, [:to_i, Numeric] => Numeric | |
end | |
puts MyClass.new.sum('1', 2) | |
# puts MyClass.new.sum(:has_no_to_i, 2) | |
# ex3: You can use Any class, if you want | |
class People | |
include Types | |
def marry(people) | |
"I now thee wed" | |
end | |
typesig :marry, [People] => Any | |
end | |
puts People.new.marry(People.new) | |
class MyClass | |
include Types | |
def foo(any_obj) | |
1 | |
end | |
typesig :foo, [Any] => Numeric | |
def sum(x, y) | |
x.to_i + y | |
end | |
typesig :sum, [:to_i, Numeric] => Numeric | |
end | |
# It's totally OK!! | |
puts MyClass.new.foo(1) | |
# It's totally OK!! | |
puts MyClass.new.foo(:sym) | |
# It's totally OK!! | |
puts MyClass.new.sum(1, 2) | |
# It's totally OK!! | |
puts MyClass.new.sum('1', 2) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment