Cory Brown
@uniqname
Nav

Why yes, we are hiring. (Please come talk to me after)

Lecture 👎

Discussion 👍

The biggest problem in the development and maintenance of large-scale software systems is complexity.

Out of the Tar Pit: Ben Moseley, Peter Marks; February 6, 2006

The major contributor to this complexity in many systems is the handling of state

Out of the Tar Pit: Ben Moseley, Peter Marks; February 6, 2006

state

const sku = scan()

const item = inventory[sku]

let displayPrice = ''

if (item) {
  displayPrice = item.price
}

displayPrice = displayPrice.replace('$', '')

let price = Number(displayPrice)

let priceInCents = price * 100

let totalPriceInCents = priceInCents * TAX_RATE

totalPriceInCents = Math.floor(totalPriceInCents)

const totalPrice = totalPriceInCents / 100

const totalDisplayPrice = `$${totalPrice}`

return totalDisplayPrice

Every variable or method creation amounts to the introduction of state.

The more variables, the more state.

state

const sku = scan()

const item = inventory[sku]

let displayPrice = ''

if (item) {
  displayPrice = item.price
}

displayPrice = displayPrice.replace('$', '')

let price = Number(displayPrice)

let priceInCents = price * 100

let totalPriceInCents = priceInCents * TAX_RATE

totalPriceInCents = Math.floor(totalPriceInCents)

const totalPrice = totalPriceInCents / 100

const totalDisplayPrice = `$${totalPrice}`

return totalDisplayPrice

The more variables are mutated, the greater the complexity that state introduces

state

let currentState = ''
let location = ''

const app = () => {

  const reinitialize = () => {
    currentState = ''
  }

  const fetchDataForPage = (location) =>
   dataFetcher(location)
    .then((data) => {
      currentState = data
    })
    .catch(() => {
      currentState = 'Error!'
    })

  document.onPopState((locationState) => {
    reinitialize()
    location = locationState.path
    fetchDataForPage(location)
      .finally(() => render(currentState))
  })
}

app()

The broader the scope of the variables the greater the complexity of the state.

state

document.onPopState(({ path }) =>
  dataFetcher(path)
  .then(render)
  .catch(render)
)

Conversely, The narrower the scope of the variables the less the complexity of the state.

&

Bricks

Mortar

Business requirement

When an item's barcode is scanned, the price -- including tax -- should be displayed to the customer.

Technical requirement

Use the guid received from scanning the item to look up the price of the item from our product database. 

 

Apply tax to the product price and display updated result.

tasks

  •   scan the bar code to get a guid
  •   use the guid to lookup the item information
  •   get the price property from the item
  •   convert the price from a string to a number
  •   calculate the tax for the item
  •   add the tax to the item's price
  •   convert the number price to a display price
  •   show the final display price to the customer

 

implementation

const sku = await scan()

const item = await products.findById(sku)

let displayPrice = ''

if (item) {
  displayPrice = item.price
}

displayPrice = displayPrice.replace('$', '')

let price = Number(displayPrice)

let priceInCents = price * 100

let totalPriceInCents = priceInCents * TAX_RATE

totalPriceInCents = Math.floor(totalPriceInCents)

const totalPrice = totalPriceInCents / 100

const totalDisplayPrice = `$${totalPrice}`

return totalDisplayPrice

Perhaps it looks something like this.

Pipelines

pseudo code

  scan -> 
  lookup -> 
  extractPriceString ->
  parseAsNumber -> 
  convertToCents -> 
  addTax -> 
  convertToDollars ->
  convertToDisplayString

pipeline

pipe(
  (sku) => inventory[sku],
  (item) => item.price,
  (displayPrice) => displayPrice.replace('$', ''),
  (Number),
  (price) => price * 100,
  (price) => Math.floor(price * TAX_RATE),
  (totalCents) => totalCents / 100,
  (total) => `$${total}`
)(scan())

Each piece of state is scoped to one, very simple function.

No piece of state spans more than one function.

Each piece of state ceases to exist at the conclusion of its function body.

pipeline

pipe(
  (sku) => inventory[sku],
  (item) => item.price,
  (displayPrice) => displayPrice.replace('$', ''),
  (Number),
  (price) => price * 100,
  (price) => Math.floor(price * TAX_RATE),
  (totalCents) => totalCents / 100,
  (total) => `$${total}`
)(scan())

No gap can exist from one task to another. There is no opportunity for state manipulation to happen.

In a pipeline, the flow of data is consistent and uninterrupted.

State is contained and minimized.

The many faces of pipelines

dot chain with arrays

[scan()]
  .map((sku) => inventory[sku])
  .map((item) => item.price)
  .map((displayPrice) => Number(displayPrice.replace('$', '')))
  .map((price) => price * 100)
  .map((price) => Math.floor(price * TAX_RATE))
  .map((totalCents) => totalCents / 100)
  .map((total) => `$${total}`)

dot chain with functors

Box(scan())
  .map((sku) => inventory[sku])
  .map((item) => item.price)
  .map((displayPrice) => Number(displayPrice.replace('$', '')))
  .map((price) => price * 100)
  .map((price) => Math.floor(price * TAX_RATE))
  .map((totalCents) => totalCents / 100)
  .map((total) => `$${total}`)

dot chain with promises

scan()
  .then((sku) => inventory[sku])
  .then((item) => item.price)
  .then((displayPrice) => Number(displayPrice.replace('$', '')))
  .then((price) => price * 100)
  .then((price) => Math.floor(price * TAX_RATE))
  .then((totalCents) => totalCents / 100)
  .then((total) => `$${total}`)

composition with `pipe`

pipe(
  (sku) => inventory[sku],
  (item) => item.price,
  (displayPrice) => Number(displayPrice.replace('$', '')),
  (price) => price * 100,
  (price) => Math.floor(price * TAX_RATE),
  (totalCents) => totalCents / 100,
  (total) => `$${total}`
)(scan())

composition with `compose`

compose(
  (total) => `$${total}`,
  (totalCents) => totalCents / 100,
  (price) => Math.floor(price * TAX_RATE),
  (price) => price * 100,
  (displayPrice) => Number(displayPrice.replace('$', '')),
  (item) => item.price,
  (sku) => inventory[sku],
)(scan())

composition with
`pipeline operator`[1]

scan()
  |> await,
  |> inventory.findById(sku),
  |> await,
  |> (({ price }) => price),
  |> ((displayPrice) => Number(displayPrice.replace('$', ''))),
  |> ((price) => price * 100),
  |> ((price) => Math.floor(price * TAX_RATE)),
  |> ((totalCents) => totalCents / 100),
  |> ((total) => `$${total}`)

composition with
`pipeline operator`[2]

scan()
  |> await #,
  |> inventory.getById,
  |> #.price,
  |> Number(#.replace('$', '')),
  |> # * 100,
  |> Math.floor(# * TAX_RATE),
  |> # / 100,
  |> `$${#}`

Other closely related contributors are code volume, and explicit concern with the flow of control through the system.

Out of the Tar Pit: Ben Moseley, Peter Marks; February 6, 2006

Those are topics for another discussion.

Pipelines

By Cory Brown

Pipelines

  • 983