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

Connect.Tech 2019: Solving the Boolean Identity Crisis

Connect.Tech 2019: 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. See examples where booleans obscure the meaning of code, make code harder to maintain, and hinder usability for your teammates and users. You will learn how to harness techniques from functional programming languages in the world of JavaScript to write clearer code. Most importantly, you will learn how to place usability at the forefront of the APIs and UIs you build.

Jeremy Fairbank

October 17, 2019
Tweet

More Decks by Jeremy Fairbank

Other Decks in Programming

Transcript

  1. Jeremy Fairbank
    @elpapapollo

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  4. {
    dog: null,
    }

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  7. {
    fetching: false,
    success: false,
    dog: null,
    error: true,
    errorMessage: 'Ruh roh!',
    }

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  10. if (props.error) {
    ...
    } else if (props.fetching) {
    ...
    } else if (props.success) {
    ...
    } else {
    ...
    }

    View full-size slide

  11. STATE
    fetching
    success error
    ready

    View full-size slide

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

    View full-size slide

  13. The Problem with Booleans…

    View full-size slide

  14. – Robert Harper
    “There is no information carried
    by a Boolean beyond its value,
    and that’s the rub.”

    View full-size slide

  15. binary data type with
    no inherent meaning
    boolean

    View full-size slide

  16. bookFlight('ATL', true)
    ?

    View full-size slide

  17. bookFlight('ATL', true)
    ?
    Offloading abstraction to our
    memory instead of the code.

    View full-size slide

  18. bookFlight('ATL', true, false, true)
    ? ? ?

    View full-size slide

  19. Boolean Algebra
    George Boole
    x ∧ y
    x ∨ y
    x ∨ (y ∧ z)
    ¬x

    View full-size slide

  20. Boolean Values
    true && true
    true || false
    true || (true && false)
    !true

    View full-size slide

  21. Propositional
    Logic

    View full-size slide

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

    View full-size slide

  23. Premise 1: If it’s raining then it’s cloudy.
    Premise 2: It’s raining.
    Conclusion: It’s cloudy.
    PROPOSITIONS

    View full-size slide

  24. prop·o·si·tion
    a statement that expresses a concept
    that can be true or false

    View full-size slide

  25. Boolean ≠ Proposition
    A proposition, p, that is true is not the
    same as saying p is equal to true.

    View full-size slide

  26. LOSS OF INTENT

    View full-size slide

  27. LOSS OF INFORMATION

    View full-size slide

  28. bookFlight('ATL', true)
    ?

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  31. function bookFlight(airport, isPremium) {
    if (isPremium) {
    ...
    } else {
    ...
    }
    }
    Regular customer?

    View full-size slide

  32. const OpacitySlider = createSlider(true)
    const VolumeSlider = createSlider(false)
    const App = () => (




    )
    ?

    View full-size slide

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

    View full-size slide

  34. const createSlider = isHorizontal => () => {
    if (isHorizontal) {
    ...
    } else {
    ...
    }
    }
    Implicit
    vertical slider

    View full-size slide

  35. – Robert Martin
    “Boolean arguments loudly
    declare that the function does
    more than one thing. They are
    confusing and should be
    eliminated.”

    View full-size slide

  36. rotateFuelRod(fuelRod, 30, true)
    rotateControlRod( )
    ?

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  41. bookFlight('ATL', true, false, true)
    ? ? ?

    View full-size slide

  42. function bookFlight(airport, isPremium, hasCheckedLuggage, preferWindow) {
    const luggageCost = hasCheckedLuggage
    ? ...
    : ...
    if (isPremium) {
    if (hasCheckedLuggage) {
    ...
    } else {
    ...
    }
    } else if (hasCheckedLuggage) {
    ...
    } else {
    ...
    }
    }

    View full-size slide

  43. – 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.”

    View full-size slide

  44. Write code for humans
    Not computers

    View full-size slide

  45. bookFlight('ATL', 'Premium')

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  48. bookFlight('ATL', 'premium')
    bookFlight('ATL', 'economical')
    bookFlight('ATL', '')
    Primitive Obsession Revisited

    View full-size slide

  49. Represent a finite
    domain

    View full-size slide

  50. type CustomerType
    = Premium
    | Regular

    View full-size slide

  51. const CustomerType = {
    Premium: 'Premium',
    Regular: 'Regular',
    }
    bookFlight('ATL', CustomerType.Premium)
    bookFlight('ATL', CustomerType.Regular)

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  54. enum CustomerType {
    Premium = 'Premium',
    Regular = 'Regular',
    }
    bookFlight('ATL', CustomerType.Premium)
    bookFlight('ATL', CustomerType.Regular)

    View full-size slide

  55. function bookFlight(
    airport: string,
    customerType: CustomerType,
    ): Promise {
    switch (customerType) {
    case CustomerType.Premium:
    ...
    case CustomerType.Regular:
    ...
    }
    }

    View full-size slide

  56. function bookFlight(
    airport: string,
    customerType: CustomerType,
    ): Promise {
    switch (customerType) {
    case CustomerType.Premium:
    ...
    case CustomerType.Regular:
    ...
    }
    }

    View full-size slide

  57. function bookFlight(
    airport: string,
    customerType: CustomerType,
    ): Promise {
    switch (customerType) {
    case CustomerType.Premium:
    ...
    case CustomerType.Regular:
    ...
    }
    }

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  65. function rotateFuelRod(
    fuelRod: FuelRod,
    angle: Angle,
    ): FuelRod {
    switch (angle.kind) {
    case AngleType.Degrees:
    ...
    case AngleType.Radians:
    ...
    }
    }

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  69. rotateInDegrees(fuelRod, 30)
    rotateInRadians(controlRod, Math.PI)

    View full-size slide

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

    View full-size slide

  71. 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)
    }
    }

    View full-size slide

  72. matchingStrings(
    Case.Insensitive,
    'foo',
    ['FOOBAR', 'foobar'],
    )
    matchingStrings(
    Case.Sensitive,
    'foo',
    ['FOOBAR', 'foobar'],
    )

    View full-size slide

  73. 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())
    }
    })

    View full-size slide

  74. 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())
    }
    })

    View full-size slide

  75. 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())
    }
    })

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  82. const bookPremiumCustomer = () => { ... }
    const bookRegularCustomer = () => { ... }
    bookFlight('ATL', bookPremiumCustomer)
    bookFlight('ATL', bookRegularCustomer)

    View full-size slide

  83. Make APIs
    UNDERSTANDABLE and
    CONVENIENT

    View full-size slide

  84. Function
    True
    False

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  89. function getTime(person) {
    if (doYouKnowTheTime(person)) {
    return tellMeTheTime(person)
    } else {
    return tellMeTheTime(person)
    }
    }

    View full-size slide

  90. Boolean Blindness

    View full-size slide

  91. – Conor McBride
    “To make use of a Boolean you
    have to know its provenance so
    that you can know what it
    means.”

    View full-size slide

  92. Get your organic,
    homegrown
    booleans from
    Rhode Island!
    Providence, RI

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  96. function divisionResult(numerator, denominator) {
    if (canDivide(denominator)) {
    return `The result is ${numerator / denominator}`
    } else {
    return `The result is ${numerator / denominator}`
    }
    }

    View full-size slide

  97. – Dan Licata
    “Boolean tests let you look,
    options let you see.”

    View full-size slide

  98. Alternative
    Return Values

    View full-size slide

  99. type Maybe a
    = Nothing
    | Just a

    View full-size slide

  100. type Maybe a
    = Nothing
    | Just a

    View full-size slide

  101. type Maybe a
    = Nothing
    | Just a

    View full-size slide

  102. type Maybe a
    = Nothing
    | Just a

    View full-size slide

  103. 42
    Just 42
    "Hi"
    Just "Hi"

    View full-size slide

  104. Just 42 [42]
    Nothing [ ]

    View full-size slide

  105. 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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  110. whatTimeIsIt(person)
    .map(time => ...)
    .unwrapOr(...)

    View full-size slide

  111. import { Result } from 'true-myth'
    Result.ok(42) // Ok 42
    Result.err('Uh oh') // Err 'Uh oh'

    View full-size slide

  112. const divide = (numerator, denominator) =>
    denominator === 0
    ? Result.err('Divide by zero')
    : Result.ok(numerator / denominator)
    const divisionResult = (numerator, denominator) =>
    divide(numerator, denominator)
    .mapOrElse(
    error => `Could not divide: ${error}`,
    result => `The result is ${result}`,
    )

    View full-size slide

  113. const divide = (numerator, denominator) =>
    denominator === 0
    ? Result.err('Divide by zero')
    : Result.ok(numerator / denominator)
    const divisionResult = (numerator, denominator) =>
    divide(numerator, denominator)
    .mapOrElse(
    error => `Could not divide: ${error}`,
    result => `The result is ${result}`,
    )

    View full-size slide

  114. const divide = (numerator, denominator) =>
    denominator === 0
    ? Result.err('Divide by zero')
    : Result.ok(numerator / denominator)
    const divisionResult = (numerator, denominator) =>
    divide(numerator, denominator)
    .mapOrElse(
    error => `Could not divide: ${error}`,
    result => `The result is ${result}`,
    )

    View full-size slide

  115. See with Special Types

    View full-size slide

  116. Back to State
    {
    fetching: false,
    success: true,
    dog: { name: "Tucker" },
    error: False,
    errorMessage: "",
    }

    View full-size slide

  117. if (props.error) {
    ...
    } else if (props.fetching) {
    ...
    } else if (props.success) {
    ...
    } else {
    ...
    }

    View full-size slide

  118. {
    fetching: true,
    success: true,
    dog: { name: "Tucker" },
    error: true,
    errorMessage: "Uh oh!",
    }

    View full-size slide

  119. https://youtu.be/IcgmSRJHu_8

    View full-size slide

  120. STATE
    fetching
    success error
    ready

    View full-size slide

  121. const RemoteDoggo = {
    Ready: 'Ready',
    Fetching: 'Fetching',
    Success: 'Success',
    Fail: 'Fail',
    }

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  124. { dog: ready(), ... }
    { dog: fetching(), ... }
    { dog: success({ name: 'Tucker' }), ... }
    { dog: fail('Uh oh!'), ... }

    View full-size slide

  125. function App({ dog }) {
    switch (dog.status) {
    case RemoteDoggo.Ready:
    return
    case RemoteDoggo.Fetching:
    return
    case RemoteDoggo.Success:
    return
    case RemoteDoggo.Fail:
    return
    }
    }

    View full-size slide

  126. function App({ dog }) {
    switch (dog.status) {
    case RemoteDoggo.Ready:
    return
    case RemoteDoggo.Fetching:
    return
    case RemoteDoggo.Success:
    return
    case RemoteDoggo.Fail:
    return
    }
    }

    View full-size slide

  127. 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 }

    View full-size slide

  128. function App({ dog }: AppProps): React.ReactElement {
    switch (dog.kind) {
    case Status.Ready:
    return
    case Status.Fetching:
    return
    case Status.Success:
    return
    // case Status.Fail:
    // return
    }
    }

    View full-size slide

  129. 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.

    View full-size slide

  130. Impossible state becomes impossible*
    Code becomes understandable
    *trademark Richard Feldman

    View full-size slide

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

    View full-size slide

  132. Save to archive

    View full-size slide

  133. Save to archive
    A wild checkbox appears!

    View full-size slide

  134. Save to archive

    View full-size slide

  135. Save to archive
    ?

    View full-size slide

  136. We design APIs for callers
    So we should design UIs for users

    View full-size slide

  137. hu·mane
    having or showing compassion or
    benevolence

    View full-size slide

  138. Save to archive
    ?

    View full-size slide

  139. Save to archive
    Save to inbox

    View full-size slide

  140. Save to archive
    Save to inbox

    View full-size slide

  141. Save to archive
    Save to inbox
    Verb labels = when will it happen?

    View full-size slide

  142. Saved to archive
    Saved to inbox
    Adjective labels = describe the final result.

    View full-size slide

  143. Goodbye, boolean?

    View full-size slide

  144. B A L A N C E

    View full-size slide

  145. Jeremy Fairbank
    @elpapapollo
    Slides: bit.ly/ct-bool

    View full-size slide