Ruby + Type = Rubype
# Assert class of both args is Numeric and class of return is String
def sum(x, y)
(x + y).to_s
end
typesig :sum, [Numeric, Numeric] => String
# Assert first arg has method #to_i
def sum(x, y)
x.to_i + y
end
typesig :sum, [:to_i, Numeric] => Numeric
This gem brings you advantage of type without changing existing code's behavior.
Good point:
- Meaningful error
- Executable documentation
- Don't need to check type of method's arguments and return.
- Type info itself is object, you can check it and even change it during run time.
Bad point:
- Checking type run every time method call... it might be overhead, but it's not big deal.
- There is no static analysis.
Feature
Advantage of type
- Meaningful error
- Executable documentation
- Don't need to check type of method's arguments and return .
require 'rubype'
# ex1: Assert class of args and return
class MyClass
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
MyClass.new.sum(1, 2)
#=> 3
MyClass.new.sum(1, 'string')
#=> Rubype::ArgumentTypeError: Expected MyClass#sum's 2nd argument to be Numeric but got "string" instead
MyClass.new.wrong_sum(1, 2)
#=> Rubype::ReturnTypeError: Expected MyClass#wrong_sum to return Numeric but got "string" instead
# ex2: Assert object has specified method
class MyClass
def sum(x, y)
x.to_i + y
end
typesig :sum, [:to_i, Numeric] => Numeric
end
MyClass.new.sum('1', 2)
#=> 3
MyClass.new.sum(:has_no_to_i, 2)
#=> Rubype::ArgumentTypeError: Expected MyClass#sum's 1st argument to have method #to_i but got :has_no_to_i instead
# ex3: You can use Any class, if you want
class People
def marry(people)
# Your Ruby code as usual
end
typesig :marry, [People] => Any
end
People.new.marry(People.new)
#=> no error
People.new.marry('non people')
#=> Rubype::ArgumentTypeError: Expected People#marry's 1st argument to be People but got "non people" instead
Typed method can coexist with non-typed method
# It's totally OK!!
class MyClass
def method_with_type(x, y)
x + y
end
typesig :method_with_type, [Numeric, Numeric] => Numeric
def method_without_type(x, y)
'string'
end
end
Duck typing
You can use Any
class.
class MyClass
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!!
MyClass.new.foo(1)
# It's totally OK!!
MyClass.new.foo(:sym)
# It's totally OK!!
MyClass.new.sum(1, 2)
# It's totally OK!!
MyClass.new.sum('1', 2)
Check type info everywhere!
class MyClass
def sum(x, y)
x.to_i + y
end
typesig :sum, [:to_i, Numeric] => Numeric
end
MyClass.new.method(:sum).type_info
# => [:to_i, Numeric] => Numeric
MyClass.new.method(:sum).arg_types
# => [:to_i, Numeric]
MyClass.new.method(:sum).return_type
# => Numeric
Benchmarks
require 'rubype'
require 'benchmark'
class RubypeCommonClass
def sum(x, y)
x + y
end
typesig :sum, [Numeric, Numeric] => Numeric
end
class CommonClass
def sum(x, y)
x + y
end
end
class RubypeDucktypeClass
def sum(x, y)
x.to_i + y
end
typesig :sum, [:to_i, Numeric] => Numeric
end
class DucktypeClass
def sum(x, y)
x.to_i + y
end
end
N = 100_000
Benchmark.bm("RubypeDucktypeClass".length + 3) do |x|
x.report("RubypeCommonClass") { N.times { RubypeCommonClass.new.sum(1, 5) } }
x.report("CommonClass") { N.times { CommonClass.new.sum(1, 5) } }
end
Benchmark.bm("RubypeDucktypeClass".length + 3) do |x|
x.report("RubypeDucktypeClass") { N.times { RubypeDucktypeClass.new.sum(1, 5) } }
x.report("DucktypeClass") { N.times { DucktypeClass.new.sum(1, 5) } }
end
Results
Ruby 2.2.0, Macbook Pro 2.9Ghz Intel Core i7, 8GB RAM
user system total real
RubypeCommonClass 0.530000 0.010000 0.540000 ( 0.566493)
CommonClass 0.030000 0.000000 0.030000 ( 0.035718)
user system total real
RubypeDucktypeClass 0.590000 0.010000 0.600000 ( 0.682504)
DucktypeClass 0.030000 0.000000 0.030000 ( 0.029856)
Installation
gem install rubype or add gem 'rubype' to your Gemfile.
And require 'rubype'
, enjoy typed Ruby.
This gem requires Ruby 2.0.0+.
Contributing
Fork it ( https://github.com/[my-github-username]/rubype/fork )
Create your feature branch (git checkout -b my-new-feature
)
$ bundle install --path vendor/bundle
Commit your changes (git commit -am 'Add some feature'
)
$ bundle exec rake test
......
Finished in 0.010961s, 547.3953 runs/s, 5017.7903 assertions/s.
7 runs, 61 assertions, 0 failures, 0 errors, 0 skips
Push to the branch (git push origin my-new-feature
)
Create a new Pull Request to develop
branch
Credits
@chancancode and This article first brought this to my attention. I've stolen some idea from them.