Type Integrity: The Software Engineering Behind Stricter Typing

Type Integrity: The Software Engineering Behind Stricter Typing

Back in January, PHPSW had two great talks from Rob Allen (@akrabat) and Dave Liddament (@daveliddament) on the static analysis tools that the PHP community has built around stricter typing. If you missed them, you can check them out here: https://joind.in/event/phpsw-jan-2020.

But what is "stricter typing"? What problems does it solve, and how do you use it to solve them?

In this talk, Stuart will introduce you to the fundamental principles involved. He'll show you the basics of "robustness" and "correctness" in your software engineering, the costs of tackling those using defensive programming and Design by Contract(™), and how to use stricter typing to bring those costs right down.

Although the examples are written in PHP, these are basic principles that apply to all software. You don't have to be a PHP programmer to get something out of this talk.

2c1dc90ff7bf69097a151677624777d2?s=128

Stuart Herbert

April 08, 2020
Tweet

Transcript

  1. A presentation by @stuherbert
 for @GanbaroDigital Type Integrity The Software

    Engineering Behind Stricter Typing
  2. @GanbaroDigital This is a follow-up to the January 2020 talks

    @PHPSW by Rob Allen and Dave Liddament
  3. @GanbaroDigital Although the examples are in PHP, the underlying principles

    apply to all programming languages.
  4. @GanbaroDigital What's The Problem?

  5. @GanbaroDigital A Worked Example

  6. @GanbaroDigital A very simple example, that calculates the sales tax

    / VAT owed.
  7. @GanbaroDigital function calculateVat($amount, $rate) { return ($amount/100) * $rate; }

  8. @GanbaroDigital calculateVat() is not robust and not always correct.

  9. @GanbaroDigital function calculateVat($amount, $rate) { return ($amount/100) * $rate; }

  10. @GanbaroDigital function calculateVat($amount, $rate) { if (!is_int($amount)) { throw new

    Exception(...); } return ($amount/100) * $rate; }
  11. @GanbaroDigital function calculateVat($amount, $rate) { if (!is_int($amount)) { throw new

    Exception(...); } if (!is_int($rate)) { throw new Exception(...); } return ($amount/100) * $rate; }
  12. @GanbaroDigital We've gone from 1 line of code to 5

    lines of code ... ... and we're just getting started!
  13. @GanbaroDigital Without type hints, someone has to write every single

    check.
  14. @GanbaroDigital Every check you add is executed every time the

    function / method is called.
  15. @GanbaroDigital Every line of code that you add needs its

    own unit test.
  16. @GanbaroDigital We live in a world where the time it

    takes to create and ship working code is often the biggest cost for a project / org / business.
  17. @GanbaroDigital Some of these runtime checks can be replaced with

    type-hinting ...
  18. @GanbaroDigital function calculateVat($amount, $rate) { if (!is_int($amount)) { throw new

    Exception(...); } if (!is_int($rate)) { throw new Exception(...); } return ($amount/100) * $rate; }
  19. @GanbaroDigital declare(strict_types=1); function calculateVat( int $amount, int $rate ) {

    return ($amount/100) * $rate; }
  20. @GanbaroDigital ... but calculateVat() still isn't robust and still isn't

    always correct.
  21. @GanbaroDigital Remaining Issues Include ... • Generating negative values •

    Locale-specific rules on rounding up / down • Accepting the wrong currency • Accepting invalid VAT rates
  22. @GanbaroDigital Every legal value of $amount and $rate is an

    integer. Not every integer is a legal value of $amount and $rate.
  23. @GanbaroDigital The Engineering

  24. @GanbaroDigital Defensive Programming https://flic.kr/p/9SuCos

  25. @GanbaroDigital Defensive programming uses runtime checks to prevent "bad" data

    reaching the business logic.
  26. @GanbaroDigital Design By Contract https://unsplash.com/photos/dtZcW9cSot4

  27. @GanbaroDigital Design By Contract ™ uses runtime checks to inspect

    input AND return values.
  28. @GanbaroDigital Your Business Logic 
 Has To Wait In Line

    https://flic.kr/p/LnuA4G
  29. @GanbaroDigital “ Code that relies entirely on runtime checks is

    slower to write, and slower to run.
  30. @GanbaroDigital “Defensive programming is like the pandemic lockdown. It only

    works if it is practiced everywhere and by everyone.
  31. @GanbaroDigital Design By Contract ™ is a mid-80s technique to

    mitigate the risks of Object-Oriented Programming.
  32. @GanbaroDigital The Problem Is State https://unsplash.com/photos/IPx7J1n_xUc

  33. @GanbaroDigital Make The Incompatible, Impossible! https://flic.kr/p/8Ws1Sn

  34. @GanbaroDigital declare(strict_types=1); function calculateVat( int $amount, int $rate ) {

    return ($amount/100) * $rate; }
  35. @GanbaroDigital declare(strict_types=1); function calculateVat( CartAmountToTax $amount, VatRate $rate ) {

    return ($amount/100) * $rate; }
  36. @GanbaroDigital “ Use type systems to make bad data a

    bad fit.
  37. @GanbaroDigital Using Stricter Typing

  38. @GanbaroDigital ?? ?? How do we create stricter types that

    make our code robust and correct?
  39. @GanbaroDigital Type Refinement

  40. @GanbaroDigital declare(strict_types=1); function calculateVat( CartAmountToTax $amount, VatRate $rate ) {

    return ($amount/100) * $rate; }
  41. @GanbaroDigital $amount and $rate are values.

  42. @GanbaroDigital “ A value is data that has no identity.

    Two values are the same if their state is identical.
  43. @GanbaroDigital “Data that has identity is called an entity. Two

    entities are the same if their identities are identical.
  44. @GanbaroDigital The differences between values and entities aren't important for

    this talk.
  45. @GanbaroDigital declare(strict_types=1); function calculateVat( CartAmountToTax $amount, VatRate $rate ) {

    return ($amount/100) * $rate; }
  46. @GanbaroDigital CartAmountToTax and VatRate are value types.

  47. @GanbaroDigital In many languages (including PHP), the only way to

    define a value type is to define a class.
  48. @GanbaroDigital declare(strict_types=1); class CartAmountToTax { public constructor(???) { ... }

    } class VatRate { public constructor(???) { ... } }
  49. @GanbaroDigital ?? ?? What do we need to know to

    create these values?
  50. @GanbaroDigital If you're not sure where to start, start with

    the primitive types that you are replacing.
  51. @GanbaroDigital declare(strict_types=1); class CartAmountToTax { public constructor( int $amount )

    { .... } }
  52. @GanbaroDigital declare(strict_types=1); class VatRate { public constructor( string $jurisdiction, int

    $rate ) { ... } }
  53. @GanbaroDigital ?? ?? What work will these constructors do?

  54. @GanbaroDigital Every legal value of $amount and $rate is an

    integer. Not every integer is a legal value of $amount and $rate.
  55. @GanbaroDigital Type refinement takes a wider data type (like an

    int) and reduces it to a narrower data type (like a VatRate).
  56. @GanbaroDigital Type refinement is done by smart constructors.

  57. @GanbaroDigital Smart Constructors

  58. @GanbaroDigital ?? ?? What work will these constructors do?

  59. @GanbaroDigital Smart constructors enforce the data constraints for their value

    type.
  60. @GanbaroDigital A constraint is a non-negotiable condition that must be

    met.
  61. @GanbaroDigital ?? ?? What are the constraints of the CartAmountToTax

    value type?
  62. @GanbaroDigital ?? ?? What pre-conditions must be met?

  63. @GanbaroDigital class CartAmountToTax { public constructor( int $amount ) {

    // robustness! if ($amount < 0) { throw new Exception(...); } } }
  64. @GanbaroDigital class VatRate { public constructor( string $jurisdiction, int $rate

    ) { // robustness! if (!this->isValidVatRate(...)) { throw new Exception(...); } } }
  65. @GanbaroDigital Smart constructors prevent the creation of illegal values.

  66. @GanbaroDigital ?? ?? Haven't we simply added defensive programming to

    our class constructors?
  67. @GanbaroDigital Values built by smart constructors are guaranteed to be

    legal.
  68. @GanbaroDigital We move defensive programming from everywhere we use data

    to everywhere we create typed data.
  69. @GanbaroDigital Data Guards

  70. @GanbaroDigital class VatRate { public constructor( string $jurisdiction, int $rate

    ) { // robustness! if (!this->isValidVatRate(...)) { throw new Exception(...); } } }
  71. @GanbaroDigital class VatRate { public function isValidVatRate(...): boolean { //

    inspect params here } }
  72. @GanbaroDigital Data guards are functions / methods that return TRUE

    if a data constraint has been met.
  73. @GanbaroDigital Each data guard checks one data constraint, and ONLY

    one.
  74. @GanbaroDigital A specification is a data guard that calls other

    data guards.
  75. @GanbaroDigital Data guards never throw Exceptions.

  76. @GanbaroDigital Data Guarantees

  77. @GanbaroDigital class VatRate { public constructor( string $jurisdiction, int $rate

    ) { // robustness! if (!this->isValidVatRate(...)) { throw new Exception(...); } } }
  78. @GanbaroDigital ?? ?? Who checks the return values from function

    / method calls all the time?
  79. @GanbaroDigital You can't rely on developers checking return values from

    function calls. Never use return values to report an error.
  80. @GanbaroDigital class VatRate { public constructor( string $jurisdiction, int $rate

    ) { // robustness! mustBeValidVatRate(...); } }
  81. @GanbaroDigital function mustBeValidVatRate(...) { if (isValidVatRate(...)) { return; } throw

    new Exception(...); }
  82. @GanbaroDigital function mustBeValidVatRate(...) { if (isValidVatRate(...)) { return; } throw

    new Exception(...); }
  83. @GanbaroDigital Data guarantees are built from data guards.

  84. @GanbaroDigital We don't have to repeat the unit tests, because

    we are not repeating the code (the input validation).
  85. @GanbaroDigital function mustBeValidVatRate(...) { if (isValidVatRate(...)) { return; } throw

    new Exception(...); }
  86. @GanbaroDigital Data guarantees throw Exceptions if a data constraint has

    not been met.
  87. @GanbaroDigital ?? ?? Should data guarantees and data checks be

    methods, or functions?
  88. @GanbaroDigital Data guards and guarantees are more of a modular

    programming style than pure OOP.
  89. @GanbaroDigital ?? ?? Why did we call it CartAmountToTax and

    not something like Currency or Money?
  90. @GanbaroDigital Robustness

  91. @GanbaroDigital declare(strict_types=1); function calculateVat( CartAmountToTax $amount, VatRate $rate ) {

    return ($amount->value/100) * $rate->value; }
  92. @GanbaroDigital declare(strict_types=1); function calculateVat( Money $amount, VatRate $rate ) {

    return ($amount->value/100) * $rate->value; }
  93. @GanbaroDigital ?? ?? Are the data constraints for Money the

    same as for CartAmountToTax?
  94. @GanbaroDigital Every legal value of $amount and $rate is an

    integer. Not every integer is a legal value of $amount and $rate.
  95. @GanbaroDigital Every legal value of $amount is Money. Not every

    value of Money is a legal value of $amount.
  96. @GanbaroDigital (The right!) stricter types improve the robustness of our

    code.
  97. @GanbaroDigital Robustness is a measure of how well your code

    detects and rejects invalid input.
  98. @GanbaroDigital Robust code actively prevents bad inputs reaching your business

    logic.
  99. @GanbaroDigital Stricter types represent and reflect the problem domain's language

    / jargon.
  100. @GanbaroDigital declare(strict_types=1); function calculateVat( CartAmountToTax $amount, VatRate $rate ) {

    return ($amount->value/100) * $rate->value; }
  101. @GanbaroDigital declare(strict_types=1); function calculateVat( CartAmountToTax $amount, VatRate $rate ) {

    return ($amount->value/100) * $rate->value; } ✗
  102. @GanbaroDigital declare(strict_types=1); function calculateVatForCart( CartAmountToTax $amount, VatRate $rate ) {

    return ($amount->value/100) * $rate->value; }
  103. @GanbaroDigital class Cart { function calculateVat( CartAmountToTax $amount, VatRate $rate

    ) { return ($amount->value/100) * $rate->value; } }
  104. @GanbaroDigital calculateVat() is not robust and not always correct.

  105. @GanbaroDigital ?? ?? calculateVat() is now robust. Is it always

    correct?
  106. @GanbaroDigital Correctness

  107. @GanbaroDigital class Cart { function calculateVat( CartAmountToTax $amount, VatRate $rate

    ) { return ($amount->value/100) * $rate->value; } }
  108. @GanbaroDigital ?? ?? Is that calculation correct for every tax

    jurisdiction?
  109. @GanbaroDigital ?? ?? Why don't we ask the VatRate value

    object for the correct calculation?
  110. @GanbaroDigital class Cart { function calculateVat( CartAmountToTax $amount, VatRate $rate

    ) { return ($amount->value/100) * $rate->value; } }
  111. @GanbaroDigital class Cart { function calculateVat( CartAmountToTax $amount, VatRate $rate

    ) { return $rate->calculateVat( $amount ); } }
  112. @GanbaroDigital By themselves, stricter types do not guarantee that your

    code is now correct.
  113. @GanbaroDigital Stricter types allow us to plug the correct behaviour

    into our business logic.
  114. @GanbaroDigital Code is correct if it always produces the expected

    legal output for each given legal input.
  115. @GanbaroDigital Correct code is always deterministic.

  116. @GanbaroDigital In Summary ...

  117. @GanbaroDigital “ Code that relies entirely on runtime checks is

    slower to write, and slower to run.
  118. @GanbaroDigital We live in a world where the time it

    takes to create and ship working code is often the biggest cost for a project / org / business.
  119. @GanbaroDigital ?? ?? How do we create stricter types that

    make our code robust and correct?
  120. @GanbaroDigital We move defensive programming from everywhere we use data

    to everywhere we create data.
  121. @GanbaroDigital Type refinement takes a wider data type (like an

    int) and reduces it to a narrower data type (like a VatRate).
  122. @GanbaroDigital Smart constructors prevent the creation of illegal values.

  123. @GanbaroDigital Data guards are functions / methods that return TRUE

    if a data constraint has been met.
  124. @GanbaroDigital Each data guard checks one data constraint, and ONLY

    one.
  125. @GanbaroDigital A specification is a data guard that calls other

    data guards.
  126. @GanbaroDigital Data guards never throw Exceptions.

  127. @GanbaroDigital You can't rely on developers checking return values from

    function calls. Never use return values to report an error.
  128. @GanbaroDigital Data guarantees throw Exceptions if a data constraint has

    not been met.
  129. @GanbaroDigital Data guarantees are built from data guards.

  130. @GanbaroDigital (The right!) stricter types improve the robustness of our

    code.
  131. @GanbaroDigital Stricter types represent and reflect the problem domain's language

    / jargon.
  132. @GanbaroDigital By themselves, stricter types do not guarantee that your

    code is now correct.
  133. @GanbaroDigital Stricter types allow us to plug the correct behaviour

    into our business logic.
  134. Thank You How Can We Help You? A presentation by

    @stuherbert
 for @GanbaroDigital