Treetable: A Case-study in Q. ------------------------------ 0. Introduction This paper is a case-study in Q programming. I've chosen as my subject the problem of the "treetable". A treetable is a table with a few additional properties. First, the records of the table are related hierarchically. Thus, a record may have one or more child-records, which may in turn have children. If a record has a parent, it has exactly one. A record without a parent is called a root record. A record without any children is called a leaf record. A record with children is called a node record. Second, it is possible to "drill down" into a treetable. If a record is a parent, then some of its columns may be aggregations of those columns in its child-records. By drilling down into a parent-record, it is possible to inspect the elements which are aggregated in the parent. Third, treetables have "state". If the user drills down into the tree along a particular path, then closes a node along that path, the records on that path become invisible. If the user re-opens that parent, then the nodes along that path will become visible if they were visible before the parent was closed. In other words, closing an open parent does not destroy the visibility state of its children. Finally, a treetable is naturally sorted in a way that is an extension of ordinary table sort. Intuitively, the sort of a treetable is the recursive application of up- or down-sorting on the "blocks" out of which it is composed. I've included an explanation of how such a multi-column recursive sort works. The treetable is a natural candidate for a control in a non-procedural data-driven GUI. Q and K have a long tradition of such GUIs, stretching back to A+ and the native K3 GUI. The examples in this paper are abstracted from an implementation of a GUI recently developed for Q. But the case-study is meant to stand alone, as an exercise in pure data design. In a future installment, I hope to show how data designs like this one are smoothly integrated into a data-driven GUI. In the appendix, I've included a few screen-shots of the treetable control. 1. Lists, Dictionaries, Tables, Keytables This section contains the necessary background on Q's collection data-types. A list is a collection indexed by position: l:10 20 30 l 2 0 30 10 l?30 10 2 0 A dictionary is a collection indexed by a Q object: d:`a`b`c!10 20 30 d`c`a 30 20 d?30 20 `c`a key d `a`b`c value d 10 20 30 The Q object is usually a symbol (a name) but need not be. For example: e:(10 20;30 40 50;70 80)!`a`b e(30 40 50;10 20) `b`a A dictionary is a map from a list of elements (the key) to a list of elements (the value). The key and the value must have the same count. Moreover, the key must not contain duplicates. Atomic functions penetrate both lists and dictionaries: l+1 11 21 31 d+1 a| 11 b| 21 c| 31 A table is a list of dictionaries, or "records", all of which have the same key. For example: t:(d;d+1;d+2;d+3) a b c -------- 10 20 30 11 21 31 12 22 32 13 23 33 A list of key-dissimilar dictionaries is not a table: (`a`b!10 20;`b`c`d!30 40 50) `a`b!10 20 `b`c`d!30 40 50 Since tables are lists, we can index them positionally: t 1 a| 11 b| 21 c| 31 The transpose of a table is a dictionary whose values are lists: flip t a| 10 11 12 13 b| 20 21 22 23 c| 30 31 32 33 The flip of a dictionary of equal-length lists is a table: t~flip flip t 1b Q has a compact notation for constructing tables: u:([]a:10 11 12 13;b:20 21 22 23;c:30 31 32 33) t~u 1b A table can therefore be constructed in three ways: - as a list of dictionaries - as the flip of a dictionary of vectors - using table-notation A keytable is a dictionary in which the key and the value are both tables. For example: k:([]f:`a`a`b;g:1 2 1) v:([]a:10 20 30;b:40 50 60;c:70 80 90) a:k!v a f g| a b c ---| -------- a 1| 10 40 70 a 2| 20 50 80 b 1| 30 60 90 Since keytables are dictionaries, they are indexed by key: a(`a;1) a| 10 b| 40 c| 70 a((`a;1);(`a;2)) a b c -------- 10 40 70 20 50 80 Since keytables are dictionaries, they can be split into key and value: key a f g --- a 1 a 2 b 1 value a a b c -------- 10 40 70 20 50 80 30 60 90 A keytable can also be defined using Q table-notation: a~([f:`a`a`b;g:1 2 1]a:10 20 30;b:40 50 60;c:70 80 90) 1b The data universe of Q can be summarized as follows: There are atoms and lists. Dictionaries map lists to lists. Tables are lists of dictionaries and keytables are dictionaries which map tables to tables. 2. Trees We can represent a tree as a list of paths. For each element of the tree there is a corresponding path to that element: tree path ---- ---- A A B A B C A B C D A B D E A E F A E F G A E F G H A E F H I A E F I From the path of an element e we can easily compute the parent: the parent of e is enlist e if the path is a singleton, else the parent is the drop of the last element of the path. While the path-list representation is intuitive, it can be clumsy to work with. For example, to find the children of A we need to search the path-list for all 2-element lists which have A as their first element. To find the children of A B we need to find all 3-element lists which have A B as their first two elements. We can represent the parent-child relation explicitly, as a table PC: parent child ------ ----- A A A B B C B D A E E F F G F H F I Then: select child from PC where parent=`A child ----- B E select parent from PC where child=`F parent ------ E We can represent the relation as a parent-vector: p:0 0 1 1 0 4 5 5 5 That is: i tree p - ---- - 0 A 0 1 B 0 2 C 1 3 D 1 4 E 0 5 F 4 6 G 5 7 H 5 8 I 5 p[i] is the index of the parent of the i-th element of the tree. To find the children of any element e in the tree, search p for occurrences of e's index: where p=5 6 7 8 To find the root of any element e, repeatedly index p, starting with e: p 6 5 p 5 4 p 4 0 p 0 0 To find the root of e in one step, reduce p over e: p over 6 0 p is applied repeatedly to the previous result until the result is the same twice in a row. This is why it is convenient to treat the root as self-parenting. To find the path from e to the root in one step, scan p over e: p scan 6 6 5 4 0 To find the paths of all elements of the tree, scan p over each of 0 .. count[p]-1: i:(p scan)each til count p i ,0 1 0 2 1 0 3 1 0 4 0 5 4 0 6 5 4 0 7 5 4 0 8 5 4 0 To find the leaf-elements of the tree, discard every element which is a parent: l:til[count p]except p l 2 3 6 7 8 We can use p to aggregate data associated with the tree. For example, suppose that the five leaf-elements have the following data: d:count[p]#0 d[l]:l*10 d 0 0 20 30 0 0 60 70 80 Now to sum d to the root, amend a zero-vector with + at i, the effect of which is to accumulate sums on all paths: @[count[d]#0;i;+;d] 260 50 20 30 210 210 60 70 80 Think of it this way: where r is the result, r k is sum d i k. In other words, the result at each node of the tree is the sum of that node's descendants. From the parent-vector representation and a list of elements, we can compute the path-list: e:`A`B`C`D`E`F`G`H`I n:reverse each e i n ,`A `A`B `A`B`C `A`B`D `A`E `A`E`F `A`E`F`G `A`E`F`H `A`E`F`I And from the path-list representation we can compute the parent-vector: drop the last element of each path whose count is greater than 1 and find each truncated path in the path-list: n?neg[1