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
- 1,124