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

Adopting Gradual Typing in Ruby with Sorbet

Adopting Gradual Typing in Ruby with Sorbet

Andela Online Technical Workshop Series - Wed 21st Oct 2020

2bb923c57a1fdc3a2eb484bb8d565fd2?s=128

Ufuk Kayserilioglu

October 21, 2020
Tweet

Transcript

  1. None
  2. SEATBELTS SEATBELTS

  3. Ufuk Kayserilioğlu Ufuk Kayserilioğlu Production Engineering Manager Ruby Infrastructure Team

  4. From Istanbul, Turkey Living in and working from Cyprus

  5. https://www.youtube.com/watch?v=9ScwEOEl_pc 0:00 / 0:29

  6. SEATBELTS SAVE SEATBELTS SAVE LIVES LIVES

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

    of serious injury by 50% https://www.cdc.gov/motorvehiclesafety/seatbeltbrief/index.html
  8. BUT BUT

  9. 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
  10. 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
  11. None
  12. None
  13. SORBET SORBET

  14. SORBET SORBET fast powerful static gradual type checker for Ruby

  15. QUICK PREVIEW QUICK PREVIEW

  16. # typed: true class User 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
  17. 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
  18. 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
  19. 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
  20. # 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
  21. None
  22. Q: So what is that # typed: true thing? #

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

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

  25. GRADUAL TYPECHECKING GRADUAL TYPECHECKING You can enable typechecking at: File

    level
  26. GRADUAL TYPECHECKING GRADUAL TYPECHECKING You can enable typechecking at: File

    level Method level
  27. GRADUAL TYPECHECKING GRADUAL TYPECHECKING You can enable typechecking at: File

    level Method level Argument level
  28. GRADUAL TYPECHECKING GRADUAL TYPECHECKING You can enable typechecking at: File

    level Method level Argument level Call site level
  29. File level granularity File level granularity Strictness levels

  30. File level granularity File level granularity Strictness levels All errors

    silenced typed: ignore typed: false typed: true typed: strict All errors reported typed: strong
  31. 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
  32. 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
  33. 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
  34. 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
  35. 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
  36. BILLION DOLLAR BILLION DOLLAR MISTAKE? MISTAKE?

  37. 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
  38. 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
  39. 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
  40. MORE FEATURES MORE FEATURES See for documentation Try it online

    at sorbet.org/docs sorbet.run
  41. BUT YOU BUT YOU PROMISED PROMISED

  42. Errors & Hover

  43. Go to De inition

  44. Autocomplete & Docs

  45. More tools are possible

  46. ADOPTING SORBET ADOPTING SORBET

  47. ADOPTING SORBET ADOPTING SORBET at Shopify at Shopify

  48. TIMELINE TIMELINE Usage started experimentally in November 2018 O icially

    released at the end of April 2019
  49. TOOLING TOOLING

  50. 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
  51. 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
  52. 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.
  53. RESULTS RESULTS Sigils used in Core across all iles

  54. RESULTS RESULTS Percentage of method calls with signature

  55. TESTIMONIALS TESTIMONIALS Many teams have used Sorbet extensively In-person interviews

    with members of 3 teams
  56. TESTIMONIALS TESTIMONIALS What they liked

  57. TESTIMONIALS TESTIMONIALS What they liked “We get quicker feedback than

    tests or CI”
  58. TESTIMONIALS TESTIMONIALS What they liked “We get quicker feedback than

    tests or CI” “Allowed us to write less tests”
  59. 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”
  60. 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”
  61. 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”
  62. 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”
  63. TESTIMONIALS TESTIMONIALS What they disliked

  64. TESTIMONIALS TESTIMONIALS What they disliked “Syntax is verbose, reduces code

    visibility”
  65. TESTIMONIALS TESTIMONIALS What they disliked “Syntax is verbose, reduces code

    visibility” “It is not DRY”
  66. 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”
  67. 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”
  68. 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”
  69. HOW CAN YOU ADOPT? HOW CAN YOU ADOPT?

  70. HOW CAN YOU ADOPT? HOW CAN YOU ADOPT? Try starting

    new code using Sorbet
  71. 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
  72. 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
  73. Fasten your seatbelts Fasten your seatbelts

  74. Fasten your seatbelts Fasten your seatbelts It is worth the

    minor annoyance.
  75. Fasten your seatbelts Fasten your seatbelts It is worth the

    minor annoyance. And we can make it better over time.
  76. THANK YOU THANK YOU Ufuk Kayserilioğlu Ufuk Kayserilioğlu @paracycle @paracycle

    https://www.shopify.com/careers