There was a time, long ago and far away, when I used to write proper business applications in APL. I am thinking back to 1983 and my Loughborough paper on databases, which reminded me of just how heavily I depended on a simple, reliable DBM working entirely within the confines of the APL workspace. Subsequently, I had evolved into something of a tool-builder rather than a tool user, and this state persisted until a few weeks ago.
When I had a phone call from a old Rowntree friend (he used to run Assortments in York) for whom I wrote a lot of planning and scheduling applications. He is kinda retired now, and has ended up managing the rebuild of the east end of York Minster ‘in his spare time’. This is a big project (around £21M of lottery funding over 5 years) and has put huge pressure on the Minster Stoneyard to turn out carved blocks to a predictable schedule. Herding cats comes to mind.
 
  
So Peter called me, and wondered if I would be interested in reviving some 20-year old skills and cranking out a little scheduler for them, so at least they might know a little way ahead who will be carving which blocks when, and consequently when various bits of the overall project might get completed. Was I interested – of course I was!
Panic then set in fairly quickly, as I realised that an essential chunk of the software I needed was last seen on a VS APL system that was turned off in late 1999. So I looked around my collection of heaps of things and found a flipdb user guide and (of course) a copy of “q for Mortals” which I recently reviewed. Towards the end of this review, I caught myself musing around the possibility of implementing something comparable in APL. After all, kdb was originally just a bunch of q and Paul had gone down a very parallel road with flipdb. Time to give it a go, thought Adrian.
    Firstly, this has to be designed to make the raw data available to your
    application in the simplest way imaginable. You must not have to run
    a query (or call a server) just to get some numbers back. In the old days I
    just used variables in the workspace, with a ‘master variable’ called
    (say) ⍙emp which was just a namelist of other variables
    like ∆name, ∆sal and so on. A few simple utilities implemented
    operations like take and compress on the table as a whole. The description
    variable developed a few whistles and bells, the main one being support for
    foreign keys (when the emp table has a pointer to a dept table) which were
    implemented as simple (zero-tolerant) 1-origin indexer variables. So when
    you deleted a department, it knew to fix up the indices in any other tables
    which referenced it (or yell at you if the deletion was not acceptable).
  
Let’s slide forward from the VS APL timeframe where this was all written, to the 21st century and Dyalog APL. Namespaces could be very handy here, and maybe those foreign keys could just be refs? If we start with the basic notion that a table maps to a namespace with some variables in it, then we pass the first hurdle of data-accessibility. What could be easier than:
emp.sal
12340 2345 2345 1200 1234 0 1234
+/emp.sal
20698
Rather than using prefixed names in the old-fashioned way, we just prefix with the namespace. This makes it pretty trivial to code up (for example)
8 db.take emp
emp.sal
12340 2345 2345 1200 1234 0 1234 0
∇ {tbl}←len take tbl;nl2
[1] ⍝ Normal APL take/overtake for all vars in namespace <tbl>
[2] nl2←tbl.⎕NL ¯2
[3] :With tbl
[4] ⍎¯2↓∊(⊂'↑⍨←len ⋄ '),⍨¨nl2
[5] :End
∇
    Of course you need to be sure that the only variables you leave lying around
    are valid to be taken/compressed by the same expression. Anything else we
    want to store about the data is going to be tucked away in a sub-namespace.
    Alert readers should have spotted the flaw in the above function. I should
    really have formatted the length and used dyadic execute rather than :with
    here. One day I’ll have a variable called len in a table, and it will all
    go horribly wrong.
  
Anyway, moving quickly on, if we could throw some descriptive stuff into a sub-namespace, that would help with the formatting of the output, and also give me somewhere to record those pesky relationships.
db.Columns emp
Varname Key Type Format References Shape
------- --- ---- ------ ---------- -----
#.emp.id ⍝ * num 8
#.emp.name ⍝ text 8
#.emp.dept ⍝ ptr #.dept 8
#.emp.sal ⍝ num £##,##0.00 8
#.emp.bonus ⍝ num 8
#.emp.band ⍝ num 00 8
#.emp.sex ⍝ ptr #.sex 8
    The references really are refs here, everything else in the description
    namespace is just text, so is pretty trivial to edit. There are a few handy
    utilities called db.CreateTable and db.CreateColumn which help you to get things in the right
    order. So far so good. If I re-order the dept table it can check for any
    refs to itself and go fix up the corresponding pointers (fortunately most
    primitives like iota and membership work fine on refs now) so we are still
    quite low on rocket science. Just formatting the content and throwing it
    into the session is very easy now:
  
db.Show emp
id name dept* sal bonus band sex*
---- ---- ---- ---------- ----- ---- ---
5728 │ William 451 £12,340.00 100 01 Male
1234 │ Gill 777 £2,345.00 100 03 Female
1238 │ Richard 451 £2,345.00 400 02 Male
1240 │ Tim 822 £1,200.00 100 02 Male
1241 │ Beryl 451 £1,234.00 0 01 Female
1242 │ Hello there 822 £0.00 0 01 Male
1243 │ Farewell 451 £1,234.00 321 02 Never
0 │ 0 £0.00 0 00
Better get rid of that dodgy extra row (the overtake a few lines back) but at least you can see what it did.
    To write a db.Select utility that takes a table and some expressions, and throws
    you back what is effectively another table
  
      db.Select emp ('' 'sal>1000')
#.db.sel
  Well, it returned a ref all right, can we see the content with the same kit we used to see the original table?
     db.Show qq←db.Select emp ('' 'sal>1000')
   id    name      dept*         sal  bonus  band  sex*   
 ----    ----      ----   ----------  -----  ----  ---    
 5728 │  William    451   £12,340.00    100    01  Male   
 1234 │  Gill       777    £2,345.00    100    03  Female 
 1238 │  Richard    451    £2,345.00    400    02  Male   
 1240 │  Tim        822    £1,200.00    100    02  Male   
 1241 │  Beryl      451    £1,234.00      0    01  Female 
 1243 │  Farewell   451    £1,234.00    321    02  Never 
  More to the point, can we run a further query on the result of this one?
     qqq←db.Select qq ('name,sal,bonus,both←sal+bonus' 'bonus>100')
     db.Show qqq
 name            sal  bonus  both 
 ----      ---------  -----  ---- 
 Richard   £2,345.00    400  2745 
 Farewell  £1,234.00    321  1555 
  This is the property of closure that the database gurus insist is so important. It allows you to write a proper relational algebra, with functions like set-union and set-difference that act on tables and return tables. It is one of the great strengths of q and is something we should constantly keep in mind. Of course we can still get at the raw data very easily:
qqq.(name both)
Richard Farewell 2745 1555
The only magic here is the magic that comes with your interpreter. Honest!
Hands up everyone who hates writing joins in SQL. Both Arthur and Paul (independently, I think) had the brilliant idea of using the dot-notation to allow a query to ‘look down the link’ in a very intuitive way. An example will make this clear:
sel←db.Show∘db.Select
sel emp ('name,dname←dept.name,mgr←dept.mgr.name' 'bonus<100')
name dname mgr
---- ----- ---
Beryl Main office
Hello there Dogsbodies Gill
The dot is appropriate for many-to-one relationships, and of course you can chain them to follow the links to the end of the earth (and back again, as in the example above). Just try asking Oracle to list all employees who earn more than their manager, and you will rapidly appreciate the power of this simple extension. Incidentally variables get ‘sensible’ new names if you leave them to default, but giving them names explicitly generally makes sense.
Paul takes this a step further by allowing us to look through the wrong end of the telescope and see the many from the one, like this:
sel dept 'id,name,emp[dept].bonus'
id name bonus
--- ---- -----
451 │ Main office [100 400 0 321]
977 │ Real workers are here again []
822 │ Dogsbodies [100 0]
777 │ The End [100]
Which illustrates another great thing about q (and flipdb, of course) which is that you are allowed to group things without applying a reduction. Of course you can reduce the groups if you want to:
sel dept 'id,name,max emp[dept].sal'
id name sal
--- ---- -------------------
451 │ Main office 12340
977 │ Real workers are here again ¯1.797693135E308
822 │ Dogsbodies 1200
777 │ The End 2345
… but be aware that this is APL and that reductions on empty arrays don’t always do what you expect! I need to have a proper think about missing-value support before I fix this one.
Grouping is something we need to do all the time, so it makes sense to accommodate it in the normal query syntax. Along comes another optional argument (sorry Arthur, I put these in a different order) to the selection to give us the new keys for the output table:
      sel emp ('count,sum sal,avg sal+bonus' '' 'dept.name')
 name           count  sum sal  avg col4 
 ----           -----  -------  -------- 
 Main office │      4    17153    4493.5 
 The End     │      1     2345    2445   
 Dogsbodies  │      2     1200     650   
  Note that the grouper(s) are marked as keys (as they are always unique combinations) and the output is once again a table that we can do further processing with, or just treat as a handy repository for some working data. If we group on two columns:
      sel emp ('count,sum sal,avg sal+bonus' '' 'dept.name,band')
 name         band    count  sum sal  avg col5 
 ----         ----    -----  -------  -------- 
 Main office    01 │      2    13574      6837 
 The End        03 │      1     2345      2445 
 Main office    02 │      2     3579      2150 
 Dogsbodies     02 │      1     1200      1300 
 Dogsbodies     01 │      1        0         0 
  Then we get two keys (duh!) and something stirs deep in Adrian’s subconscious. Maybe we could use these as the row/column indices into a crosstab, like this:
xtab←db.XTab∘db.Select
xtab emp ('sum sal' '' 'dept.name,band')
name 01 03 02
---- ------- ------- -------
Main office │ 13574 3579
The End │ 2345
Dogsbodies │ 0 1200
As of today, this is just a fancy way of displaying a table with two keys, but a sense that it should become another first-class object, related to a table, but not quite the same. Perhaps a brick or a box would be good names for it. Cube has already been done to death, I think! The bit in the middle looks awfully like a matrix.
    There is really only one important idea behind this. Having checked out the
    tokens in each expression, the Select function runs the expressions in the
    table (so they can see all the variables) but with the path set to look for
    the utility namespace which lives inside #.db. This allows you to write any
    APL you like in the query, or make use of some simple canned functions if
    you can’t be bothered to re-invent average every time you want it.
    Here are the 6 wettest days we have had, in date order:
  
      sel rain ('' '{⍵>¯6+⌈/⍵}⍋⍋rain')
 date        rain  mintemp  maxtemp 
 ----        ----  -------  ------- 
 25/10/92 │  38.9       ¯2        6 
 08/06/99 │  38.1        6       11 
 02/08/02 │  49.3       15       20 
 10/08/04 │  55.4       18       21 
 15/06/07 │  53.4       10       12 
 25/06/07 │  38.1       12       18 
  
    … and here is what our avg function does:
  
 avg←{1{(+/⍵)÷↑⍴,⍵}godeep ⍵}
 godeep←{        
   (1+⍺)>|≡⍵:⍺⍺ ⍵
   ⍺ ∇¨⍵         
 }
  … with much the same stuff for all the standard summary functions. I am sure 10 mins of Alan Sykes’ time will add lots more, like variance, median and all those other ones that standard databases never have when you want them.
We have a functional query engine, we have an APL session. Perhaps we should put them together to make one of those little languages that we all remember from the 1970s. Remember where we came in? Well, Stones have Types and Types have estimated days, so wouldn’t it be great if we could type:
xtab count,ManDays←sum Type.Mason+Type.Carve
from ym.Stone
by Type.Id,Level.Name
where Level.Id in 'B,C'
asc Type.Id
Id Level B Level C
-- count ManDays count ManDays
Arcading │ 1 25
Arcading/string │ 3 45
Ashlar │ 4 4 3 3
Grotesque │ 2 80 2 80
Niche head │ 4 180
Niche head course 2 │ 2 48 5 120
Shaft moulding │ 31 248 44 352
Shaft stooling │ 2 20
String/pedestal │ 3 60
… to get an overview of how much work is left. Perhaps some of you are old enough to remember those special daisy-wheels that allowed you to plot to some small fraction of a character? Well, with Unicode we have some 1/8th blocks, so it’s back to the future again:
select ManDays←sum Type.Mason+Type.Carve '⎕24 ###9'
from ym.Stone by Type.Id asc Type.Id where Type>0
Id ManDays
-- -----------------------------
Arcading │ ▌ 25
Arcading/string │ █ 45
Ashlar │ ▎ 12
Ashlar return │ ▎ 12
Carved pedestal │ █▎ 60
Grotesque │ ███▍ 160
Niche head │ ██████▋ 315
Niche head course 2 │ ████▌ 216
Shaft moulding │ ████████████████████████ 1144
Shaft stooling │ ▍ 20
String │ ▍ 20
String/grotesque │ █▋ 80
String/pedestal │ █▎ 60
String/shaft stooling │ ▌ 25
    There is a font update on your keydrive if you don’t see all the blocks
    correctly! The workspace is called iqx, it is definitely work in progress,
    and there are plenty more examples. You won’t see the stone data (for
    obvious privacy reasons) but there is 15 years of rainfall to play with, as
    well as the infamous “Suppliers and Parts” data that everyone uses these
    days. Of course what would be really nice would be:
  
select photo from ym.Stone where Id=467
 
  … but that one will have to await the next update to the Dyalog session!
    This is first and foremost a programmer’s toolkit, and I would expect to use
    the functional form of db.Select nearly all the time. The stuff with queries and
    the session I see mostly as a handy debugging tool, but you never know. That
    is really how q got started, and look what happened to it. The
    session is a much under-appreciated resource, so exploiting it in this
    simple way was good fun, and may develop into something useful. Let me know.