Typed Ruby

Typed Ruby

Ruby has is a dynamic language, it has no types, why would we care?

7102c7fcada6404753124ed8f60a64ce?s=128

Nikita Shilnikov

October 21, 2017
Tweet

Transcript

  1. None
  2. Nikita Shilnikov Me

  3. I work on

  4. Typed Ruby

  5. There are no types in Ruby

  6. Everything is an object

  7. Objects interact via message passing

  8. 1 + 2

  9. 1 :+,2 message target

  10. 1.methods

  11. => [:%, :&, :*, :+, :-, :/, :<, :>, :^,

    :|, :~, :-@, :**, :<=>, :<<, :>>, :<=, :>=, :==, :===, :[], :inspect, :size, :succ, :to_int, :to_s, :to_i, :to_f, :next, :div, :upto, :chr, :ord, :coerce, :divmod, :fdiv, :modulo, :remainder, :abs, :magnitude, :integer?, :floor, :ceil, :round, :truncate, :odd?, :even?, :downto, :times, :pred, :bit_length, :digits, :to_r, :numerator, :denominator, :rationalize, :gcd, :lcm, :gcdlcm, :+@, :eql?, :singleton_method_added, :i, :real?, :zero?, :nonzero?, :finite?, :infinite?, :step, :positive?, :negative?, :quo, :arg, :rectangular, :rect, :polar, :real, :imaginary, :imag, :abs2, :angle, :phase, :conjugate, :conj, :to_c, :between?, :clamp, :instance_of?, :kind_of?, :is_a?, :tap, :public_send, :remove_instance_variable, :public_method, :singleton_method, :instance_variable_set, :define_singleton_method, :method, :extend, :to_enum, :enum_for, :=~, :!~, :respond_to?, :freeze, :object_id, :send, :display, :nil?, :hash, :class, :singleton_class, :clone, :dup, :itself, :taint, :tainted?, :untaint, :untrust, :untrusted?, :trust, :frozen?, :methods, :singleton_methods, :protected_methods, :private_methods, :public_methods, :instance_variable_get, :instance_variables, :instance_variable_defined?, :!, :!=, :__send__, :equal?, :instance_eval, :instance_exec, :__id__]
  12. => [:%, :&, :*, :+, :-, :/, :<, :>, :^,

    :|, :~, :-@, :**, :<=>, :<<, :>>, :<=, :>=, :==, :===, :[], :inspect, :size, :succ, :to_int, :to_s, :to_i, :to_f, :next, :div, :upto, :chr, :ord, :coerce, :divmod, :fdiv, :modulo, :remainder, :abs, :magnitude, :integer?, :floor, :ceil, :round, :truncate, :odd?, :even?, :downto, :times, :pred, :bit_length, :digits, :to_r, :numerator, :denominator, :rationalize, :gcd, :lcm, :gcdlcm, :+@, :eql?, :singleton_method_added, :i, :real?, :zero?, :nonzero?, :finite?, :infinite?, :step, :positive?, :negative?, :quo, :arg, :rectangular, :rect, :polar, :real, :imaginary, :imag, :abs2, :angle, :phase, :conjugate, :conj, :to_c, :between?, :clamp, :instance_of?, :kind_of?, :is_a?, :tap, :public_send, :remove_instance_variable, :public_method, :singleton_method, :instance_variable_set, :define_singleton_method, :method, :extend, :to_enum, :enum_for, :=~, :!~, :respond_to?, :freeze, :object_id, :send, :display, :nil?, :hash, :class, :singleton_class, :clone, :dup, :itself, :taint, :tainted?, :untaint, :untrust, :untrusted?, :trust, :frozen?, :methods, :singleton_methods, :protected_methods, :private_methods, :public_methods, :instance_variable_get, :instance_variables, :instance_variable_defined?, :!, :!=, :__send__, :equal?, :instance_eval, :instance_exec, :__id__]
  13. Duck typing

  14. a + b

  15. a.respond_to?(:+)

  16. Consequences

  17. foo.size # => 8

  18. foo.size # => 8

  19. A real world example

  20. name = params[:name]

  21. name = params[:name] String

  22. name # => true

  23. name # => true

  24. But true is not a String!

  25. name.camelize

  26. new_user = User.new(params.permit(:name)) new_user.name # => ?

  27. "t"

  28. None
  29. None
  30. Problems 1. Using models for coercion is questionable. 2. Coercion

    logic is opaque. 3. Types are defined by database columns. 4. Unhappy paths end with an error (better) or a data corruption (worse).
  31. Duck typing is unreliable when working with data

  32. Duck typing is unreliable when working with data

  33. Duck typing is unreliable when working with data

  34. What is a type?

  35. What is a type? 1. A category of people or

    things having common characteristics. 2. A person or thing exemplifying the ideal or defining characteristics of something (prototype). 3. Characters or letters that are printed or shown on a screen.
  36. What is a type? 1. A category of people or

    things having common characteristics. 2. A person or thing exemplifying the ideal or defining characteristics of something (prototype). 3. Characters or letters that are printed or shown on a screen.
  37. A category of people or things having common characteristics 1.

    Natural numbers (1, 2, 3, 4, ...). 2. Emojis ( ...). 3. Vertebrates (dog, cat, horse, ...).
  38. Types in programming Also called data types. Play a major

    role in statically typed languages.
  39. Types in static languages

  40. –Haskell Wiki “Types are how you describe the data your

    program will work with.”
  41. –Type-Driven Development with Idris “Types are a means of classifying

    (data) values.”
  42. data Planet = Earth

  43. data Planet = Earth keyword

  44. data Planet = Earth type name

  45. data Planet = Earth value

  46. data Planet = Earth | Mars | ... values

  47. isEarth Earth = True isEarth x = False

  48. isEarth Earth = True isEarth x = False

  49. def earth?(planet) planet == :earth end

  50. def earth?(planet) planet.public_send(:==, :earth) end

  51. earth?(:earth) # => true earth?(:mars) # => false earth?(:sun) #

    => false earth?(1) # => false
  52. isEarth 1

  53. isEarth :: Planet -> Bool

  54. isEarth :: Planet -> Bool

  55. 1. Early sanity-check, the compiler will check your code for

    consistency. 2. Advanced compilers can check a program for correctness. 3. Better semantics and docs. 4. Better introspection/IDE integration. 5. A lot of performance optimizations can be done during the compile phase. Pros:
  56. 1. Compilation takes time. 2. Types require more typing making

    code more verbose and less flexible. Cons:
  57. Statically typed languages have type inference

  58. val firstName = "Adam"

  59. Dynamically typed languages have type annotations

  60. def is_earth(planet: Planet): ...

  61. Types in Ruby

  62. Duck typing has nothing to do with types

  63. Duck typing is useless for describing data

  64. Applications are useless without data

  65. None
  66. dry-validation

  67. schema raw input valid data app DB errors

  68. outer world application

  69. CreateUserSchema = Dry::Validation.JSON do required(:name).filled(:str?) end

  70. CreateUserSchema.call(params)

  71. CreateUserSchema.call('name' => 'John').output # => { name: 'John' }

  72. CreateUserSchema.call('name' => true).output # => { name: ["must be a

    string"] }
  73. None
  74. something.blank?

  75. dry-types

  76. —a type system built for coercion and data validation; —formerly

    known as dry-data; —initially was created as a replacement for virtus; —is a direct dependency of dry-validation, ROM, and hanami-model. dry-types
  77. Adding types to Ruby

  78. None
  79. module Types include Dry::Types.module end

  80. int = Types::Strict::Int int[3] # => 3

  81. int = Types::Strict::Int int[3] # => 3 int['3'] # =>

    '3' violates constraints # (type?(Integer, '3') failed)
  82. int = Types::Coercible::Int int[3] # => 3 int['3'] # =>

    3
  83. Integer('3') nil.to_i # => 0 Integer(nil) # => TypeError ''.to_i

    # => 0 Integer('') # => ArgumentError
  84. Type constraints

  85. None
  86. Types are like sets

  87. None
  88. Types::Age = Types::Strict::Int.constrained(gteq: 18)

  89. Case equality

  90. case value when Types::Strict::Int then :number when Types::Strict::String then :str

    end
  91. Types have algebra*

  92. Types::Strict::Int | Types::Strict::String

  93. dry-struct

  94. class User < Dry::Struct attribute :name, Types::Strict::String attribute :age, Types::Age

    end
  95. Structs are meant to be valid

  96. module ProfileEvents class PasswordChanged < Dry::Struct attribute :user_id, Types::UUID attribute

    :password, Types::Password end class EmailChanged < Dry::Struct attribute :user_id, Types::UUID attribute :email, Types::Email end Updated = PasswordChanged | EmailChanged end
  97. module ProfileEvents class PasswordChanged < Dry::Struct attribute :user_id, Types::UUID attribute

    :password, Types::Password end class EmailChanged < Dry::Struct attribute :user_id, Types::UUID attribute :email, Types::Email end Updated = PasswordChanged | EmailChanged end
  98. module ProfileEvents class PasswordChanged < Dry::Struct attribute :user_id, Types::UUID attribute

    :password, Types::Password end class EmailChanged < Dry::Struct attribute :user_id, Types::UUID attribute :email, Types::Email end Updated = PasswordChanged | EmailChanged end
  99. Custom types

  100. module Types include Dry::Types.module end Types.Instance(Customer) Types.Constructor(Geopoint) { |lon, lat|

    "POINT (#{ lat } #{ lon })" } Types.Constant(:only_allowed)
  101. What’s inside?

  102. Types are values

  103. Types::Int.constrained(gteq: 18) == Types::Int.constrained(gteq: 18)

  104. age = Types::Strict::Int.constrained(gteq: 18) types = { age => :age

    } types[Types::Strict::Int.constrained(gteq: 18)] # => :age
  105. schema do attribute :company, Types::String attribute :license, Types::PG::JSONB end

  106. where { license.contain(enabled: true) } # WHERE "companies"."license" @> '{"enabled":true}'::jsonb)

  107. TypeExtensions.register(JSONB) do def contain(type, expr, value) sql = wrap(expr).contains(value) Attribute[SQL::Types::Bool].meta(sql_expr:

    sql) end end
  108. ROM 4.0 is out

  109. Types have metadata

  110. first_name = Types::Strict::String. constrained(min_size: 3) last_name = Types::Strict::String. constrained(min_size: 3)

  111. first_name = Types::Strict::String. constrained(min_size: 3). meta(name: 'first_name') last_name = Types::Strict::String.

    constrained(min_size: 3). meta(name: 'last_name')
  112. Types have ASTs

  113. Types::Strict::String.constrained(min_size: 3) # => #<Dry::Types::Constrained type=#<Dry::Types::Definition primitive=String options={} meta={}> options={:rule=>#<Dry::Logic::Operations::And

    rules=[#<Dry::Logic::Rule::Predicate predicate=#<Method: Module(Dry::Logic::Predicates::Methods)#type?> options={:args=>[String]}>, #<Dry::Logic::Rule::Predicate predicate=#<Method: Module(Dry::Logic::Predicates::Methods)#min_size?> options={:args=>[3]}>] options={}>} rule=#<Dry::Logic::Operations::And rules=[#<Dry::Logic::Rule::Predicate predicate=#<Method: Module(Dry::Logic::Predicates::Methods)#type?> options={:args=>[String]}>, #<Dry::Logic::Rule::Predicate predicate=#<Method: Module(Dry::Logic::Predicates::Methods)#min_size?> options={:args=>[3]}>] options={}> meta={}>
  114. int = Types::Strict::Int int.to_ast [:constrained, [[:definition, [Integer, {}]], [:predicate, [:type?,

    [[:type, Integer], [:input, Undefined]]]], {}]]
  115. require 'dry/types/compiler' compiler = Dry::Types::Compiler.new(Dry::Types) int = Types::Strict::Int int ==

    compiler.(int.to_ast)
  116. Age (Int >= 18) rand(50) + 18

  117. —data generators; —serializers; —data transformers; —auto-docs; —???

  118. Types make life easier

  119. Types make life easier So use them

  120. Recap — Duck typing is neither about ducks nor typing.

    — Types are sets of possible values that shape the data. — In dry-types types are ordinary objects that can be build and composed. — Types can even be decomposed allowing you to build new abstractions on top of them.
  121. Do not use .blank?

  122. It’s not about Haskell

  123. 1.0 is almost there

  124. Links dry-rb.org/gems/dry-types github.com/dry-rb/dry-types discourse.dry-rb.org gitter.im/dry-rb/chat

  125. Thank you! github.com/flash-gordon @NikitaShilnikov