TypeProf
demo cases
A simple demo with a "User" class
def (user)
"The name is " + user.name
end
def type_error_demo(user)
"The age is " + user.age
end
user = User.new(name: "John", age: 20)
(user)
type_error_demo(user)
class User
attr_reader name: String
attr_reader age: Integer
def initialize: (name: String, age: Integer) -> void
end
Result:
$ typeprof test.rb test.rbs
# Errors
test.rb:6: [error] failed to resolve overload: String#+
# Classes
class Object
def : (User) -> String
def type_error_demo : (User) -> untyped
end
You can try this analysis online.
A simple demo to generate the signature prototype of "User" class
class User
def initialize(name:, age:)
@name, @age = name, age
end
attr_reader :name, :age
end
# A test case to tell TypeProf what types are expected by the class and methods
User.new(name: "John", age: 20)
Result:
$ typeprof -v test.rb
# Classes
class User
attr_reader name : String
attr_reader age : Integer
def initialize : (name: String, age: Integer) -> [String, Integer]
end
Type inspection by p
(Kernel#p
)
p 42 #=> Integer
p "str" #=> String
p "str".chars #=> Array[String]
Result:
$ typeprof test.rb
# Revealed types
# test.rb:1 #=> Integer
# test.rb:2 #=> String
# test.rb:3 #=> Array[String]
Block with builtin methods
# TypeProf runs this block only once
10000000000000.times do |n|
p n #=> Integer
end
# "each" with Heterogeneous array yields a union type
[1, 1.0, "str"].each do |e|
p e #=> Float | Integer | String
end
# You can use the idiom `&:method_name` too
p [1, 1.0, "str"].map(&:to_s) #=> Array[String]
User-defined blocks
def foo(n)
yield n.to_s
end
foo(42) do |n|
p n #=> String
nil
end
Result:
$ typeprof test.rb
# Revealed types
# test.rb:6 #=> String
# Classes
class Object
def foo : (Integer) { (String) -> nil } -> nil
end
Arrays
# A fixed-length array literal generates a "tuple" array
ary = [1, 1.0]
# A tuple array keeps its length, and the association between indexes and elements
p ary #=> [Integer, Float]
p ary[0] #=> Integer
p ary[1] #=> Float
# Destructive operation is well handled (in method-local analysis)
ary[0] = "str"
p ary #=> [String, Float]
# An calculated array generates a "sequence" array
ary = [1] + [1.0]
# A sequence array does not keep length nor association
p ary #=> Array[Float | Integer]
p ary[0] #=> Float | Integer
# Destructive operation is still handled (but "weak update" is applied)
ary[0] = "str"
p ary #=> Array[Float | Integer | String]
Multiple return values by using a tuple array
def foo
return 42, "str"
end
int, str = foo
p int #=> Integer
p str #=> String
Delegation by using a tuple array
def foo(x, y, z)
end
def proxy(dummy, *args)
foo(*args)
end
proxy(:dummy, 1, 1.0, "str")
Symbols
# Symbols are handled as concrete values instead of abstract ones
p [:a, :b, :c] #=> [:a, :b, :c]
Hashes
# A Hash is a "type-to-type" map
h = { "int" => 1, "float" => 1.0 }
p h #=> {String=>Float | Integer}
p h["int"] #=> Float | Integer
# Symbol-key hashes (a.k.a. records) can have distinct types for each key as Symbols are concrete
h = { int: 1, float: 1.0 }
p h #=> {:int=>Integer, :float=>Float}
p h[:int] #=> Integer
p h[:float] #=> Float
# Symbol-key hash can be appropriately passed to a keyword method
def foo(int:, float:)
p [int, float] #=> [Integer, Float]
end
foo(**h)
Structs
FooBar = Struct.new(:foo, : )
obj = FooBar.new(42)
obj.foo = :dummy
obj. = "str"
Result:
$ typeprof test.rb
# Classes
class FooBar < Struct
attr_accessor foo() : :dummy | Integer
attr_accessor () : String?
end
Exceptions
# TypeProf assumes that any exception may be raised anywhere
def foo
x = 1
x = "str"
x = :sym
ensure
p(x) #=> :sym | Integer | String
end
Result:
$ typeprof test.rb
# Revealed types
# test.rb:6 #=> :sym | Integer | String
# Classes
class Object
def foo : -> :sym
end
RBS overloaded methods
# TypeProf selects all overloaded method declarations that matches actual arguments
p foo(42) #=> Integer
p foo("str") #=> String
p foo(1.0) #=> failed to resolve overload: Object#foo
class Object
def foo: (Integer) -> Integer
| (String) -> String
end
Flow-sensitive analysis demo: case/when with class constants
def foo(n)
case n
when Integer
p n #=> Integer
when String
p n #=> String
else
p n #=> Float
end
end
foo(42)
foo(1.0)
foo("str")
Result:
$ typeprof test.rb
# Revealed types
# test.rb:4 #=> Integer
# test.rb:8 #=> Float
# test.rb:6 #=> String
# Classes
class Object
def foo : (Float | Integer | String) -> (Float | Integer | String)
end
Flow-sensitive analysis demo: is_a?
and respond_to?
def foo(n)
if n.is_a?(Integer)
p n #=> Integer
else
p n #=> Float | String
end
if n.respond_to?(:times)
p n #=> Integer
else
p n #=> Float | String
end
end
foo(42)
foo(1.0)
foo("str")
Flow-sensitive analysis demo: x || y
# ENV["FOO"] returns String? (which means String | nil)
p ENV["FOO"] #=> String?
# Using "|| (default value)" can force it to be non-nil
p ENV["FOO"] || "default" #=> String
Recursion
def fib(x)
if x <= 1
x
else
fib(x - 1) + fib(x - 2)
end
end
fib(40000)
Result:
$ typeprof test.rb
# Classes
class Object
def fib : (Integer) -> Integer
end
"Stub-execution" that invokes methods without tests
def foo(n)
# bar is invoked with Integer arguments
(42)
n
end
def (n)
n
end
# As there is no test code to call methods foo and bar,
# TypeProf tries to invoke them with "untyped" arguments
Result:
$ typeprof test.rb
# Classes
class Object
def foo : (untyped) -> untyped
def : (Integer) -> Integer
end
Library demo
require "pathname"
p Pathname("foo") #=> Pathname
p Pathname("foo").dirname #=> Pathname
p Pathname("foo").ctime #=> Time
More
See ruby/typeprof's smoke directory.