Skip to content

12 days of Christmas

Carol singers

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]
Let’s break ours down accordingly.

@[; 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 of d 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 its x argument. Our v 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";]
Index finds us all the first lines.
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"
..
Making that a quaternary gives us Amend.
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..
Notice that all the iteration we need is implicit – built in to Amend.

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;
That might impress your reader (most often a future you) but does it help her? Or just daunt her?

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
Setting each step on its own line helps here but at a cost: the lines are evaluated ‘in reverse order’, i.e. last first, against the familiar direction of ‘reading gravity’.

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;
This helps, because verses is a good correlate to the data structure. We now have two steps, both in good pipeline style:

  1. compose the verses
  2. 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

  1. 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}.

  2. 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.

12days.q
Q solution at Rosetta Code


  1. An earlier version of this article appeared on code.kx.com. 

  2. Not actually free, of course. But the iteration implicit in the primitives generally evaluates faster than any iteration you specify explicitly. And it is certainly free in terms of code volume.