Share
Embed
Ruby3 is a typed language
Ruby3 summit online (https://rhc.connpass.com/event/169873/)
Soutaro Matsumoto
April 17, 2020
Transcript
43 slides
-
Soutaro Matsumoto • Leads the design and implementation of RBS.
• Develops a static type checker for Ruby “Steep”. • Software Engineer at Square. -
What is a Typed Language • You can write types.
• Type checker find problems. class Student { fullName: string; constructor(public firstName: string, public lastName: str this.fullName = firstName + " " + lastName; } } interface Person { firstName: string; lastName: string; } function greeter(person: Person): string { return "Hello, " + person.first_name + " " + person.lastNa } let user = new Student("Jane", "User"); document.body.textContent = greeter(user); -
What is a Typed Language • You can write types.
• Type checker find problems. class Student { fullName: string; constructor(public firstName: string, public lastName: str this.fullName = firstName + " " + lastName; } } interface Person { firstName: string; lastName: string; } function greeter(person: Person): string { return "Hello, " + person.first_name + " " + person.lastNa } let user = new Student("Jane", "User"); document.body.textContent = greeter(user); -
Ruby 2.x class Student attr_reader :first_name attr_reader :last_name def initialize(first_name,
last_name) @first_name = first_name @last_name = last_name end def full_name "#{first_name} #{last_name}" end end def greeter(peron) "Hello, #{person.first_name} #{person.last_name}" end user = Student.new("Jane", "User") puts greeter(user) -
Ruby3 class Student attr_reader :first_name attr_reader :last_name def initialize(first_name, last_name)
= @first_name, @last_name = first_name, last_name def full_name = "#{first_name} #{last_name}" end module Greeter def self.greet(peron) = "Hello, #{person.first_name} #{person.last_name}" end user = Student.new("Jane", "User") puts Greeter.greet(user) -
Ruby3 class Student attr_reader :first_name attr_reader :last_name def initialize(first_name, last_name)
= @first_name, @last_name = first_name, last_name def full_name = "#{first_name} #{last_name}" end module Greeter def greet(peron) = "Hello, #{person.first_name} #{person.last_name}" end user = Student.new("Jane", "User") puts Greeter.greet(user) class Student attr_reader first_name: String attr_reader last_name: String def initialize(String, String) -> void def full_name: () -> String end interface _Person def first_name: () -> String def last_name: () -> String end module Greeter def self.greet: (_Person) -> String end -
RBS • Type declaration for Ruby programs. • No type
annotation in Ruby code, but in different files. • Declare classes, modules, methods, ... class Student attr_reader first_name: String attr_reader last_name: String def initialize(String, String) -> void def full_name: () -> String end interface _Person def first_name: () -> String def last_name: () -> String end module Greeter def greet: (_Person) -> String end -
Ruby3 • Ruby2 program == Ruby code Ruby3 program ==
Ruby code + RBS. • Ruby3 will ship with RBS (+ stdlib signatures). • RBS (+ stdlib signatures) will release as a gem for 2.7 soon. • Type checkers won't be a part of Ruby. -
State of RBS • https://github.com/ruby/ruby-signature • No release yet. •
Will rename to RBS ($ gem install rbs). • PR is open on ruby/ruby for migration. • PRs from Rubyists! • Improvements on .rbs files • Improvements on test features -
RBS Ecosystem • Type checkers • Steep natively supports RBS.
• type-profiler uses RBS (@mame) • Integration with existing tools/frameworks • Translation from/to Sorbet RBI (shopify/rbs_parser) • Rails support (pocke/rbs_rails) -
Do we need to ship gems with RBS? Type checking
from IDE? I don't want to type check my code. RBS is not DRY! My code is small. Cannot justify the extra cost for type checking. I don't want to write types. What if a gem doesn't provide RBS? How can I write RBS for this? How about duck typing? -
RBS Expressiveness Union types Interface types Method overloading class Array[E]
def []: (Integer) -> E | (Integer, Integer) -> Array[E] end Keyword args class Kernel private def gets: () -> (String | nil) # String? end interface _ToStr def to_str: () -> String end class String def +: (_ToStr) -> String end class Kernel private def clone: (?freeze: bool) -> self end -
RBS Expressiveness • Supports • Duck typing (by interface types)
• Open class • Union types • Method overloading • Doesn't support • Array#map! • Meta programming • Conditional mixin a = [1,2,3] a.map! { @1.to_s } class GPU include Win32 if os.windows? include Metal if os.mac? end attr_reader :user belongs_to :group attr_gzip :content -
Type Checking Options 1. Type checking using a static type
checker. 2. No type checking. 3. Between 1 and 2. Chose the best option for your project. -
Using Static Type Checker • Pros • Fully utilize type
checker • LSP based integrations provide better coding experience. • Cons • You have to take care of RBS • You have to write type-checking compatible code -
Using Static Type Checker • Pros • Fully utilize type
checker • LSP based integrations provide better coding experience. • Cons • You have to take care of RBS • You have to write type-checking compatible code -
“My Code is Small” • 100 lines of Ruby code
is worth type checking. • Small programs sometimes use generic data structures like Array/Hash. Let type checkers manage the data structure. -
class Student attr_reader first_name: String attr_reader last_name: String def initialize(String,
String) -> void def full_name: () -> String end sig/student.rbs -
class Student attr_reader first_name: String attr_reader last_name: String def initialize(String,
String) -> void def full_name: () -> String end sig/student.rbs lib/student.rb class Student attr_reader :first_name attr_reader :last_name def initialize(first_name, last_name) = @first_name, @last_name = first_name, last_name def full_name = "#{first_name} #{last_name}" end -
class Student attr_reader first_name: String attr_reader last_name: String def initialize(String,
String) -> void def full_name: () -> String end sig/student.rbs interface _Person def first_name: () -> String def last_name: () -> String end module Greeter def self.greet: (_Person) -> String end sig/greeter.rbs -
class Student attr_reader first_name: String attr_reader last_name: String def initialize(String,
String) -> void def full_name: () -> String end sig/student.rbs interface _Person def first_name: () -> String def last_name: () -> String end module Greeter def self.greet: (_Person) -> String end sig/greeter.rbs lib/main.rb user = Student.new("Jane", "User") puts Greeter.greet(user) -
class Student attr_reader first_name: String attr_reader last_name: String def initialize(String,
String) -> void def full_name: () -> String end sig/student.rbs interface _Person def first_name: () -> String def middle_name: () -> String? def last_name: () -> String end module Greeter def self.greet: (_Person) -> String end sig/greeter.rbs lib/main.rb user = Student.new("Jane", "User") puts Greeter.greet(user) -
class Student attr_reader first_name: String attr_reader last_name: String def initialize(String,
String) -> void def full_name: () -> String end sig/student.rbs interface _Person def first_name: () -> String def middle_name: () -> String? def last_name: () -> String end module Greeter def self.greet: (_Person) -> String end sig/greeter.rbs lib/main.rb user = Student.new("Jane", "User") puts Greeter.greet(user) -
Coding with Type Checker • Design API before implementation. Design
Doc Test Implementation Abstract Concrete Works (Revenue) Executable (pass/fail) Human readable (review) -
Coding with Type Checker • Design API before implementation. Design
Doc Test Implementation Abstract Concrete Works (Revenue) Executable (pass/fail) Human readable (review) Type check Verifiable -
Type Check Partially • What if app already exists? •
Type checkers allows mixing typed and untyped Ruby code. • App code vs test code. • Models vs controller. • This file cannot be typed. lib/student.rb eval <<-SRC class Student def method_missing(name, *args) case name when :first_name @first_name sig/student.rbs class Student attr_reader first_name: String attr_reader last_name: String def initialize: (String, String) -> void end -
lib/person.rb class Student attr_reader :first_name attr_reader :last_name def initialize(first_name, last_name)
= @first_name, @last_name = first_name, last_name def full_name = "#{first_name} #{last_name}" end lib/greeter.rb module Greeter def self.greeter(person) = "Hello, #{person.first_name} #{person.last_name}" end require "person" require "greeter" user = Student.new("Jane", "User") puts greeter(user) lib/main.rb -
lib/person.rb class Student attr_reader :first_name attr_reader :last_name def initialize(first_name, last_name)
= @first_name, @last_name = first_name, last_name def full_name = "#{first_name} #{last_name}" end lib/greeter.rb module Greeter def self.greeter(person) = "Hello, #{person.first_name} #{person.last_name}" end require "person" require "greeter" user = Student.new("Jane", "User") puts greeter(user) lib/main.rb sig/person.rbs class Student attr_reader first_name: String attr_reader last_name: String def initialize: (String, String) -> void def full_name: () -> String end -
lib/person.rb class Student attr_reader :first_name attr_reader :last_name def initialize(first_name, last_name)
= @first_name, @last_name = first_name, last_name def full_name = "#{first_name} #{last_name}" end lib/greeter.rb module Greeter def self.greeter(person) = "Hello, #{person.first_name} #{person.last_name}" end require "person" require "greeter" user = Student.new("Jane", "User") puts greeter(user) lib/main.rb sig/person.rbs class Student attr_reader first_name: String attr_reader last_name: String def initialize: (String, String) -> void def full_name: () -> String end sig/greeter.rbs interface _Person def first_name: () -> String def last_name: () -> String end module Greeter def self.greet: (_Person) -> Strin end -
Type Checking Options 1. No type checking. 2. Type checking
using a static type checker. 3. Between 1 and 2. -
RBS without Type Checker • You write RBS but don't
use type checker. • Pros • You can help your gem users type check their programs. • You don't have to write type-checker compatible code. • Cons • You cannot type check. -
Writing RBS • You don't have to write perfect RBS.
• List of methods/classes of untyped is okay. • Write methods you really need to type check your code. class CSV def self.read: (String) -> Array[Array[String]] end sig/polyfill/csv.rbs class IO def self.popen: (*untyped args) -> untyped end sig/polyfill/io.rbs -
rbs prototype $ bundle exec rbs prototype runtime Integer class
Integer < Numeric include JSON::Ext::Generator::GeneratorMethods::Integer def self.sqrt: (untyped) -> untyped public def %: (untyped) -> untyped def &: (untyped) -> untyped def *: (untyped) -> untyped Prints list of classes, modules, includes and methods. Replace untypeds with precise types. -
Runtime Type Check • Opt-in runtime type check with RBS
definition. • Technically possible, does anyone want this? $ RUBYOPT="-rbundler/setup -rrbs/test/runtime" \ RBS_RUNTIME_CHECK_TARGETS='Optcarrot::*' \ bundle exec rake test -
Inline Type Annotations • A feature of type checkers. •
[Steep] Not yet, but worth considering. class Student attr_reader :first_name # @type String attr_reader :last_name # @type String # @type (String, String) -> void def initialize(first_name, last_name) = @first_name, @last_name = first_name, last_name # @type () -> String def full_name = "#{first_name} #{last_name}" end -
Inline Type Annotations • A feature of type checkers. •
No plan yet, but worth considering. class Student < ApplicationRecord include Enumerable # including Enumerable[Student] # @type method classes: () -> ActiveRecord::Relation[Student] # @type method class_ids: () -> ActiveRecord::IDRelation has_many :classes # @type (University, classes: Array[ClassDefinition], requirements: Requirement, credits: Integer) -> Ar def minimum_classes_to_graduations(univ, classes:, requirements:, credits:) ... end end -
Do we need to ship gems with RBS? Type checking
from IDE? I don't want to type check my code. RBS is not DRY! My code is small. Cannot justify the extra cost for type checking. I don't want to write types. What if a gem doesn't provide RBS? How can I write RBS for this? How about duck typing?