CoffeeScript in Action (2014)
Appendix B. Answers to exercises
About the exercises
These exercises are provided for you to practice using CoffeeScript and also to spend some time reflecting on CoffeeScript. These two activities are an essential component of learning a new programming language. Attempting the exercises is more important than looking at the solutions.
Exercise 2.3.3
torch = price: 21
umbrella = {}
combinedCost = (torch.price || 0) + (umbrella.price || 0)
# 21
Exercise 2.4.4
animal = "crocodile"
collective = switch animal
when "antelope" then "herd"
when "baboon" then "rumpus"
when "badger" then "cete"
when "cobra" then "quiver"
when "crocodile" then "bask"
# bask
Exercise 2.5.3
animal = "cobra"
collective = switch animal
when "antelope" then "herd"
when "baboon" then "rumpus"
when "badger" then "cete"
when "cobra" then "quiver"
when "crocodile" then "bask"
"The collective of #{animal} is #{collective}"
# The collective of cobra is quiver
Exercise 2.6.5
animals = 'baboons badgers antelopes cobras crocodiles'
result = for animal in animals.split " "
collective = switch animal
when "antelopes" then "herd"
when "baboons" then "rumpus"
when "badgers" then "cete"
when "cobras" then "quiver"
when "crocodiles" then "bask"
"A #{collective} of #{animal}"
Exercises 3.1.5
countWords = (text) ->
words = text.split /[\s,]/
significantWords = (word for word in words when word.length > 3)
significantWords.length
everyOtherWord = (text) ->
words = text.split /[\s,]/
takeOther = for word, index in words
if index % 2 then ""
else word
takeOther.join(" ").replace /\s\s/gi, " "
Exercises 3.3.4
http = require 'http'
fs = require 'fs'
sourceFile = 'attendees'
fileContents = 'File not read yet.'
readSourceFile = ->
fs.readFile sourceFile, 'utf-8', (error, data) ->
if error
console.log error
else
fileContents = data
fs.watchFile sourceFile, readSourceFile
countWords = (text) ->
text.split(/,/gi).length
readSourceFile sourceFile
server = http.createServer (request, response) ->
response.end "#{countWords(fileContents)}"
server.listen 8080, '127.0.0.1'
Exercises 3.4.4
accumulate = (initial, items, accumulator) ->
total = initial
for item in items
total = accumulator total, item
total
sumFractions = (fractions) ->
accumulator = (lhs, rhs) ->
if lhs is '0/0'
rhs
else if rhs is '0/0'
lhs
else
lhsSplit = lhs.split /\//gi
rhsSplit = rhs.split /\//gi
lhsNumer = 1*lhsSplit[0]
lhsDenom = 1*lhsSplit[1]
rhsNumer = 1*rhsSplit[0]
rhsDenom = 1*rhsSplit[1]
if lhsDenom isnt rhsDenom
commonDenom = lhsDenom*rhsDenom
else
commonDenom = lhsDenom
sumNumer = lhsNumer*(commonDenom/lhsDenom) + rhsNumer*(commonDenom/rhsDenom)
"#{sumNumer}/#{commonDenom}"
accumulate '0/0', fractions, accumulator
console.log sumFractions ['2/6', '1/4']
# '14/24'
And the keep function:
keep = (arr, cond) ->
item for item in arr when cond item
Exercises 4.2.3
Adding edit to listing 4.2:
phonebook =
numbers:
hannibal: '555-5551'
darth: '555-5552'
hal9000: 'disconnected'
freddy: '555-5554'
'T-800': '555-5555'
list: ->
"#{name}: #{number}" for name, number of @numbers
add: (name, number) ->
if not (name of @numbers)
@numbers[name] = number
else
"#{name} already exists"
edit: (name, number) ->
if name of @numbers
@numbers[name] = number
else
"#{name} not found"
get: (name) ->
if name of @numbers
"#{name}: #{@numbers[name]}"
else
"#{name} not found"
console.log "Phonebook. Commands are add, get, edit, list, and exit."
process.stdin.setEncoding 'utf8'
stdin = process.openStdin()
stdin.on 'data', (chunk) ->
args = chunk.split ' '
command = args[0].trim()
name = args[1].trim() if args[1]
number = args[2].trim() if args[2]
switch command
when 'add'
res = phonebook.add(name, number) if name and number
console.log res
when 'get'
console.log phonebook.get(name) if name
when 'edit'
console.log phonebook.edit(name, number) if name and number
when 'list'
console.log phonebook.list()
when 'exit'
process.exit 1
Setting properties on an object:
css = (element, styles) ->
element.style ?= {}
for key, value of styles
element.style[key] = value
class Element
div = new Element
css div, width: 10
div.style.width
# 10
Exercise 4.6.3
The original music device is
cassette =
title: "Awesome songs. To the max!"
duration: "10:34"
released: "1988"
track1: "Safety Dance - Men Without Hats"
track2: "Funkytown - Lipps, Inc"
track3: "Electric Avenue - Eddy Grant"
track4: "We built this city - Starship"
The music device was created from it:
musicDevice = Object.create cassette
Creating another one from the first is the same:
secondMusicDevice = Object.create musicDevice
Changes to either the original cassette or the music device will be visible on the second music device:
cassette.track5 = "Hello - Lionel Richie"
secondMusicDevice.track5
# "Hello - Lionel Richie"
musicDevice.track6 = "Mickey - Toni Basil"
secondMusicDevice.track6
# "Mickey - Toni Basil"
Multiple prototype references like this are called the prototype chain. You’ll learn more about it in chapter 5.
Exercise 4.7.2
views =
excluded: []
pages: {}
clear: ->
@pages = {}
increment: (key) ->
unless key in @excluded
@pages[key] ?= 0
@pages[key] = @pages[key] + 1
ignore: (page) ->
@excluded = @excluded.concat page
total: ->
sum = 0
for own page, count of @pages
sum = sum + count
sum
Exercises 4.8.3
class GranTurismo
constructor: (options) ->
@options = options
summary: ->
("#{key}: #{value}" for key, value of @options).join "\n"
options =
wheels: 'phat'
dice: 'fluffy'
scruffysGranTurismo = new GranTurismo options
scruffysGranTurismo.summary()
# wheels: phat
# dice: fluffy
The constructor could use the shorthand for arguments:
class GranTurismo
constructor: (@options) ->
summary: ->
("#{key}: #{value}" for key, value of @options).join "\n"
This is equivalent to the first version.
Exercise 5.3.3
Here are Product and Camera classes based on listing 5.4 with an alphabetical class method added to the Camera class:
class Product
# any implementation of Product
class Camera extends Product
cameras = []
@alphabetical = ->
cameras.sort (a, b) -> a.name > b.name
constructor: ->
all.push @
super
The Camera class can keep an array of all instances for alphabetical just as the Product class kept an array of products for find. The Camera constructor uses super to ensure the Product constructor is also invoked so that Product.find doesn’t break.
Exercise 5.8.1
Applying some basic class techniques to the server-side application helps to increase clarity:
fs = require 'fs'
http = require 'http'
url = require 'url'
coffee = require 'coffee-script'
class ShopServer
constructor: (@host, @port, @shopData, @shopNews) ->
@css = ''
fs.readFile './client.css', 'utf-8', (err, data) =>
if err then throw err
@css = data
readClientScript: (callback) ->
script = "./client.coffee"
fs.readFile script, 'utf-8', (err, data) ->
if err then throw err
callback data
headers: (res, status, type) ->
res.writeHead status, 'Content-Type': "text/#{type}"
renderView: ->
"""
<!doctype html>
<head>
<title>Agtron's Emporium</title>
<link rel='stylesheet' href='/css/client.css' />
</head>
<body>
<div class='page'>
<h1>----Agtron’s Emporium----</h1>
<script src='/js/client.js'></script>
</div>
</body>
</html>
"""
handleClientJs: (path, req, res) ->
@headers res, 200, 'javascript'
writeClientScript = (script) ->
res.end coffee.compile(script)
@readClientScript writeClientScript
handleClientCss: (path, req, res) ->
@headers res, 200, 'css'
res.end @css
handleImage: (path, req, res) ->
fs.readFile ".#{path}", (err, data) =>
if err
@headers res, 404, 'image/png'
res.end()
else
@headers res, 200, 'image/png'
res.end data, 'binary'
handleJson: (path, req, res) ->
switch path
when '/json/list'
@headers res, 200, 'json'
res.end JSON.stringify(@shopData)
when '/json/list/camera'
@headers res, 200, 'json'
camera = @shopData.camera
res.end JSON.stringify(camera)
when '/json/news'
@headers res, 200, 'json'
res.end JSON.stringify(@shopNews)
else
@headers res, 404, 'json'
res.end JSON.stringify(status: 404)
handlePost: (path, req, res) ->
category = /^\/json\/purchase\/([^/]*)\/([^/]*)$/.exec(path)?[1]
item = /^\/json\/purchase\/([^/]*)\/([^/]*)$/.exec(path)?[2]
if category? and item? and data[category][item].stock > 0
data[category][item].stock -= 1
@headers res, 200, 'json'
res.write JSON.stringify
status: 'success',
update: data[category][item]
else
res.write JSON.stringify
status: 'failure'
res.end()
handleGet: (path, req, res) ->
if path is '/'
@headers res, 200, 'html'
res.end @renderView()
else if path.match /\/json/
@handleJson path, req, res
else if path is '/js/client.js'
@handleClientJs path, req, res
else if path is '/css/client.css'
@handleClientCss path, req, res
else if path.match /^\/images\/(.*)\.png$/gi
@handleImage path, req, res
else
@headers res, 404, 'html'
res.end '404'
start: ->
@httpServer = http.createServer (req, res) =>
path = url.parse(req.url).pathname
if req.method == "POST"
@handlePost path, req, res
else
@handleGet path, req, res
@httpServer.listen @port, @host, =>
console.log "Running at #{@host}:#{@port}"
stop: ->
@httpServer?.close()
data = require('./data').all
news = require('./news').all
shopServer = new ShopServer '127.0.0.1', 9999, data, news
shopServer.start()
There are some further opportunities in the preceding program for encapsulation, but for a program of little over 100 lines you’d likely find any more to be overkill.
Exercises 7.2.5
Your first thought might be to do something like this:
swapPairs = (array) ->
for index in array by 2
[first, second] = array[index-1..index]
[second, first]
This is close, but you’ll end up with an array of arrays:
swapPairs([3,4,3,4,3,4])
# [ [ 4, 3 ], [ 4, 3 ], [ 4, 3 ] ]
swapPairs([1,2,3,4,5,6])
# [ [ 2, 1 ], [ 4, 3 ], [ 6, 5 ] ]
Solve this by using the array concat method with rest:
swapPairs = (array) ->
reversedPairs = for index in array by 2
[first, second] = array[index-1..index]
[second, first]
[].concat reversedPairs...
swapPairs([3,4,3,4,3,4])
# [ 4, 3, 4, 3, 4, 3 ]
swapPairs([1,2,3,4,5,6])
# [ 2, 1, 4, 3, 6, 5 ]
For the second exercise, use a combination of rest, array, and object destructuring:
phoneDirectory =
A: [
name: 'Abe'
phone: '555 1110'
,
name: 'Andy'
phone: '555 1111'
,
name: 'Alice'
phone: '555 1112'
]
B: [
name: 'Bam'
phone: '555 1113'
]
lastNumberForLetter = (letter, directory) ->
[..., lastForLetter] = directory[letter]
{phone} = lastForLetter
phone
lastNumberForLetter 'A', phoneDirectory
# 555 1112
Exercise 10.4.4
Suppose the Tracking class and http object are as follows:
class Tracking
constructor: (prefs, http) ->
@http = http
start: ->
@http.listen()
http =
listen: ->
A potential double function follows:
double = (original) ->
mock = {}
for key, value of original
if value.call?
do ->
stub = ->
stub.called = true
mock[key] = stub
mock
This double function returns a mock version of the original object. It has all the same method names, but the methods themselves are just empty functions that remember if they’ve been called or not.
In other circumstances your test might call for a spy instead. When you spy on an object, any method calls still occur on the original object but are seen by the spy. A double function that returns a spy follows:
double = (original) ->
spy = Object.create original
for key, value of original
if value.call?
do ->
originalMethod = value
spyMethod = (args...) ->
spyMethod.called = true
originalMethod args...
spy[key] = spyMethod
spy
Depending on the test framework you’re using, there may be both mocking and spying libraries provided for you. The pros and cons of using mocks, spies, and other testing techniques aren’t covered here.
That’s it for the exercises. Happy travels.