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

What you need to know about static typing

What you need to know about static typing

Lucas Uyezu

November 28, 2019
Tweet

More Decks by Lucas Uyezu

Other Decks in Programming

Transcript

  1. • 800k+ merchants in 175 countries • RoR 6.1.0.alpha •

    Majestic Monolith with satellite services • Hundreds of developers • 50+ deploys per day
  2. Ruby 3 • Coming on Xmas 2020. • Speed (Ruby

    3x3) • Concurrency • Static analysis
  3. • Defined what a strong tyepd language is in 1974.

    • One of the SOLID principles is named after her.
  4. Barbara Liskov "... whenever an object is passed from a

    calling function to a called function, its type must be compatible with the type declared in the called function." -- 1974
  5. Strong Typing lucasuyezu@Lucass-MBP ~/s/g/l/r/src> irb irb(main):001:0> 1 + '1' TypeError:

    String can't be coerced into Fixnum from (irb):1:in `+' from (irb):1 from /usr/bin/irb:11:in `<main>' irb(main):002:0>
  6. WAT

  7. Static Typing lucasuyezu@Lucass-MBP ~/s/g/l/rubyconfbr2019> cargo build Compiling rubyconfbr2019 v0.1.0 (file:///Users/lucasuyezu/src/github.com/lucasuyezu/rubyconfbr2019)

    error[E0308]: mismatched types --> main.rs:6:27 | 6 | println!("{:?}", add(1, '1')); | ^^^ expected usize, found char error: aborting due to previous error For more information about this error, try `rustc --explain E0308`. error: Could not compile `rubyconfbr2019`. To learn more, run the command again with --verbose.
  8. Dynamic Typing def add(a, b) a + b end lucasuyezu@Lucass-MBP

    ~/s/g/l/rubyconfbr2019> ruby main.rb Traceback (most recent call last): 2: from main.rb:5:in `<main>' 1: from main.rb:2:in `add' main.rb:2:in `+': String can't be coerced into Integer (TypeError)
  9. • Optional. You won't need to change your Ruby code

    to run on Ruby 3. • Level 1: No signatures. Reports possible bugs. • May report false positives. • Level 2: requires signatures. Verifies that code complies with signatures. • Steep. • Sorbet. • RDL.
  10. • Ruby 3 will provide a standard type signature format.

    • The official format is still WIP.
  11. .rbs • Ruby type signatures: github.com/ruby/ruby- signature • library developers

    will be able to bundle .rbs definitions. • application developers will be able to write their own for the ones missing.
  12. # -- example.rbs -- class Array[A] include Enumerable def []:

    (Integer) -> A? def each: { (A)->void } -> self ... end
  13. Sorbet • Level 2 static & runtime typechecking (We do

    runtime in development only) • Scalable: 100k LoC/second. 100x faster than rubocop. • It leverages Language Server Protocol so you can put it in your IDE. • 6 month private beta @ Shopify.
  14. Why

  15. Why • It eliminates a whole class of errors. •

    It's been proved successful by other languages (TypeScript, mypy). • You have small wins by just turning it on.
  16. Sorbet Why: Gradual | All errors silenced All errors reported

    | |---------------------|--------------|-------------|---------------|---------------------| | typed: ignore | typed: false | typed: true | typed: strict | typed: strong |
  17. Sorbet Why: Dead code if Random.rand foo = 1 else

    foo = 2 end editor.rb:5: This code is unreachable https://srb.help/7006 5 | foo = 2 ^ Errors: 1
  18. Sorbet Why: Typos begin data = JSON.parse(File.read(path)) rescue JSON::ParseError =>

    e raise "Got invalid JSON: #{e}" end json.rb:6: Unable to resolve constant ParseError 6 | rescue JSON::ParseError => e ^^^^^^^^^^^^^^^^ shims/gems/json.rbi:319: Did you mean: ::JSON::ParserError? 319 |class JSON::ParserError < JSON::JSONError ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  19. Example sig do params( url: T.nilable(String), sid: String ).returns(T.nilable(String)) end

    def append_search_id(url, sid) return nil if url.nil? url + "?_sid=#{sid}" end
  20. • Do not stop the world to add type annotations.

    • Keep delivering value and add them as you touch files. • Add a CI step, and runtime checks on development.
  21. Primitive Obsession The use of primitives like Strings and/or numbers

    for objects that need special handling. Aka Missing abstraction. Remember with_indifferent_access?
  22. Example sig do params( url: T.nilable(String), sid: String ).returns(T.nilable(String)) end

    def append_search_id(url, sid) return nil if url.nil? url + "?_sid=#{sid}" end
  23. Example sig do params( url: T.nilable(String), sid: String ).returns(T.nilable(String)) end

    def append_search_id(url, sid) return nil if url.nil? separator = url =~ /\?/ ? '&' : '?' url + "#{url}#{separator}_sid=#{sid}" end
  24. sig do params( url: T.nilable(String), sid: String ).returns(T.nilable(String)) end def

    append_search_id(url, sid) return nil if url.nil? separator = url =~ /\?/ ? '?' : '&' sid = URI.encode(sid) url + "#{url}#{separator}_sid=#{sid}" end
  25. Use an URI object sig do params( url: T.nilable(String), sid:

    String ).returns(T.nilable(String)) end def append_search_id(url, sid) return nil if url.nil? url = Addressable::URI.parse(url) params = url.query_values || {} params['_sid'] = sid url.query_values = params url.to_s end
  26. Recieve and return an URI object sig do params( url:

    T.nilable(Addressable::URI), sid: String ).returns(T.nilable(Addressable::URI)) end def append_search_id(url, sid) return nil if url.nil? url = Addressable::URI.parse(url) params = url.query_values || {} params['_sid'] = sid url.query_values = params url end
  27. Things that should not be primitives • URIs • IPs

    • Currency • Temperature • Measurements • Protip: (De)serialize them at the border.
  28. Sir Tony Hoare "I call it my billion-dollar mistake. It

    was the invention of the null reference in 1965. This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years." -- QCon London, 2009
  29. The case against null • Null references explode when they

    are used, not when they are returned. • It litters your code with null checks everywhere, disguised as .to_s, .to_i, .to_f, etc. • Modern languages like Rust don't have null by design. • C# introduced Nullable Reference Types, a stricter way of dealing with nulls.
  30. sig do params( url: T.nilable(Addressable::URI), sid: String ).returns(T.nilable(Addressable::URI)) end def

    append_search_id(url, sid) return nil if url.nil? url = Addressable::URI.parse(url) params = url.query_values || {} params['_sid'] = sid url.query_values = params url end
  31. # restaurant_suggestions.rb class RestaurantSuggestion DEFAULT_IMAGE_URL_STRING = "https://example.com/no_image.png" attr_accessor :url_string def

    url Addressable::URI.parse(url_string || DEFAULT_IMAGE_URL_STRING) end end ... url = append_search_id(restaurant_suggestion.url, search_id)
  32. Example sig do params( url: Addressable::URI, sid: String ).returns(Addressable::URI) end

    def append_search_id(url, sid) params = url.query_values || {} params['_sid'] = sid url.query_values = params url end
  33. Result: After sig do params( uri: Addressable::URI, sid: String ).returns(Addressable::URI)

    end def append_search_id(uri, sid) params = uri.query_values || {} params[:_sid] = sid uri.query_values = params uri end
  34. • Static type checkers and type annotations can help you

    find implicit concepts and code smells in your code. • Making these concepts explicit and fixing these smells is more important than adding type annotations.
  35. • Avoid null references whenever possible. • Avoid primitives whenever

    possible. • Do not stop the world to implement sorbet. The key point is gradual typing. Add a CI step and move on.
  36. Homework • EuRuKo 2019 - A Plan towards Ruby 3

    Types by Yusuke Endoh • Sorbet Homepage • Strange Loop 2018 Stripe's Sorbet Talk • Null Object Design Pattern • Primitive Obsession Anti-pattern • WAT