Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Ruby3 is a typed language

Ruby3 is a typed language

Soutaro Matsumoto

April 17, 2020
Tweet

More Decks by Soutaro Matsumoto

Other Decks in Programming

Transcript

  1. Soutaro Matsumoto • Leads the design and implementation of RBS.

    • Develops a static type checker for Ruby “Steep”. • Software Engineer at Square.
  2. 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);
  3. 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);
  4. 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)
  5. 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)
  6. 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
  7. 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
  8. 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.
  9. 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
  10. 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)
  11. 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?
  12. 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
  13. 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
  14. 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.
  15. 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
  16. 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
  17. “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.
  18. 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
  19. 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
  20. 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
  21. 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)
  22. 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)
  23. 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)
  24. Coding with Type Checker • Design API before implementation. Design

    Doc Test Implementation Abstract Concrete Works (Revenue) Executable (pass/fail) Human readable (review)
  25. 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
  26. 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
  27. 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
  28. 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
  29. 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
  30. Type Checking Options 1. No type checking. 2. Type checking

    using a static type checker. 3. Between 1 and 2.
  31. 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.
  32. 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
  33. 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.
  34. 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
  35. 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
  36. 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
  37. 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?