$30 off During Our Annual Pro Sale. View Details »

Adopting Gradual Typing in Ruby with Sorbet

Adopting Gradual Typing in Ruby with Sorbet

Andela Online Technical Workshop Series - Wed 21st Oct 2020

Ufuk Kayserilioglu

October 21, 2020
Tweet

More Decks by Ufuk Kayserilioglu

Other Decks in Technology

Transcript

  1. Reduce the risk of death by 45% Cut the risk

    of serious injury by 50% https://www.cdc.gov/motorvehiclesafety/seatbeltbrief/index.html
  2. 4% of drivers in Canada 13% of drivers in the

    US 54% of drivers in Turkey drive without a seatbelt https://en.wikipedia.org/wiki/Seat_belt_use_rates_by_country
  3. They ind them uncomfortable Over 65s ind wearing them too

    restricting Do not think they’ll ever need them They think they are less likely to have an accident https://www.bbc.com/news/blogs-magazine-monitor-29416372
  4. user_hash.fetch("name", "[No Name]") User.name_from_hash(name: "Ufuk") # typed: true 1 2

    class User 3 def self.name_from_hash(user_hash) 4 5 end 6 end 7 8 9
  5. sig do params(user_hash: T::Hash[Symbol, String]) .returns(String) end # typed: true

    1 2 class User 3 extend(T::Sig) 4 5 6 7 8 9 def self.name_from_hash(user_hash) 10 user_hash.fetch("name", "[No Name]") 11 end 12 end 13 14 User.name_from_hash(name: "Ufuk") 15
  6. extend(T::Sig) # typed: true 1 2 class User 3 4

    5 sig do 6 params(user_hash: T::Hash[Symbol, String]) 7 .returns(String) 8 end 9 def self.name_from_hash(user_hash) 10 user_hash.fetch("name", "[No Name]") 11 end 12 end 13 14 User.name_from_hash(name: "Ufuk") 15
  7. # typed: true class User extend(T::Sig) sig do params(user_hash: T::Hash[Symbol,

    String]) .returns(String) end def self.name_from_hash(user_hash) user_hash.fetch("name", "[No Name]") end end User.name_from_hash(name: "Ufuk") 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
  8. Q: So what is that # typed: true thing? #

    typed: true 1 2 class Foo 3 extend(T::Sig) 4 ... 5
  9. Q: So what is that # typed: true thing? A:

    It is gradual type checking in action # typed: true class Foo extend(T::Sig) ...
  10. GRADUAL TYPECHECKING GRADUAL TYPECHECKING You can enable typechecking at: File

    level Method level Argument level Call site level
  11. File level granularity File level granularity Strictness levels All errors

    silenced typed: ignore typed: false typed: true typed: strict All errors reported typed: strong
  12. Method level granularity Method level granularity Opt methods for more

    checks by adding a sig # typed: true class User extend(T::Sig) sig do params(user_hash: T::Hash[Symbol, String]) .returns(String) end def self.name_from_hash(user_hash) user_hash.fetch(:name, "[No Name]") end # Not checked def age 45 end end User.name_from_hash(name: "Ufuk") User.new.age 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
  13. Argument level granularity Argument level granularity T.untyped is a gift

    that keeps on giving: # typed: true class User def age 45 end end user = User.new user.age #=> 45 user.country # Error: Method `country` does not exist on `User` untyped_user = T.let(User.new, T.untyped) untyped_user.country # No errors city = untyped_user.country.capital # No errors, `city` is also `T.untyped` 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
  14. Argument level granularity Argument level granularity Use T.untyped when parameter

    is unknown # typed: true class User extend(T::Sig) sig do params(user_hash: T::Hash[Symbol, T.untyped]) .returns(String) end def self.name_from_hash(user_hash) user_hash.fetch(:name, "[No Name]") end # Same as: sig { returns(T.untyped) } def age 45 end end User.name_from_hash(name: "Ufuk") User.name_from_hash(name: "Ufuk", age: 45) User.new.age 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
  15. Call-site granularity Call-site granularity T.unsafe to the rescue: # typed:

    true class A define_method(:foo) { puts 'In A#foo' } define_singleton_method(:bar) { puts 'In A.bar' } end a = A.new a.foo # => Method `foo` does not exist on `A` T.unsafe(a).foo # ok 1 2 3 4 5 6 7 8 9 10 11
  16. Call-site granularity Call-site granularity What if there is no receiver?

    # typed: true class A define_method(:foo) { puts 'In A#foo' } define_singleton_method(:bar) { puts 'In A.bar' } end class B < A bar # => Method `bar` does not exist on `T.class_of(B)` T.unsafe(self).bar # ok end 1 2 3 4 5 6 7 8 9 10 11
  17. NILABLE TYPES NILABLE TYPES Types are non-nilable by default. T.nilable

    makes them nilable. # typed: true class User extend(T::Sig) sig { params(birth_date: T.nilable(Date)).returns(Integer) } def age(birth_date) # Error: Returning value that does not conform to method result type if birth_date calculate_age(birth_date) end end sig { params(birth_date: Date).returns(Integer) } def calculate_age(birth_date) # Do hard date math end end 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
  18. NILABLE TYPES NILABLE TYPES Sorbet understands low typing # typed:

    true class User extend(T::Sig) sig { params(birth_date: T.nilable(Date)).returns(Integer) } def age(birth_date) if birth_date calculate_age(birth_date) else 45 end end sig { params(birth_date: Date).returns(Integer) } def calculate_age(birth_date) # Do hard date math end end 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
  19. MORE FEATURES MORE FEATURES Runtime typechecker Runtime typechecker Ruby Interface

    Files Ruby Interface Files Flow sensitive typing Flow sensitive typing Generic types Generic types Union, intersection types Union, intersection types Exhaustiveness checking Exhaustiveness checking
  20. Running semi-custom tooling on top of Sorbet sorbet used for

    static type checking sorbet runtime used for runtime type checking, but not in production. tapioca used for RBI generation from gems rubocop sorbet to enforce certain rules spoom to encapsulate Sorbet operations Releasing custom tooling back to community as open-source rubocop sorbet - Open-source, announced tapioca - Open-source, announced spoom - Open-source, announced
  21. tapioca tapioca Cost of entry to use Sorbet is for

    all constants to resolve. Sorbet has no idea about the constants exported from gems. srb init (along with sorbet typed repo) does a decent enough job but we wanted better
  22. tapioca tapioca Works a little differently: . requires your Gem

    ile, . runs Sorbet over each gem’s source code, . uses re lection to discover ancestors, mixins, methods and subconstants . generates a separate RBI for each gem tagged with its version.
  23. TESTIMONIALS TESTIMONIALS What they liked “We get quicker feedback than

    tests or CI” “Allowed us to write less tests”
  24. TESTIMONIALS TESTIMONIALS What they liked “We get quicker feedback than

    tests or CI” “Allowed us to write less tests” “Both static and runtime typecheckers caught errors not caught by tests alone”
  25. TESTIMONIALS TESTIMONIALS What they liked “We get quicker feedback than

    tests or CI” “Allowed us to write less tests” “Both static and runtime typecheckers caught errors not caught by tests alone” “Easier to use in new code”
  26. TESTIMONIALS TESTIMONIALS What they liked “We get quicker feedback than

    tests or CI” “Allowed us to write less tests” “Both static and runtime typecheckers caught errors not caught by tests alone” “Easier to use in new code” “Improves code quality, makes you design better code”
  27. TESTIMONIALS TESTIMONIALS What they liked “We get quicker feedback than

    tests or CI” “Allowed us to write less tests” “Both static and runtime typecheckers caught errors not caught by tests alone” “Easier to use in new code” “Improves code quality, makes you design better code” “Living documentation of the public contract of your code”
  28. TESTIMONIALS TESTIMONIALS What they disliked “Syntax is verbose, reduces code

    visibility” “It is not DRY” “Types can get hard to write in more complex cases”
  29. TESTIMONIALS TESTIMONIALS What they disliked “Syntax is verbose, reduces code

    visibility” “It is not DRY” “Types can get hard to write in more complex cases” “Hard to add types to existing code”
  30. TESTIMONIALS TESTIMONIALS What they disliked “Syntax is verbose, reduces code

    visibility” “It is not DRY” “Types can get hard to write in more complex cases” “Hard to add types to existing code” “Rails and other metaprogramming is not fully functional yet”
  31. HOW CAN YOU ADOPT? HOW CAN YOU ADOPT? Try starting

    new code using Sorbet Whenever you can and when it is easy, add sig s to existing methods
  32. HOW CAN YOU ADOPT? HOW CAN YOU ADOPT? Try starting

    new code using Sorbet Whenever you can and when it is easy, add sig s to existing methods Use gradual typing to your advantage
  33. Fasten your seatbelts Fasten your seatbelts It is worth the

    minor annoyance. And we can make it better over time.