Chapter 6
Composing functions
6.1 Clarity
profit = (50+(20/10)*(200-price))*price-(140+(100*(50+((20/10)*(200-price)))))
profit = (salePrice) ->
(revenue salePrice) ? (cost salePrice)
revenue = (salePrice) ->
(numberSold salePrice) * salePrice
cost = (salePrice) ->
overhead + (numberSold salePrice) * costPrice
overhead = 140
costPrice = 100
numberSold = (salePrice) ->
50 + 20/10 * (200 - salePrice)
overhead = 140
costPrice = 100
numberSold = (salePrice) ->
50 + 20/10 * (200 - salePrice)
revenue = (salePrice) ->
(numberSold salePrice) * salePrice
cost = (salePrice) ->
overhead + (numberSold salePrice) * costPrice
profit = (salePrice) ->
(revenue salePrice) ? (cost salePrice)
Listing 6.1 Profit from selling PhotomakerExtreme
profit = (salePrice) ->
overhead = 140
costPrice = 100
numberSold = (salePrice) ->
50 + 20/10 * (200 - salePrice)
revenue = (salePrice) ->
(numberSold salePrice) * salePrice
cost = (salePrice) ->
overhead + (numberSold salePrice) * costPrice
(revenue salePrice) - (cost salePrice)
revenue salePrice
revenue(salePrice)
revenue salePrice ? cost salePrice
revenue(salePrice(-(cost(salePrice))));
revenue(salePrice) - cost(salePrice)
(revenue salePrice) - (cost salePrice)
profit = (overhead, costPrice, numberSold, salePrice) ->
revenue = (salePrice) ->
(numberSold salePrice) * salePrice
cost = (salePrice) ->
overhead + (numberSold salePrice) * costPrice
(revenue salePrice) ? (cost salePrice)
tenSold = -> 10
profit 10, 40, tenSold, 100
# 590
photoProOverhead = 140
photoProCostPrice = 100
photoProNumberSold = (salePrice) -> 50 + 20/10 * (200 - salePrice)
profit photoProOverhead, photoProCostPrice, photoProNumberSold, 162
# 7672
pixelKingOverhead = 140
pikelKingCostPrice = 100
pixelKingNumberSold = (salePrice) -> 50 + 20/10 * (200 - salePrice)
profit pixelKingOverhead, pixelKingCostPrice, pixelKingNumberSold, 200
# 14860
photoProProfit 162
# 7672
pixelKingProfit 200
# 14860
profit = (overhead, costPrice, numberSold) ->
revenue = (salePrice) ->
(numberSold salePrice) * salePrice
cost = (salePrice) ->
overhead + (numberSold salePrice) * costPrice
(salePrice) ->
(revenue salePrice) - (cost salePrice)
x1Overhead = 140
x1CostPrice = 100
x1NumberSold = (salePrice) -> 50 + 20/10 * (200 - salePrice)
x1Profit = profit x1Overhead, x1CostPrice, x1NumberSold
x1Profit 162
# 7672
http://www.agtronsemporium.com/api/product/photomakerExtreme
{
"PhotomakerExtreme": {
"manufacturer": "Photo Co",
"stock": 3,
"cost": 100,
"base": {
"price": 200,
"sold": 50
},
"reduction": {
"discount": 20,
"additionalSold": 10
}
}
}
Listing 6.2 How not to write a program
# Important: Do not write like this
http = require 'http'
url = require 'url'
{users, products} = require './db'
server = http.createServer (req, res) ->
path = url.parse(req.url).path
parts = path.split /\//
switch parts[1]
when 'profit'
res.writeHead 200, 'Content-Type': 'text/plain;charset=utf-8'
if parts[2] and /^[0-9]+$/gi.test parts[2]
price = parts[2]
profit = (50+(20/10)*(200-price))*
price-(140+(100*(50+((20/10)*(200-price)))))
res.end (JSON.stringify { profit: profit parts[2] })
else
res.end JSON.stringify { profit: 0 }
when 'user'
res.writeHead 200, 'Content-Type': 'text/plain;charset=utf-8'
if req.method is "GET"
if parts[2] and /^[a-z]+$/gi.test parts[2]
users.get parts[2], (error, user) ->
res.end JSON.stringify user, 'utf8'
else
users.all (error, users) ->
res.end JSON.stringify users, 'utf8'
else if parts[2] and req.method is "POST"
user = parts[2]
requestBody = ''
req.on 'data', (chunk) ->
requestBody += chunk.toString()
req.on 'end', ->
pairs = requestBody.split /&/g
decodedRequestBody = for pair in pairs
o = {}
splitPair = pair.split /\=/g
o[splitPair[0]] = splitPair[1]
o
users.set user, decodedRequestBody, ->
res.end 'success', 'utf8'
else
res.writeHead 404, 'Content-Type': 'text/plain;charset=utf-8'
res.end '404'
when 'product'
res.writeHead 200, 'Content-Type': 'text/plain;charset=utf-8'
if req.method is "GET"
products.get parts[2], (product) ->
res.end JSON.stringify product, 'utf8'
else if parts[2] and req.method is "POST"
product = parts[2]
requestBody = ''
req.on 'data', (chunk) ->
requestBody += chunk.toString()
req.on 'end', ->
pairs = requestBody.split /&/g
decodedRequestBody = for pair in pairs
o = {}
splitPair = pair.split /\=/g
o[splitPair[0]] = splitPair[1]
o
product.set user, decodedRequestBody, ->
res.end 'success', 'utf8'
requestBody = ''
req.on 'data', (chunk) ->
requestBody += chunk.toString()
req.on 'end', ->
decodedRequestBody = requestBody
res.end decodedRequestBody, 'utf8'
else
res.writeHead 404, 'Content-Type': 'text/plain;charset=utf-8'
res.end '404'
else
res.writeHead 200, 'Content-Type': 'text/html;charset=utf-8'
res.end 'The API'
server.listen 8080, '127.0.0.1'
# Important: Do not write like this
profit = (50+(20/10)*(200-price))*price-(140+(100*(50+((20/10)*(200-price)))))
profit = (overhead, costPrice, numberSold, salePrice) ->
revenue = (salePrice) ->
(numberSold salePrice) * salePrice
cost = (salePrice) ->
overhead + (numberSold salePrice) * costPrice
(revenue salePrice) ? (cost salePrice)
6.2 State and mutability
state = on
state = off
numberSold = (salePrice) ->
50 + 20/10 * (200 - salePrice)
numberSold = 0
calculateNumberSold = (salePrice) ->
numberSold = 50 + 20/10 * (200 - salePrice)
calculateNumberSold 220
# 10
calculateRevenue = (salePrice) ->
numberSold * salePrice
for price in [140..145]
calculateRevenue price
# [1400,1410,1420,1430,1440,1450]
for price in [140..145]
calculateNumberSold price
calculateRevenue price
# [23800,23688,23572,23425,23328,23200]
Listing 6.3 Local state and shared state
numberSold = 0
calculateNumberSold = (salePrice) ->
numberSold = 50 + 20/10 * (200 - salePrice)
calculateRevenue = (salePrice, callback) ->
callback numberSold * salePrice
revenueBetween = (start, finish) ->
totals = []
for price in [start..finish]
calculateNumberSold price
addToTotals = (result) ->
totals.push result
calculateRevenue price, addToTotals
totals
revenueBetween 140, 145
# [ 23800, 23688, 23572, 23452, 23328, 23200 ]
oneSecond = 1000
calculateRevenue = (callback) ->
setTimeout ->
callback numberSold * salePrice
, oneSecond
revenueBetween 140, 145
# []
numberSold = 0
calculateNumberSold = (salePrice) ->
numberSold = 50 + 20/10 * (200 - salePrice)
calculateRevenue = (salePrice, callback) ->
callback numberSold * salePrice
revenueBetween = (start, finish, callback) ->
totals = []
receivedResponses = 0
expectedResponses = 0
for price in [start..finish]
calculateNumberSold price
expectedResponses++
addToTotals = (result) ->
totals.push result
receivedResponses++
if receivedResponses == expectedResponses
callback totals
calculateRevenue price, addToTotals
numberSold = 0
calculateNumberSold = (salePrice) ->
numberSold = 50 + 20/10 * (200 - salePrice)
calculateRevenue = (salePrice, callback) ->
callback numberSold * salePrice
log = (message) ->
console.log message
numberSold = 'uh oh'
revenueBetween = (start, finish, callback) ->
totals = []
receivedResponses = 0
expectedResponses = 0
for price in [start..finish]
calculateNumberSold price
expectedResponses++
addToTotals = (result) ->
totals.push result
receivedResponses++
if receivedResponses == expectedResponses
callback totals
else
log 'waiting'
calculateRevenue price, addToTotals
revenueBetween 140, 145
# [ 22400, NaN, NaN, NaN, NaN, NaN ]
class Camera
overhead: -> 140
costPrice: -> 100
profit: (salePrice) ->
(@revenue salePrice) - (@cost salePrice)
numberSold: (salePrice) ->
50 + 20/10 * (200 - salePrice)
revenue: (salePrice) ->
(@numberSold salePrice) * salePrice
cost: (salePrice) ->
@overhead() + (@numberSold salePrice) * @costPrice()
phototaka500 = new Camera
phototaka500.profit 162
# 7672
class Camera
constructor: (@price) ->
calculateRevenue: ->
@revenue = (50 + (20 / 10) * (200 - @price)) * @price
calculateCost: ->
@cost = 140 + (100 * (50 + ((20 / 10) * (200 - @price))))
calculateProfit: ->
@calculateRevenue()
@calculateCost()
@profit = @revenue - @cost
phototaka500 = new Camera 162
phototaka500.calculateProfit()
console.log phototaka500.profit
# 7672
revenue = 0
cost = 0
sold = 0
calculateRevenue = (salePrice) ->
revenue = sold * salePrice
calculateCost = (salePrice) ->
cost = 140 + sold * 100
calculateNumberSold = (salePrice) ->
sold = 50 + 20/10 * (200 - salePrice)
calculateProfit = (salePrice) ->
calculateNumberSold salePrice
calculateRevenue salePrice
calculateCost salePrice
revenue ? cost
db.stock 'ijuf', (error, response) ->
# handling code here
Listing 6.4 State in program or external?
http = require 'http'
db = (require './db').stock
stock = 30
serverOne = http.createServer (req, res) ->
response = switch req.url
when '/purchase'
res.writeHead 200, 'Content-Type': 'text/plain;charset=utf8'
if stock > 0
stock = stock - 1
"Purchased! There are #{stock} left."
else
'Sorry! no stock left!'
else
res.writeHead 404, 'Content-Type': 'text/plain;charset=utf8'
'Go to /purchase'
res.end response
serverTwo = http.createServer (req, res) ->
purchase = (callback) ->
db.decr 'stock', (error, response) ->
if error
callback 0
else
callback response
render = (stock) ->
res.writeHead 200, 'Content-Type': 'text/plain;charset=utf8'
response = if stock > 0
"Purchased! There are #{stock} left."
else
'Sorry! no stock left'
res.end response
switch req.url
when '/purchase'
purchase render
else
res.writeHead 404, 'Content-Type': 'text/plain;charset=utf8'
res.end 'Go to /purchase'
serverOne.listen 9091, '127.0.0.1'
serverTwo.listen 9092, '127.0.0.1'
6.3 Abstraction
users.get parts[2], (error, user) ->
res.end JSON.stringify user, 'utf8'
products.get parts[2], (product) ->
res.end JSON.stringify product, 'utf8'
loadUserData = (user, callback) ->
users.get user, (data) ->
callback data
loadProductData = (product, callback) ->
products.get product, (data) ->
callback data
makeLoadData = (db) ->
(entry, callback) ->
db.get entry, (data) ->
callback data
makeSaveData = (type) ->
(entry, value, callback) ->
db.set entry, value, callback?()
loadUserData = makeLoadData 'user'
saveUserData = makeSaveData 'user'
loadAnythingData = makeLoadData 'anything'
makeDbOperator = (db) ->
(operation) ->
(entry, value=null, callback) ->
db[operation] entry, value, (error, data) ->
callback? error, data
makeDbOperator = (db) ->
(operation) ->
(entry, params...) ->
db[operation] entry, params...
loadProductData = (makeDbOperator 'product') 'get'
saveProductData = (makeDbOperator 'product') 'set'
saveProductData 'photonify1100', 'data for the photonify1100'
loadProductData 'photonify1100'
# 'data for the photonify1100'
Listing 6.5 The improved program
http = require 'http'
url = require 'url'
{products, users} = require './db'
withCompleteBody = (req, callback) ->
body = ''
req.on 'data', (chunk) ->
body += chunk.toString()
request.on 'end', -> callback body
paramsAsObject = (params) ->
pairs = params.split /&/g
result = {}
for pair in pairs
splitPair = pair.split /\=/g
result[splitPair[0]] = splitPair[1]
result
header = (response, status, contentType='text/plain;charset=utf-8') ->
response.writeHead status, 'Content-Type': contentType
httpRequestMatch = (request, method) -> request.method is method
isGet = (request) -> httpRequestMatch request, "GET"
isPost = (request) -> httpRequestMatch request, "POST"
render = (response, content) ->
header response, 200
response.end content, 'utf8'
renderAsJson = (response, object) -> render response, JSON.stringify object
notFound = (response) ->
header response, 404
response.end 'not found', 'utf8'
handleProfitRequest = (request, response, price, costPrice, overhead) ->
valid = (price) -> price and /^[0-9]+$/gi.test price
if valid price
renderAsJson response, profit: profit price, costPrice, overhead
else
renderAsJson response, profit: 0
makeDbOperator = (db) ->
(operation) ->
(entry, params...) ->
db[operation] entry, params...
makeRequestHandler = (load, save) ->
rendersIfFound = (response) ->
(error, data) ->
if error
notFound response
else
renderAsJson response, data
(request, response, name) ->
if isGet request
load name, rendersIfFound response
else if isPost request
withCompleteBody request, ->
save name, rendersIfFound response
else
notFound response
numberSold = (salePrice) ->
50 + 20/10 * (200 - salePrice)
profit = (salePrice, costPrice, overhead) ->
revenue = (salePrice) ->
(numberSold salePrice) * salePrice
cost = (salePrice) ->
overhead + (numberSold salePrice) * costPrice
(revenue salePrice) - (cost salePrice)
loadProductData = (makeDbOperator products) 'get'
saveProductData = (makeDbOperator products) 'set'
loadUserData = (makeDbOperator users) 'get'
saveUserData = (makeDbOperator users) 'set'
handleUserRequest = makeRequestHandler loadUserData, saveUserData
handleProductRequest = makeRequestHandler loadProductData, saveProductData
onProductDataLoaded = (error, data) ->
price = (parseInt (query.split '=')[1], 10)
handleProfitRequest request,response,price,data.costPrice,data.overhead
apiServer = (request, response) ->
path = url.parse(request.url).path
query = url.parse(request.url).query
parts = path.split /\//
switch parts[1]
when 'user'
handleUserRequest request, response, parts[2]
when 'product'if parts.length == 4 and /^profit/.test parts[3]
loadProductData parts[2], onProductDataLoaded
else
handleProductRequest request, response, parts[2]
else
notFound response
server = http.createServer(apiServer).listen 8080, '127.0.0.1'
exports.server = server
request GET 'All work and no play'
response 'makes Jack a dull boy' (5 ms)
request GET 'All work and no play'
response 'makes Jack a dull boy' (4 ms)
request GET 'All work and no play'
response 'makes Jack a dull boy' (2 ms)
request GET 'All work and no play'
response 'makes Jack a dull boy' (5 ms)
request GET 'All work and no play'
response 'makes Jack a dull boy' (5 ms)
makeDbOperator = (db) ->
(operation) ->
(entry, params...) ->
db[operation] entry, params...
productDataCache = Object.create null
loadProductData = (name, callback) ->
cachedCall = (makeDbOperator products) 'get'
if productDataCache.hasOwnProperty name
console.log 'cache hit'
console.log productDataCache[name]...
callback productDataCache[name]...
else
cachedCall name, (results...) ->
productDataCache[name] = results
withCachedCallback = (fn) ->
cache = Object.create null
(params...) ->
key = params[0]
callback = params[params.length - 1]
if key of cache
callback cache[key]...
else
paramsCopy = params[..]
paramsCopy[params.length-1] = (params...) ->
cache[key] = params
callback params...
fn paramsCopy...
loadProductData = withCachedCallback ((makeDbOperator products) 'get')
withExpiringCachedCallback = (fn, ttl) ->
cache = Object.create null
(params...) ->
key = params[0]
callback = params[params.length - 1]
if cache[key]?.expires > Date.now()
callback cache[key].entry...
else
paramsCopy = params[..]
paramsCopy[params.length - 1] = (params...) ->
console.log params
cache[key] =
entry: params
expires: Date.now() + ttl
console.log cache[key]
callback params...
fn paramsCopy...
factorial = (n) ->
if n is 0 then 1
else
n * (factorial n - 1)
factorial 0
# 1
factorial 4
# 24
factorial 5
# 120
logUserDataFor = (user) ->
users.get user, (error, data) ->
if error then console.log 'An error occurred'
else console.log 'Got the data'
logUserDataFor 'fred'
# 'Got the data'
logUserDataFor 'fred'
# 'An error occurred'
logUserDataFor = (user) ->
users.get user, (error, data) ->
if error then users.get user, (error, data) ->
if error then console.log 'An error occurred both times'
else 'Got the data (on the second attempt)'
else console.log 'Got the data'
logUserDataFor = (user) ->
dbRequest = ->
users.get user, (error, data) ->
if error then dbRequest()
else console.log 'Got the data'
dbRequest()
logUserDataFor 'fred'
# 'An error occurred'
# 'An error occurred'
# 'An error occurred'
# 'Got the data'
logUserDataFor = (user) ->
dbRequest = (attempt) ->
users.get user, (error, data) ->
if error and attempt < 5
setTimeout ->
(dbRequest attempt + 1), 1000
else console.log 'Got the data'
dbRequest()
advanceTime = (time, advanceBy) ->
new Date time*1 + advanceBy
retryFor = (duration, interval) ->
start = new Date
retry = (fn, finalCallback) ->
attempt = new Date
if attempt < (advanceTime start, duration)
proxyCallback = (error, data) ->
if error
console.log "Error: Retry in #{interval}"
setTimeout ->
retry fn, finalCallback
, interval
else
finalCallback error, data
fn proxyCallback
else
console.log "Gave up after #{duration}"
seconds = (n) ->
1000*n
getUserData = (user) ->
(callback) ->
users.get user, callback
getUserDataForFred = getUserData 'fred'
retryForFiveSeconds = (retryFor (seconds 5), (seconds 1))
retryForFiveSeconds getUserDataForFred, (error, data) ->
console.log data
Error: Retry in 1000
Error: Retry in 1000
Error: Retry in 1000
Success
memoryEater = ->
memoryEater()
memoryEater = -> memoryEater()
memoryEater()
# RangeError: Maximum call stack size exceeded
tenThousandCalls = (depth) ->
if depth < 10000
(tenThousandCalls depth + 1)
6.4 Combinators
profit = ->
tax = (amount) ->
amount / 3
netProfit = (products) ->
profits = (profit product) for product in products
profits.reduce (acc, p) -> acc + p
netProfitForProducts = netProfit products
taxForProducts = tax netProfitForProducts
userSpend = (user) ->
spend = 100
loyaltyDiscount = (spend) ->
if spend < 1000 then 0
else if spend < 5000 then 5
else if spend < 10000 then 10
else if spend < 50000 then 20
else if spend > 50000 then 40
fredSpend = userSpend fred
loyaltyDiscountForFred = loyaltyDiscount fredSpend
initialValue = 5
intermediateValue = firstFunction initialValue
finalValue = secondFunction intermediateValue
initialValue = 5
secondFunction (firstFunction initialValue)
netProfitForProducts = netProfit products
taxForProducts = tax netProfitForProducts
taxForProducts = tax (netProfit products)
taxForProducts = tax (netProfit products)
compose = (f, g) -> (x) -> f g x
taxForProducts = compose tax, netProfit
loyaltyDiscountForUser = compose loyaltyDiscount, userSpend
addFive = (x) -> x + 5
multiplyByThree = (x) -> x * 3
multiplyByThreeAndThenAddFive = compose addFive, multiplyByThree
multiplyByThreeAndThenAddFive 10
# 35
sale = (user, product) ->
auditLog "Sold #{product} to #{user}"
# Some other stuff happens here
refund = (user, product) ->
auditLog "Refund for #{product} to #{user}"
# Some other stuff happens here
auditedRefund = withAuditLog refund
refund = withAuditLog refund
before = (decoration) ->
(base) ->
(params...) ->
decoration params...
base params...
withAuditLog = before (params...) ->
auditLog params...
after = (decoration) ->
(base) ->
(params...) ->
result = base params...
decoration params...
result
openConnection()
doSomethingToTheDb()
doSomethingElseToTheDb()
closeConnection()
dbConnectionIsOpen = openConnection()
if dbConnectionIsOpen
doSomethingToTheDb()
doSomethingElseToTheDb()
closeConnection()
around = (decoration) ->
(base) ->
(params...) ->
callback = -> base params...
decoration ([callback].concat params)...
withOpenDb = around (dbActivity) ->
openDbConnection()
dbActivity()
closeDbConnection()
getUserData = withOpenDb (users) ->
users.get 'user123'
withOpenDb = around (dbActivity) ->
if openDbConnection()
dbActivity()
closeDbConnection()
class Robot
constructor: (@at=0) ->
position: ->
@at
move: (displacement) ->
@at += displacement
startEngine: -> console.log 'start engine'
stopEngine: -> console.log 'stop engine'
forward: ->
@startEngine()
@move 1
@stopEngine()
reverse: ->
@startEngine()
@move -1
@stopEngine()
class Robot
withRunningEngine = around (action) ->
@startEngine()
action()
@stopEngine()
constructor: (@at=0) ->
position: ->
@at
move: (displacement) ->
console.log 'move'
@at += displacement
startEngine: -> console.log 'start engine'
stopEngine: -> console.log 'stop engine'
forward: withRunningEngine ->
@move 1
reverse: withRunningEngine ->
@move -1
bender = new Robot
bender.forward()
bender.forward()
# TypeError: Object #<Object> has no method 'startEngine'
airplane =
startEngine: -> 'Engine started!'
withRunningEngine = (first, second) ->
@startEngine()
"#{first} then #{second}"
withRunningEngine 'Take-off', 'Fly'
# Object #<Object> has no method 'startEngine'
withRunningEngine.call airplane, 'Take-off', 'Fly'
'Take-off then Fly'
withRunningEngine.apply airplane, ['Take-off', 'Fly']
'Take-off then Fly'
Listing 6.6 Before, after,
and around
with function binding
before = (decoration) ->
(base) ->
(params...) ->
decoration.apply @, params
base.apply @, params
after = (decoration) ->
(base) ->
(params...) ->
result = base.apply @, params
decoration.apply @, params
result
around = (decoration) ->
(base) ->
(params...) ->
result = undefined
func = =>
result = base.apply @, params
decoration.apply @, ([func].concat params)
result
bender = new Robot 3
bender.forward()
# start engine
# move
# stop engine
# 4
bender.forward()
# start engine
# move
# stop engine
# 5
bender.reverse()
# start engine
# move
# stop engine
# 4
bender.position()
# 4
forward = (callback) ->
setTimeout callback, 1000
forward ->
forward ->
forward ->
forward ->
forward ->
console.log 'done!'
start = (callback) ->
console.log 'started'
setTimeout callback, 200
forward = (callback) ->
console.log 'moved forward'
setTimeout callback, 200
startThenForward = compose forward, start
startThenForward (res) ->
console.log res
# TypeError: undefined is not a function
composeAsync = (f, g) -> (x) -> g -> f x
startThenForward = composeAsync forward, start
startThenForward ->
console.log 'done'
# started
# moved forward
# done
Listing 6.7 Asynchronous before
and after
beforeAsync = (decoration) ->
(base) ->
(params..., callback) ->
result = undefined
applyBase = =>
result = base.apply @, (params.concat callback)
decoration.apply @, (params.concat applyBase)
result
afterAsync = (decoration) ->
(base) ->
(params..., callback) ->
decorated = (params...) =>
decoration.apply @, (params.concat -> (callback.apply @, params))
base.apply @, (params.concat decorated)
6.5 Summary