CodeMash 2020: Solving the Boolean Identity Crisis

CodeMash 2020: Solving the Boolean Identity Crisis

While powerful in its simplicity and important to computation, the boolean can be limiting in applications. In this talk, briefly explore the history of boolean logic in computation and look at how booleans can become misused in programming languages. Explore examples where booleans obscure the meaning of code, make code harder to maintain, and hinder usability for teammates and users. Learn how to harness custom types and higher-order functions to write clearer code. More importantly, learn how to place empathy and usability at the forefront of the APIs and UIs you build.

94bd558238b69c45d3d3e15797ae94f7?s=128

Jeremy Fairbank

January 10, 2020
Tweet

Transcript

  1. Jeremy Fairbank @elpapapollo bit.ly/cm-bool

  2. @testdouble helps improves how the world build software. testdouble.com

  3. Available in print or e-book programming-elm.com

  4. None
  5. { ? }

  6. { dog: null, }

  7. FETCH?

  8. { fetching: true, dog: null, }

  9. SUCCESS?

  10. { fetching: false, success: true, dog: { name: 'Tucker' },

    }
  11. ERRORS?

  12. { fetching: false, success: false, dog: null, error: true, errorMessage:

    'Ruh roh!', }
  13. { fetching: true, success: true, dog: { name: 'Tucker' },

    error: true, errorMessage: 'Uh oh!', } ¯\_(ツ)_/¯ Invalid State
  14. { fetching: true, success: true, dog: { name: 'Tucker' },

    error: true, errorMessage: 'Uh oh!', } ¯\_(ツ)_/¯ Invalid State
  15. if (props.error) { ... } else if (props.fetching) { ...

    } else if (props.success) { ... } else { ... }
  16. STATE fetching success error ready

  17. PRIMITIVE OBSESSION USING COMIC SANS IS KINDA LIKE PRIMITIVE OBSESSION

  18. The Problem with Booleans…

  19. – Robert Harper “There is no information carried by a

    Boolean beyond its value, and that’s the rub.”
  20. binary data type with no inherent meaning boolean

  21. bookFlight('CLE', true) ?

  22. bookFlight('CLE', true) ? Offloading abstraction to our memory instead of

    the code.
  23. bookFlight('CLE', true, false, true) ? ? ?

  24. ?

  25. Boolean Algebra George Boole x ∧ y x ∨ y

    x ∨ (y ∧ z) ¬x
  26. Boolean Values true && true true || false true ||

    (true && false) !true
  27. Propositional Logic

  28. Premise 1: If it’s raining then it’s cloudy. Premise 2:

    It’s raining. Conclusion: It’s cloudy. Propositional Logic
  29. Premise 1: If it’s raining then it’s cloudy. Premise 2:

    It’s raining. Conclusion: It’s cloudy. PROPOSITIONS
  30. prop·o·si·tion a statement that expresses a concept that can be

    true or false
  31. Boolean ≠ Proposition A proposition, p, that is true is

    not the same as saying p is equal to true.
  32. LOSS OF INTENT

  33. LOSS OF INFORMATION

  34. 4

  35. bookFlight('CLE', true) ?

  36. function bookFlight(airport, isPremium) { if (isPremium) { ... } else

    { ... } }
  37. function bookFlight(airport, isPremium) { if (isPremium) { ... } else

    { ... } }
  38. function bookFlight(airport, isPremium) { if (isPremium) { ... } else

    { ... } } Regular customer?
  39. const OpacitySlider = createSlider(true) const VolumeSlider = createSlider(false) const App

    = () => ( <div> <OpacitySlider /> <VolumeSlider /> </div> ) ?
  40. const createSlider = isHorizontal => () => { if (isHorizontal)

    { ... } else { ... } }
  41. const createSlider = isHorizontal => () => { if (isHorizontal)

    { ... } else { ... } } Implicit vertical slider
  42. – Robert Martin “Boolean arguments loudly declare that the function

    does more than one thing. They are confusing and should be eliminated.”
  43. rotateFuelRod(fuelRod, 30, true) rotateControlRod( ) ?

  44. rotateFuelRod(fuelRod, 30, true) rotateControlRod(controlRod, 30, true)

  45. None
  46. function rotateFuelRod(fuelRod, amount, inDegrees) { ... } function rotateControlRod(controlRod, amount,

    inRadians) { ... }
  47. function rotateFuelRod(fuelRod, amount, inDegrees) { ... } function rotateControlRod(controlRod, amount,

    inRadians) { ... }
  48. function rotateFuelRod(fuelRod, amount, inDegrees) { ... } function rotateControlRod(controlRod, amount,

    inRadians) { ... }
  49. bookFlight('CLE', true, false, true) ? ? ?

  50. function bookFlight(airport, isPremium, hasCheckedLuggage, preferWindow) { const luggageCost = hasCheckedLuggage

    ? ... : ... if (isPremium) { if (hasCheckedLuggage) { ... } else { ... } } else if (hasCheckedLuggage) { ... } else { ... } }
  51. None
  52. – Martin Fowler “…an API should be written to make

    it easier for the caller, so if we know where the caller is coming from we should design the API with that information in mind.”
  53. Write code for humans Not computers

  54. None
  55. bookFlight('CLE', 'Premium')

  56. function bookFlight(airport, customerType) { switch (customerType) { case 'Premium': ...

    case 'Regular': ... default: ... } }
  57. function bookFlight(airport, customerType) { switch (customerType) { case 'Premium': ...

    case 'Regular': ... default: ... } }
  58. bookFlight('CLE', 'premium') bookFlight('CLE', 'economical') bookFlight('CLE', '') Primitive Obsession Revisited

  59. Represent a finite domain

  60. type CustomerType = Premium | Regular

  61. bookFlight "CLE" Premium bookFlight "CLE" Regular

  62. bookFlight city customerType = case customerType of Premium -> ...

    Regular -> ...
  63. bookFlight city customerType = case customerType of Premium -> ...

    Regular -> ...
  64. bookFlight city customerType = case customerType of Premium -> ...

    Regular -> ...
  65. const CustomerType = { Premium: 'Premium', Regular: 'Regular', } bookFlight('CLE',

    CustomerType.Premium) bookFlight('CLE', CustomerType.Regular)
  66. function bookFlight(airport, customerType) { switch (customerType) { case CustomerType.Premium: ...

    case CustomerType.Regular: ... default: ... } }
  67. function bookFlight(airport, customerType) { switch (customerType) { case CustomerType.Premium: ...

    case CustomerType.Regular: ... default: ... } }
  68. enum CustomerType { Premium = 'Premium', Regular = 'Regular', }

    bookFlight('CLE', CustomerType.Premium) bookFlight('CLE', CustomerType.Regular)
  69. function bookFlight( airport: string, customerType: CustomerType, ): Promise<Flight> { switch

    (customerType) { case CustomerType.Premium: ... case CustomerType.Regular: ... } }
  70. function bookFlight( airport: string, customerType: CustomerType, ): Promise<Flight> { switch

    (customerType) { case CustomerType.Premium: ... case CustomerType.Regular: ... } }
  71. function bookFlight( airport: string, customerType: CustomerType, ): Promise<Flight> { switch

    (customerType) { case CustomerType.Premium: ... case CustomerType.Regular: ... } }
  72. const AngleType = { Degrees: 'Degrees', Radians: 'Radians', } const

    degrees = value => ({ kind: AngleType.Degrees, value }) const radians = value => ({ kind: AngleType.Radians, value })
  73. const AngleType = { Degrees: 'Degrees', Radians: 'Radians', } const

    degrees = value => ({ kind: AngleType.Degrees, value }) const radians = value => ({ kind: AngleType.Radians, value })
  74. const AngleType = { Degrees: 'Degrees', Radians: 'Radians', } const

    degrees = value => ({ kind: AngleType.Degrees, value }) const radians = value => ({ kind: AngleType.Radians, value })
  75. rotateFuelRod(fuelRod, degrees(30)) rotateControlRod(controlRod, radians(Math.PI))

  76. enum AngleType { Degrees = 'Degrees', Radians = 'Radians', }

    type Angle = { kind: AngleType, value: number, }
  77. enum AngleType { Degrees = 'Degrees', Radians = 'Radians', }

    type Angle = { kind: AngleType, value: number, }
  78. enum AngleType { Degrees = 'Degrees', Radians = 'Radians', }

    type Angle = { kind: AngleType, value: number, }
  79. function rotateFuelRod( fuelRod: FuelRod, angle: Angle, ): FuelRod { switch

    (angle.kind) { case AngleType.Degrees: ... case AngleType.Radians: ... } }
  80. function rotate( rod: Rod, angle: Angle, ): Rod { switch

    (angle.kind) { case AngleType.Degrees: ... case AngleType.Radians: ... } }
  81. function rotate( rod: Rod, angle: Angle, ): Rod { switch

    (angle.kind) { case AngleType.Degrees: ... case AngleType.Radians: ... } }
  82. function rotate( rod: Rod, angle: Angle, ): Rod { switch

    (angle.kind) { case AngleType.Degrees: ... case AngleType.Radians: ... } }
  83. rotateInDegrees(fuelRod, 30) rotateInRadians(controlRod, Math.PI)

  84. rotateInDegrees(fuelRod, 30) rotateInRadians(controlRod, Math.PI) Still encourages primitive obsession

  85. function rotate( rod: Rod, angle: Angle, ): Rod { switch

    (angle.kind) { case AngleType.Degrees: return rotateInDegrees(rod, angle.value) case AngleType.Radians: return rotateInRadians(rod, angle.value) } }
  86. matchingStrings( Case.Insensitive, 'foo', ['FOOBAR', 'foobar'], ) matchingStrings( Case.Sensitive, 'foo', ['FOOBAR',

    'foobar'], )
  87. const matchingStrings = (case_, pattern, strings) => strings.filter(string => {

    switch (case_) { case Case.CaseSensitive: return string.includes(pattern) case Case.CaseInsensitive: return string .toUpperCase() .includes(pattern.toUpperCase()) } })
  88. const matchingStrings = (case_, pattern, strings) => strings.filter(string => {

    switch (case_) { case Case.CaseSensitive: return string.includes(pattern) case Case.CaseInsensitive: return string .toUpperCase() .includes(pattern.toUpperCase()) } })
  89. const matchingStrings = (case_, pattern, strings) => strings.filter(string => {

    switch (case_) { case Case.CaseSensitive: return string.includes(pattern) case Case.CaseInsensitive: return string .toUpperCase() .includes(pattern.toUpperCase()) } })
  90. const caseInsensitive = string => string.toUpperCase() const caseSensitive = string

    => string matchingStrings( caseInsensitive, 'foo', ['FOOBAR', 'foobar'], ) matchingStrings( caseSensitive, 'foo', ['FOOBAR', 'foobar'], )
  91. const caseInsensitive = string => string.toUpperCase() const caseSensitive = string

    => string matchingStrings( caseInsensitive, 'foo', ['FOOBAR', 'foobar'], ) matchingStrings( caseSensitive, 'foo', ['FOOBAR', 'foobar'], )
  92. const caseInsensitive = string => string.toUpperCase() const caseSensitive = string

    => string matchingStrings( caseInsensitive, 'foo', ['FOOBAR', 'foobar'], ) matchingStrings( caseSensitive, 'foo', ['FOOBAR', 'foobar'], )
  93. const matchingStrings = ( normalize, pattern, strings ) => strings.filter(string

    => normalize(string).includes(normalize(pattern)) )
  94. const matchingStrings = ( normalize, pattern, strings ) => strings.filter(string

    => normalize(string).includes(normalize(pattern)) )
  95. const matchingStrings = ( normalize, pattern, strings ) => strings.filter(string

    => normalize(string).includes(normalize(pattern)) )
  96. const matchingStrings = ( normalize, pattern, strings ) => strings.filter(string

    => normalize(string).includes(normalize(pattern)) ) Higher-order Function
  97. const bookPremiumCustomer = () => { ... } const bookRegularCustomer

    = () => { ... } bookFlight('CLE', bookPremiumCustomer) bookFlight('CLE', bookRegularCustomer)
  98. Make APIs UNDERSTANDABLE and CONVENIENT

  99. Function True False

  100. None
  101. function getTime(person) { if (doYouKnowTheTime(person)) { return tellMeTheTime(person) } else

    { return ` Does anybody really know what time it is? ` } }
  102. function getTime(person) { if (doYouKnowTheTime(person)) { return tellMeTheTime(person) } else

    { return ` Does anybody really know what time it is? ` } }
  103. function getTime(person) { if (doYouKnowTheTime(person)) { return tellMeTheTime(person) } else

    { return ` Does anybody really know what time it is? ` } }
  104. function getTime(person) { if (doYouKnowTheTime(person)) { return tellMeTheTime(person) } else

    { return ` Does anybody really know what time it is? ` } }
  105. function getTime(person) { if (doYouKnowTheTime(person)) { return tellMeTheTime(person) } else

    { return tellMeTheTime(person) } }
  106. Boolean Blindness

  107. – Conor McBride “To make use of a Boolean you

    have to know its provenance so that you can know what it means.”
  108. Get your organic, homegrown booleans from Rhode Island! Providence, RI

  109. PROVENANCE

  110. function findDogByName(name, dogs) { if (name in dogs) { return

    `${name} is a ${dogs[name].breed}`; } else { return 'Heck! No pupper found.'; } }
  111. function findDogByName(name, dogs) { if (name in dogs) { return

    `${name} is a ${dogs[name].breed}`; } else { return `${name} is a ${dogs[name].breed}`; } }
  112. function canDivide(denominator) { return denominator != 0 } function divisionResult(numerator,

    denominator) { if (canDivide(denominator)) { return `The result is ${numerator / denominator}` } else { return 'Could not divide' } }
  113. function canDivide(denominator) { return denominator != 0 } function divisionResult(numerator,

    denominator) { if (canDivide(denominator)) { return `The result is ${numerator / denominator}` } else { return 'Could not divide' } }
  114. function divisionResult(numerator, denominator) { if (canDivide(denominator)) { return `The result

    is ${numerator / denominator}` } else { return `The result is ${numerator / denominator}` } }
  115. – Dan Licata “Boolean tests let you look, options let

    you see.”
  116. Alternative Return Values

  117. type Maybe a = Nothing | Just a

  118. type Maybe a = Nothing | Just a

  119. type Maybe a = Nothing | Just a

  120. type Maybe a = Nothing | Just a

  121. 42 Just 42 "Hi" Just "Hi"

  122. Just 42 [42] Nothing [ ]

  123. import { Maybe } from 'true-myth' Maybe.just(42) // Just 42

    Maybe.nothing() // Nothing Maybe.of(42) // Just 42 Maybe.of(null) // Nothing Maybe.of(undefined) // Nothing
  124. function findDogByName(name, dogs) { return Maybe.of(dogs[name]) .map(dog => `${name} is

    a ${dog.breed}.`) .unwrapOr('Heck! No pupper found!') }
  125. function findDogByName(name, dogs) { return Maybe.of(dogs[name]) .map(dog => `${name} is

    a ${dog.breed}.`) .unwrapOr('Heck! No pupper found!') }
  126. Maybe.of undefined Maybe.of Nothing dog Just dog

  127. function findDogByName(name, dogs) { return Maybe.of(dogs[name]) .map(dog => `${name} is

    a ${dog.breed}.`) .unwrapOr('Heck! No pupper found!') }
  128. .map n => n * 2 Just 21 .map n

    => n * 2 Just 42 Nothing Nothing
  129. function findDogByName(name, dogs) { return Maybe.of(dogs[name]) .map(dog => `${name} is

    a ${dog.breed}.`) .unwrapOr('Heck! No pupper found!') }
  130. function findDogByName(name, dogs) { return Maybe.of(dogs[name]) .map(dog => `${name} is

    a ${dog.breed}.`) .unwrapOr('Heck! No pupper found!') }
  131. .unwrapOr(-1) Just 42 .unwrapOr(-1) 42 Nothing -1

  132. function findDogByName(name, dogs) { return Maybe.of(dogs[name]) .map(dog => `${name} is

    a ${dog.breed}.`) .unwrapOr('Heck! No pupper found!') }
  133. whatTimeIsIt(person) .map(time => ...) .unwrapOr(...)

  134. type Result error value = Ok value | Err error

  135. type Result error value = Ok value | Err error

  136. type Result error value = Ok value | Err error

  137. import { Result } from 'true-myth' Result.ok(42) // Ok 42

    Result.err('Uh oh') // Err 'Uh oh'
  138. const divide = (numerator, denominator) => denominator === 0 ?

    Result.err('Divide by zero') : Result.ok(numerator / denominator)
  139. const divide = (numerator, denominator) => denominator === 0 ?

    Result.err('Divide by zero') : Result.ok(numerator / denominator)
  140. const divide = (numerator, denominator) => denominator === 0 ?

    Result.err('Divide by zero') : Result.ok(numerator / denominator)
  141. const divide = (numerator, denominator) => denominator === 0 ?

    Result.err('Divide by zero') : Result.ok(numerator / denominator)
  142. const divisionResult = (numerator, denominator) => divide(numerator, denominator) .mapOrElse( error

    => `Could not divide: ${error}`, quotient => `The quotient is ${quotient}`, )
  143. const divisionResult = (numerator, denominator) => divide(numerator, denominator) .mapOrElse( error

    => `Could not divide: ${error}`, quotient => `The quotient is ${quotient}`, )
  144. const divisionResult = (numerator, denominator) => divide(numerator, denominator) .mapOrElse( error

    => `Could not divide: ${error}`, quotient => `The quotient is ${quotient}`, )
  145. const divisionResult = (numerator, denominator) => divide(numerator, denominator) .mapOrElse( error

    => `Could not divide: ${error}`, quotient => `The quotient is ${quotient}`, )
  146. See with Special Types

  147. Back to State { fetching: false, success: true, dog: {

    name: "Tucker" }, error: False, errorMessage: "", }
  148. if (props.error) { ... } else if (props.fetching) { ...

    } else if (props.success) { ... } else { ... }
  149. { fetching: true, success: true, dog: { name: "Tucker" },

    error: true, errorMessage: "Uh oh!", }
  150. https://youtu.be/IcgmSRJHu_8

  151. STATE fetching success error ready

  152. const RemoteDoggo = { Ready: 'Ready', Fetching: 'Fetching', Success: 'Success',

    Fail: 'Fail', }
  153. const ready = () => ({ status: RemoteDoggo.Ready }) const

    fetching = () => ({ status: RemoteDoggo.Fetching }) const success = value => ({ status: RemoteDoggo.Success, value }) const fail = message => ({ status: RemoteDoggo.Fail, message })
  154. const ready = () => ({ status: RemoteDoggo.Ready }) const

    fetching = () => ({ status: RemoteDoggo.Fetching }) const success = value => ({ status: RemoteDoggo.Success, value }) const fail = message => ({ status: RemoteDoggo.Fail, message })
  155. { dog: ready(), ... } { dog: fetching(), ... }

    { dog: success({ name: 'Tucker' }), ... } { dog: fail('Uh oh!'), ... }
  156. function App({ dog }) { switch (dog.status) { case RemoteDoggo.Ready:

    return <FindDog /> case RemoteDoggo.Fetching: return <Spinner /> case RemoteDoggo.Success: return <Dog dog={dog.value} /> case RemoteDoggo.Fail: return <Error message={dog.message} /> } }
  157. function App({ dog }) { switch (dog.status) { case RemoteDoggo.Ready:

    return <FindDog /> case RemoteDoggo.Fetching: return <Spinner /> case RemoteDoggo.Success: return <Dog dog={dog.value} /> case RemoteDoggo.Fail: return <Error message={dog.message} /> } }
  158. enum Status { Ready = 'Ready', Fetching = 'Fetching', Success

    = 'Success', Fail = 'Fail', } type RemoteDoggo = { kind: Status.Ready } | { kind: Status.Fetching } | { kind: Status.Success, value: Dog } | { kind: Status.Fail, message: string }
  159. function App({ dog }: AppProps): React.ReactElement { switch (dog.kind) {

    case Status.Ready: return <FindDog /> case Status.Fetching: return <Spinner /> case Status.Success: return <Dog dog={dog.value} /> // case Status.Fail: // return <Error message={dog.message} /> } }
  160. app.tsx:20:34 - error TS2366: Function lacks ending return statement and

    return type does not include 'undefined'. 20 function App({ dog }: AppProps): React.ReactElement { ~~~~~~~~~~~~~~~~~~ Found 1 error.
  161. Impossible state becomes impossible* Code becomes understandable *trademark Richard Feldman

  162. But wait, There’s MORE! AS SEEN ON U I

  163. Save to archive

  164. Save to archive A wild checkbox appears!

  165. Save to archive ✓

  166. Save to archive ?

  167. None
  168. We design APIs for callers So we should design UIs

    for users
  169. None
  170. hu·mane having or showing compassion or benevolence

  171. Save to archive ?

  172. Save to archive Save to inbox

  173. Save to archive Save to inbox

  174. Save to archive Save to inbox Verb labels = when

    will it happen?
  175. Saved to archive Saved to inbox Adjective labels = describe

    the final result.
  176. Goodbye, boolean?

  177. B A L A N C E

  178. EMPATHY

  179. Jeremy Fairbank @elpapapollo Slides: bit.ly/cm-bool