12 days of Christmas
Map a simple data structure to a complex one
Nested indexes describe the structure of the result, produced by a single (elided) use of Index At.
Amend lets us change items at depth in the result structure.
:fontawesome-solid-check-circle: Two code lines: no loops, no counters, no control structures.
Write a program that prints the lyrics of the Christmas carol “The Twelve Days of Christmas”
from Rosetta Code.1
Follow a python
Rosetta Code offers a Python solution.
gifts = '''
A partridge in a pear tree.
Two turtle doves
Three french hens
Four calling birds
Five golden rings
Six geese a-laying
Seven swans a-swimming
Eight maids a-milking
Nine ladies dancing
Ten lords a-leaping
Eleven pipers piping
Twelve drummers drumming'''.split('\n')
days = '''first second third fourth fifth
sixth seventh eighth ninth tenth
eleventh twelfth'''.split()
for n, day in enumerate(days, 1):
g = gifts[:n][::-1]
print(('\nOn the %s day of Christmas\nMy true love gave to me:\n' % day) +
'\n'.join(g[:-1]) +
(' and\n' + g[-1] if n > 1 else g[-1].capitalize()))
Straightforward translation into q:
GIFTS:(
"A partridge in a pear tree.";
"Two turtle doves";
"Three french hens";
"Four calling birds";
"Five golden rings";
"Six geese a-laying";
"Seven swans a-swimming";
"Eight maids a-milking";
"Nine ladies dancing";
"Ten lords a-leaping";
"Eleven pipers piping";
"Twelve drummers drumming")
DAYS:" "vs"first second third fourth fifth sixth",
" seventh eighth ninth tenth eleventh twelfth"
A useful convention: name consonants in upper case.
Now we need a function that returns verse x
, which we can iterate through til 12
.
The Python script is content to print the output. But printing is a side effect. We prefer functional style and returning a result. (It can default to stdout or pass as a value to some other process.)
Let’s generate the whole carol as a list of strings.
First line:
q){ssr["On the %s day of Christmas";"%s";]DAYS x}3
"On the fourth day of Christmas"
First two lines:
q){(ssr["On the %s day of Christmas";"%s";DAYS x];"My true love gave to me")}3
"On the fourth day of Christmas"
"My true love gave to me"
But we do not need the power of ssr
. We can just join strings.
q){("On the ",(DAYS x)," day of Christmas";"My true love gave to me")}3
"On the fourth day of Christmas"
"My true love gave to me"
And some gifts.
q){("On the ",(DAYS x)," day of Christmas";"My true love gave to me"),(x+1)#GIFTS}3
"On the fourth day of Christmas"
"My true love gave to me"
"A partridge in a pear tree."
"Two turtle doves"
"Three french hens"
"Four calling birds"
But not in that order.
q){("On the ",(DAYS x)," day of Christmas";"My true love gave to me"),reverse(x+1)#GIFTS}3
"On the fourth day of Christmas"
"My true love gave to me"
"Four calling birds"
"Three french hens"
"Two turtle doves"
"A partridge in a pear tree."
Almost. Except on the first day, the last line begins And a partridge. We can deal with this. A little conditional execution.
Conditional execution
Here is the first line expressed as the result of a Cond.
$[x;"And a partridge in a pear tree";"A partridge in a pear tree"]
We do not need to compare x
to zero. If it is zero, we get the second version of the line. But we already have the short version of the line.
We want to amend it.
$[x;"And a";"A"],1_"A partridge in a pear tree"
The second part of the conditional above is a no-op.
Better perhaps to say we may want to amend the line.
With a function that drops the first char and prepends "And a"
:
"And a", 1_ / a composition
We could use the Do iterator to apply it – zero or one times.
q)1("And a", 1_)\"A partridge"
"A partridge"
"And a partridge"
Now we do have to compare x
to 0. And cast the result to long.
("j"$x=0)("And a", 1_)/"A partridge"
Nothing here seems quite satisfactory. We shall revisit it. For now we shall prefer the slightly shorter and syntactically simpler Cond.
Apply At
We want to make the changes above, conditionally, to the last gift of the day. Happily, until we reverse the list, that is the first gift: index is 0.
q){("On the ",(DAYS x)," day of Christmas";"My true love gave to me"),
reverse @[;0;{y,1_x};$[x;"And a";"A"]](x+1)#GIFTS}0
"On the first day of Christmas"
"My true love gave to me"
"A partridge in a pear tree."
Here we have used the quaternary form of Amend At. The Reference gives its syntax as
@[d; i; v; vy]
@[; 0; {y,1_x}; $[x;"And a";"A"]]
d
-
The
d
argument is missing. It is the only argument missing, so we have a unary projection of Amend At. That makes the value ofd
the expression to its right:(x+1)#GIFTS
. The list of gifts, partridge first. i
-
0: we are amending the first item in the list. The partridge line.
v
-
This is the function to be applied to the partridge line. We are using the quaternary form of Amend At, so
v
is a binary. The partridge line is itsx
argument. Ourv
is{y,1_x}
. It will drop the first character of the partridge line and prepend the value of the fourth argument. vy
-
This the right argument of
v
: a choice between"And a"
and"A"
.
We need a blank line at the end of each verse.
q){("On the ",(DAYS x)," day of Christmas";"My true love gave to me"), reverse(enlist""),@[;0;{y,1_x};$[x;"And a";"A"]](x+1)#GIFTS}0
Put this into the script.
day:{("On the ",(DAYS x)," day of Christmas";"My true love gave to me"),
reverse(enlist""),@[;0;{y,1_x};$[x;"And a";"A"]](x+1)#GIFTS}
And run it.
q)1 "\n"sv raze day each til 12;
On the first day of Christmas
My true love gave to me
A partridge in a pear tree.
On the second day of Christmas
My true love gave to me
Two turtle doves
And a partridge in a pear tree.
..
On the twelfth day of Christmas
My true love gave to me
Twelve drummers drumming
Eleven pipers piping
Ten lords a-leaping
Nine ladies dancing
Eight maids a-milking
Seven swans a-swimming
Six geese a-laying
Five golden rings
Four calling birds
Three french hens
Two turtle doves
And a partridge in a pear tree.
Q eye for the scalar guy
Translating the Python solution worked, but we can do better. Much better. Start again.
Leave aside for now how the day changes at the beginning of each verse.
Set aside also the "And a"
on the first verse, and notice that only the first verse varies this way.
Suppose we construct each verse as a subset of the final stanza?
STANZA:( / final stanza
"On the twelfth day of Christmas";
"My true love gave to me:";
"Twelve drummers drumming";
"Eleven pipers piping";
"Ten lords a-leaping";
"Nine ladies dancing";
"Eight maids a-milking";
"Seven swans a-swimming";
"Six geese a-laying";
"Five golden rings";
"Four calling birds";
"Three french hens";
"Two turtle doves";
"And a partridge in a pear tree.";
"")
Nested indexes
Fifteen lines. For verse x
we want the first two and the last x+2
.
q)0 1,/:#\:[;til 15] -2 -til 12
0 1 13 14
0 1 12 13 14
0 1 11 12 13 14
0 1 10 11 12 13 14
0 1 9 10 11 12 13 14
0 1 8 9 10 11 12 13 14
0 1 7 8 9 10 11 12 13 14
0 1 6 7 8 9 10 11 12 13 14
0 1 5 6 7 8 9 10 11 12 13 14
0 1 4 5 6 7 8 9 10 11 12 13 14
0 1 3 4 5 6 7 8 9 10 11 12 13 14
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14
Indexing is atomic. Index STANZA
using Index At @
.
(Which we can elide to prefix syntax.)
q)show verses:STANZA 0 1,/:#\:[;til 15] -2 -til 12
("On the twelfth day of Christmas";"My true love gave to me:";"And a partridg..
("On the twelfth day of Christmas";"My true love gave to me:";"Two turtle dov..
("On the twelfth day of Christmas";"My true love gave to me:";"Three french h..
("On the twelfth day of Christmas";"My true love gave to me:";"Four calling b..
("On the twelfth day of Christmas";"My true love gave to me:";"Five golden ri..
("On the twelfth day of Christmas";"My true love gave to me:";"Six geese a-la..
("On the twelfth day of Christmas";"My true love gave to me:";"Seven swans a-..
("On the twelfth day of Christmas";"My true love gave to me:";"Eight maids a-..
("On the twelfth day of Christmas";"My true love gave to me:";"Nine ladies da..
("On the twelfth day of Christmas";"My true love gave to me:";"Ten lords a-le..
("On the twelfth day of Christmas";"My true love gave to me:";"Eleven pipers ..
("On the twelfth day of Christmas";"My true love gave to me:";"Twelve drummer..
A list. Each item is a list of strings. Nice.
Amend in depth
Now for those first lines.
Start with a function (in fact, a projection) that returns its left argument with"twelfth"
replaced by the right argument.
ssr[;"twelfth";]
q).[verses;(::;0)] / i.e. verses[;0]
"On the twelfth day of Christmas"
"On the twelfth day of Christmas"
"On the twelfth day of Christmas"
..
q).[verses;(::;0);ssr[;"twelfth";];DAYS]
("On the first day of Christmas";"My true love gave to me:";"And a partridge i..
("On the second day of Christmas";"My true love gave to me:";"Two turtle doves..
("On the third day of Christmas";"My true love gave to me:";"Three french hens..
("On the fourth day of Christmas";"My true love gave to me:";"Four calling bir..
("On the fifth day of Christmas";"My true love gave to me:";"Five golden rings..
("On the sixth day of Christmas";"My true love gave to me:";"Six geese a-layin..
("On the seventh day of Christmas";"My true love gave to me:";"Seven swans a-s..
("On the eighth day of Christmas";"My true love gave to me:";"Eight maids a-mi..
("On the ninth day of Christmas";"My true love gave to me:";"Nine ladies danci..
("On the tenth day of Christmas";"My true love gave to me:";"Ten lords a-leapi..
("On the eleventh day of Christmas";"My true love gave to me:";"Eleven pipers ..
("On the twelfth day of Christmas";"My true love gave to me:";"Twelve drummers..
Similarly we can fix verse 0, line 2.
q)first .[;0 2;"A",5_] .[verses;(::;0);ssr[;"twelfth";];DAYS]
"On the first day of Christmas"
"My true love gave to me:"
"A partridge in a pear tree."
""
Here we use the ternary form of Amend to apply a unary function to line 2 of verse 0.
The function we apply is a composition: "A",5_
.
Raze and print
Raze to a list of strings and print.
q)verses:STANZA 0 1,/:#\:[;til 15] -2 -til 12
q)-1 raze .[;0 2;"A",5_] .[verses;(::;0);ssr[;"twelfth";];DAYS];
On the first day of Christmas
My true love gave to me:
A partridge in a pear tree.
On the second day of Christmas
My true love gave to me:
Two turtle doves
And a partridge in a pear tree.
..
On the twelfth day of Christmas
My true love gave to me:
Twelve drummers drumming
Eleven pipers piping
Ten lords a-leaping
Nine ladies dancing
Eight maids a-milking
Seven swans a-swimming
Six geese a-laying
Five golden rings
Four calling birds
Three french hens
Two turtle doves
And a partridge in a pear tree.
Style
After defining the constants, we can write this solution as a single expression.
-1 raze .[;0 2;"A",5_] .[;(::;0);ssr[;"twelfth";];DAYS] STANZA 0 1,/:#\:[;til 15] -2 -til 12;
It is certainly good ‘pipeline style’: a sequence of unaries, each acting on the entire expression to its right. A few gratuitous spaces help to distinguish the steps, but more could be done.
-1 raze
.[;0 2;"A",5_] / tweak one line
.[;(::;0);ssr[;"twelfth";];DAYS] / number the verses
STANZA 0 1,/:#\:[;til 15] -2 -til 12; / compose 12 verses
Naming an intermediate result mitigates this.
verses:STANZA 0 1,/:#\:[;til 15] -2 -til 12
-1 raze .[;0 2;"A",5_] .[;(::;0);ssr[;"twelfth";];DAYS] verses;
- compose the verses
- tweak and print them
But here too there is a cost. Naming a value demands your reader remember it for use later. Respecting your reader’s attention, you keep such requests to a minimum. Naming a value to refer to it just once, in the following line, abuses that scarce attention. Conversely, it falsely implies the value will be needed later – somewhere currently out of view.
Legibility here pulls in different directions. There is no ‘correct’ resolution of these tensions, but you should keep them in mind when writing.
You will favour some over others, but each instance must be judged on its merits; no rule serves for all cases. Your preferences in this form your personal style. If working in a team, you will be wise to subordinate personal to ‘house’ style to assist a wider group of readers.
Exercises
-
Using string-search-and-replace to change the days looks like a sledgehammer to crack a nut. Can you find an alternative?
Answer
Replace the projection
ssr[;"twelfth";]
with{(7#x),y,14_x}
. -
Write an expression to generate all the first lines.
Answer
"On the ",x," day of Christmas"}each DAYS
Less obviously you can use an elision for the substitution. (A list with one item missing is a unary projection of
enlist
.)q)("On the ";;" day of Christmas")"first" "On the " "first" " day of Christmas" q)raze("On the ";;" day of Christmas")"first" "On the first day of Christmas"
For the whole list that gives
raze each("On the ";;" day of Christmas")each DAYS
Remembering that with unary functions
f each g each
can be composed as(f g::)each
gets us(raze("On the ";;" day of Christmas")::)each DAYS
which is interesting, but the lambda is preferable as syntactically simpler.
Review
We got a lot from seeing each verse as a subset of STANZA
.
We avoided lots of explicit iteration by generating a nested list of indexes, and indexing STANZA
with it.
Indexing is atomic, and returned us a nested list of strings, each item a verse.
We used an Each Right and an Each Left to generate the indexes; otherwise iteration was free.2
The structure was put together from integer indexes – lighter work than pushing strings around.
We used Amend at depth to fix the first lines, and again for the last line of the first verse.
Finally, raze
reduced the verses to a list of strings ready to print.
Great example of what you can get done with nested indexes.