Tool of Thought

APL for the Practical Man

"You don't know how bad your design is until you write about it."

HTML Tables Revisited

February 16, 2025

The last time we looked at tables was way back in 2021. That's a cute little function, and it's useful for relatively small tables, but to put it kindly, it is not particularly efficient.

The Abacus DOM creates a namespace for every element. Tables have lots of elements. This is not good in space:

      m←⍕¨{⍵⍴⍳×/⍵}1000 100
      t←NewTable m
      'CI12' ⎕FMT ⎕SIZE ,¨'mt' 
   4,800,040
 242,324,696

Yikes!

And the time to create is excessive as well. Rendering is slow too. For the Abacus DataGrid component we had to write some special code to get around this as we recreate and re-render the table on every key stroke when scrolling around. A general solution is called for. Rather than creating a namespace for every element, we can create just a few namespaces to hold content values and attribute values:

NewOptiTable←{
     t←⎕NS''
     t.Tag←'optitable'
     t.(Body Header Footer)←{
         s←⎕NS''
         s.Values←⍵⍴⍨¯2↑1,⍴⍵
         s.Rows←⎕NS''
         s.Cells←⎕NS''
         s}¨3↑⍬ ⍬,⍨(⊂⍣(2=≡⍵))⍵
     t
 } 

This takes hardly any space:

      t2←NewOptiTable m
      'CI12' ⎕FMT ⎕SIZE ,¨'m' 't' 't2' 
   4,800,040
 242,324,696
   4,821,416

And no time, as it doesn't really do anything:

      cmpx 'NewOptiTable m' 'NewTable m'
  NewOptiTable m → 2.3E¯3 |      0%                                         
* NewTable m     → 9.4E¯1 | +41822% ⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕     

(Results are of course different.)

Rendering now requires special code:

RenderOptiTable←{
     ⍝ ⍵ ←→ OptiTable
     o←⊂'Whitespace' 'Preserve'
     o,←⊂'UnknownEntity' 'Preserve'
     xml←⎕XML⍠o
     a←ComposeAttributes ⍵
     xml 0 'table' ''a⍪⊃⍪/'thead' 'tbody' 'tfoot'{
         r c←⍴⍵.Values
         0∊r,c:0 4⍴0
         n←r+r×c
         m←n 2⍴1 c⌿2 2⍴2 'tr' 3((⍺≡'tbody')⊃'th' 'td')
         m,←,(⊂''),⍵.Values
         a←,⊃,/r(r c){
             d←⍺
             n←(⎕C ⎕A)⍵.⎕NL ¯2
             0=≢n:d⍴⊂0 2⍴⊂''
             v←⍵⍎¨n
             n←'-'@('_'∘=)¨n
             b←{80=⎕DR⊃⊃⍵}¨v
             a←{
                 0=∨/b:d⍴⊂0 2⍴⊂''
                 k←↑{d⍴⊆⍵}¨b/v
                 (r q)←0 ¯1+⍴⍴k
                 ⊂[0 r](b/n),⍤0⍤0 q⊢k ⍝ Hat tip: AB
             }0
             a⊣(n/⍨~b){
                 ' '=⊃⍺:0
                 n i v←↓⍉(⊂⍺),↑⍵
                 0⊣a[i]←a[i]⍪¨↓⍉↑n v
             }¨v/⍨~b
         }¨⍵.Rows ⍵.Cells
         t←ComposeAttributes ⍵
         1 ⍺''t⍪m,a
     }¨⍵.(Header Body Footer)
 }

But is much faster:

        cmpx 'RenderOptiTable t2'  'DOM2HTML t'
  RenderOptiTable t2 → 2.7E¯2 |     0% ⎕                                       
  DOM2HTML t         → 1.5E0  | +5441% ⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕

The argument to NewOptiTable is identical to NewTable, but specifying attributes is done differently:

      t←NewOptiTable (⍕¨3 2⍴⍳6) ('One' 'Two')
      t.class←'table-class'
      t.Header.class←'header-class'
      t.Header.Cells.class←'col1' 'col2'
      t.Body.Rows.id←'rowid1' 'rowid2' 'rowid3'
      t.Body.Cells.class←'cell-class'
      t.Body.Cells.id←'id'∘,¨⍕¨3 2⍴⍳6
      t.Body.Cells.onclick←((0 0) 'foo1') ((2 1) 'foo2')

Attributes are specified by assignment in the root for the table element, and in the Header, Body, and Footer subspaces for the thead, tbody, and tfoot elements respectively. Each of these 3 subspaces contain a Rows and Cells subspace for attributes for tr and either td or th elements as appropriate. The only purpose of the Rows and Cells subspaces is to have place to specify attributes.

Attributes are enclosed if simple, and reshaped to match the target elements in question, providing something analagous to scalar extension. (For example, to specfiy a class per column in the above example simply do t.Body.Cells.class←'c1' 'c2'.) Alternatively, attributes may be specified by explicit index. So the above produces:

       RenderOptiTable t
<table class="table-class">                                
  <thead class="header-class">                             
    <tr>                                                   
      <th class="col1">One</th>                            
      <th class="col2">Two</th>                            
    </tr>                                                  
  </thead>                                                 
  <tbody>                                                  
    <tr id="rowid1">                                       
      <td class="cell-class" id="id0" onclick="foo1">0</td>
      <td class="cell-class" id="id1">1</td>               
    </tr>                                                  
    <tr id="rowid2">                                       
      <td class="cell-class" id="id2">2</td>               
      <td class="cell-class" id="id3">3</td>               
    </tr>                                                  
    <tr id="rowid3">                                       
      <td class="cell-class" id="id4">4</td>               
      <td class="cell-class" id="id5" onclick="foo2">5</td>
    </tr>                                                  
  </tbody>                                                 
</table>

An optitable element cannot be added to the DOM like a regular element. It must be rendered first and then double enclosed. So to add as a child to some element e:

         e.Content←⊂⊂RenderOptiTable t

Rethinking ⎕VSET and ⎕VGET

February 5, 2025

In the previous post we played around with the new system functions ⎕VSET and ⎕VGET, and noted a couple of unfortunate, though perhaps unavoidable, design decisions. First is having to enclose when setting or getting a single name, and second is having to provide a matrix of names when the argument is two separate arrays of names and values, rather than name and value pairs.

What if we simplify (and sacrifice some) things by insisting the argument is always two items, a list of names and a list of values, defining ⎕VSET as:

VSET←{
   ⍺←⎕THIS
   n v←⍵
   2=|≡n:⍺ ⎕VSET(↑n)v
   2=⍴⍴n:⍺ ⎕VSET n v
   ⍺ ⎕VSET ⊂n v
}

And ⎕VGET as:

VGET←{
   ⍺←⎕THIS
   1=≢⊆⍵:⍺ ⎕VGET⊃⊆⍵
   n v←⍵
   (1=⍴⍴n)∧2>|≡n:⍺ ⎕VGET ⊂⍵  
   ⍺ ⎕VGET (↑n) v
}

These functions do not allow name and value pairs, and as a corrollary do not allow the provision of only some default values for ⎕VGET. It's all or nothing with respect to default values.

However, we can do:

      s←()VSET 'One' 1
      s.One
1

And:

      n←'One' 'two' 'Three'
      v←1 2 3
      s←()VSET n v

or:

      s←()VSET (↑n) v

Similarly for getting values, we can avoid an enclose:

      s VGET 'One'
1
      s VGET 'Four' 4
4

But for multiple names only we need to enclose:

      s VGET ⊂'One' 'Two'
1 2
      s VGET ⊂n
1 2 3

And if providing default values, we must provide them all:

     s VGET ('One' 'Five') (1 5)
1 5

One nice thing about this design is the simplicity of documenting it. We simply say the right argument is composed of two items, names and values. We don't need to explain that if there are two elements, and the first element is a matrix, then things are interpreted one way, but if the first element is a vector then it means somehthing else. This kind of design always makes us feel a bit uneasy.

The loss of being able to default only some values and not all for a given set of names is not much to give up. The structure:


      (Name Value) Name (Name Value)...

does not arise very naturally in code, though it may as a literal structure.

Regardless of all of this, it is easy enough to cover ⎕VGET and ⎕VSET as we do above to get the behavior we want, if indeed that is what we want, and arguably the reverse would not be true.

Version 20 Goodies

January 31, 2025

A pre-release edition of version 20 has found its way to our machine and it introduces the long-awaited ⎕VGET and ⎕VSET. Documentation does not seem to be at hand yet, so let's just play around and see how they work.

First, we can create a new namespace now with () rather than ⎕NS '':

      s←()
      s
#.[Namespace]
      s.One←1
      s.One
1

Nice. Ok, lets set a variable:

      s←()
      s ⎕VSET 'One' 1
DOMAIN ERROR
      s ⎕VSET'One' 1
        ∧

Probably wants it nested:

      s←()⎕VSET ⊂'One' 1
      s.One
1
      s←()⎕VSET ('One' 1) ('Two' 2)
      s.(One Two)
1 2

Yes indeed. Why? It seems there could be a way to disambiguate a two item vector argument as Name Value vs (Name Value) (Name Value) given the level of nesting of the first item.

How about a matrix of names and values:

      s←()⎕VSET ↑('One' 1) ('Two' 2)
RANK ERROR: Invalid right argument
      s←()⎕VSET↑('One' 1)('Two' 2)

Doesn't like it. Often we have a list of names and a list of values in separate arrays:

      n←'one' 'two' 'three'
      v←1 2 3
      s←() ⎕VSET n v
DOMAIN ERROR

Does not like that either. This is because if allowed, then:

      () ⎕VSET ('One' 'Two') ('Three' 'Four')

would be ambiguous. However we can do:

      s←()⎕VSET (↑n) v

...providing a matrix of names. Not really happy with this but no alternative seems to present itself.

Let's extract values:

      s ⎕VGET 'One'
1
      s ⎕VGET 'One' 'Two'
1 2
      s ⎕VGET ↑'One' 'Two'
1 2

Getting is more flexible than setting. How about with default values:

      s ⎕VGET'One' 'Two'('Three' 'three')
1 2  three 
      s ⎕VGET ('One' 'one') ('Two' 'two') ('Three' 'three')
1 2  three 
      s ⎕VGET ↑('One' 'one') ('Two' 'two') ('Three' 'three')
RANK ERROR: Invalid right argument
      s ⎕VGET↑('One' 'one')('Two' 'two')('Three' 'three')
        ∧

Does not like a matrix of names values. Here though we see a problem with a two-item argument of Name and Value:

      s ⎕VGET 'One' 'one'
VALUE ERROR: Undefined name: one
      s ⎕VGET'One' 'one'
        ∧
      s ⎕VGET ⊂'One' 'one'
1

Are we trying to get the value of two names, or one name with a default value? So if ⎕VGET requires a single name/value pair to be enclosed, maybe ⎕VSET should as well, even though maybe not strictly necessary.

And with names and values in separate arrays:

       s←()⎕VSET↓⍉↑n v
       s ⎕VGET n
1 2 3
       s ⎕VGET n v
DOMAIN ERROR

If ⎕VSET would take n and v as is, and that last expresssion worked, this would be nice and symetrical.

How about an array of spaces as the left argument:

      a←()()() ⎕VSET ('One' 1) ('Two' 2) ('Three' 3)
      a.One
1 1 1
      a.(One Two Three)
┌─────┬─────┬─────┐
│1 2 3│1 2 3│1 2 3│
└─────┴─────┴─────┘

Oooooh. Very nice. Every name on the right is injected into every space on the left. Same as a.Var←5 injects Var into every namespace in a. How far can we go? How about a matrix:

      m←2 3⍴()()()()()()
      m ⎕VSET ('One' 1) ('Two' 2) ('Three' 3)
DOMAIN ERROR: Invalid left argument

Doh! But as namespaces are refs we can do:

      (,m) ⎕VSET ('One' 1) ('Two' 2) ('Three' 3)
      m.One
1 1 1
1 1 1

Not bad, but it would be nicer if it just worked, just as m.Var←5 works. It's not so nice going the other way, as we actual need to use the result of the function:

      m ⎕VGET 'One'
DOMAIN ERROR: Invalid left argument
      m ⎕VGET'One'
        ∧
      (,m) ⎕VGET 'One'
1 1 1 1 1 1

Where we need something like:

     m (⍴⍤⊣⍴,⍛⎕VGET) 'One'
1 1 1
1 1 1

as a workaround suggested, perhaps humorously but functional none the less, over on the Orchard.

Other notes. If no left argument is provided, it defaults to the current space or ⎕THIS:

      ⎕VSET ⊂'Hello' 'World'
      Hello
World 
      ⎕VGET 'Hello'
World

This is all great stuff and is going to replace a lot of hokey code.

More posts...