Tool of Thought

APL for the Practical Man

"We make software the old-fashioned way, we write it."

APL Makes Us Smile

May 21, 2026

In Abacus we have a function for setting the value of a component:

SetComponentValue←{
     ⍝ ⍺ ←→ Component
     ⍝ ⍵ ←→ Value
     ⍺ ⍺.Class.SetValue ⍵
 }

The left argument is the component, so we need to have the component in hand before calling this function.

Abacus does not use Dyalog classes, but the "instance" has a reference to the component namespace that contains the SetValue function.

The component is often retrieved via its name from the document or a node using GetComponent, which takes a node as the left argument and the name as the right argument, so this pattern is common:

    C←D GetComponent 'MyComponentName' 
    C SetComponentValue 'MyValue'

It was suggested that SetComponentValue should be able to take a component name as its argument to avoid calling GetComponent. Of course it would also need know about the document or node. A first pass produced:

SetComponentValue←{
     ⍝ ⍺ ←→ Component|Node and Name
     ⍝ ⍵ ←→ Value
     1=≢⍺:⍺ ⍺.Class.SetValue ⍵
     c←(0⊃⍺) GetComponent 1⊃⍺
     c c.Class.SetValue ⍵
 }

But we noticed GetComponent could be called with a reduction:

     c←GetComponent/⍺

and then why have the guard? As reduction on single item returns that item, we can just write:

SetComponentValue←{
     ⍝ ⍺ ←→ Component|Node and Name
     ⍝ ⍵ ←→ Value
     c←GetComponent/⍺
     c c.Class.SetValue ⍵
 }

Little things like this are nice.

A Query in R and Kap

May 8, 2026

Elias Mårtenson posts about a query in R and compares it to his APL-inspired language Kap. Elias of course provides a succint, APL-style solution in Kap. Here we take a look at the R solution and compare it to FlipDB, a relational database management system written in APL. The problem starts with the following table:

Country Amount Discount
USA 2,00010
USA 3,50015
USA 3,00020
Canada 120 12
Canada 180 18
Canada 3,10021
UK 130 13
UK 160 16
...

Given this, the task is to produce a table of total discounted sales by country, excluding outliers defined as entries in each country with an amount greater than 10 times the median for that particular country. The result we are looking for is:

Country Total
Australia540
Brazil 414
Canada 270
France 450
Germany 513
India 648
Italy 567
Japan 621
Spain 594
UK 432
USA 8,455

We thus have a row-dependent where clause. That is, whether or not a row is included depends on other rows in the table. More precisely, whether or not a row is included in a group depends on the other rows in the group. This is the crux of the matter, and what makes the query problematic in some tools.

The R solution is given as:

  purchases |>
  group_by(country) |>   
  filter(amount <= median(amount) * 10) |>
  summarize(total = sum(amount - discount))

As the original post suggests, this is indeed a nice solution.

A comment on reddit makes the observation:

It's true that R's DSL has some nice defaults here, like the filtering happening implicitly on the grouped columns. But a DSL means there's stuff happening without straightforward execution semantics, there's some magic. And that example is very short in R, because it relies on those defaults, but if it needed sorting by total (instead of by country), or computing a flat amount-discount before grouping, then it'd start looking a bit longer.

We don't see the magic here. The R solution starts with a table or dataset in a column-store format, which is then partioned by country. In APL terms we can think of this as just making a vector of unique values from country and corresponding nested vectors out of amount and discount. All of the basic scalar operations are happy to work on nested vectors, applying scalar extension where necessary. We can then imagine sum and median being aggregate functions that have a built-in each operator when running on nested data. Same for filtering, where APL's replicate function would have a built-in each for nested data, taking a nested boolean array and masking the nested column data. DSLs can just be a collection of well-crafted, higher-level functions. If R were APL, the underlying code just jumps right out of this solution. It is simple, direct, executable and traceable step-by-step, unlike, say, an SQL query. And in fact, this is almost exactly how FlipDB works, and the solution is remarkably similar:

GroupBy:NameExpression
CountryCountry
Measures:NameExpression
Totalsum (Amount - Discount) where Amount <= 10 * median Amount
OrderBy:NameDirection
CountryUp

(Note that we have to explicitly specify the ordering which seems to be a default in R. We don't show the OrderBy clause in the queries below in the interest of space.)

The difference is that we are filtering in-line, just for one column in the result set, whereas the R solution is filtering the entire table. In-line filtering is useful because we can apply different filters, or no filter at all, for different columns in the result table. (Useful for a poor man's cross-tab.) For example, it might be useful to display the totals without excluding the outliers side-by-side for comparison which can be done by just adding another measure:

Measures:NameExpression
Totalsum (Amount - Discount) where Amount <= 10 * median Amount
TotalAllsum Amount - Discount

yielding:

Country Total TotalAll
Australia540 540
Brazil 414 414
Canada 270 3,349
France 450 450
Germany 513 513
India 648 648
Italy 567 567
Japan 621 621
Spain 594 594
UK 432 432
USA 8,4558,455

Here we can see by inspection that only Canada has outliers.

Note that where is just a simple function that takes an array of one or more columns on the left and a corresponding boolean on the right and then returns the filtered columns. In this case, because we are just summing the result, we could replace where with multiplication, an age-old APL technique, zeroing out instead of filtering.

We can, like the R solution, specify a where clause for the query as a whole:

Where:Expression
Amount <= 10 * median by Amount (group Country)
GroupBy:NameExpression
CountryCountry
Measures:NameExpression
Totalsum Amount - Discount

However, unlike the R solution, this is applied before, and independently of, grouping. Thus we need to specify a grouping in the where clause itself, and then apply the median function to each group using the by operator. The by operator handles the details of applying an aggregate function to grouped data, and then replicating the results to line up with the ungrouped data. The advantage here over the R solution is that we may then group our query by some other column or value than Country. For example, we might group by region, but keep outliers defined within country:

Where:Expression
Amount <= 10 * median by Amount (group Country)
GroupBy:NameExpression
AmericasCountry in 'USA,Canada,Brazil'
EuropeCountry in 'UK,France,Germany,Italy,Spain'
AsiaCountry in 'Australia,Japan,India'
Measures:NameExpression
Totalsum Amount - Discount

which yields:

Region Total
Americas9,139
Europe 2,556
Asia 1,809

What if we want to group by region but in addition display totals including the outliers, as we did above? We can't exclude that data from the query, but now we can't assume that the grouping for the query is the same grouping for computing the outliers. Rather than applying a where clause, we can pre-compute a column that flags outliers according to their country and then group based on region:

ComputedColumns:NameExpression
OutlierAmount > 10 * median by Amount (group Country)
GroupBy:NameExpression
AmericasCountry in 'USA,Canada,Brazil'
EuropeCountry in 'UK,France,Germany,Italy,Spain'
AsiaCountry in 'Australia,Japan,India'
Measures:NameExpression
Totalsum (Amount - Discount) where not Outlier
TotalAllsum Amount - Discount

which yields:

Region Total TotalAll
Americas9,13912,218
Europe 2,5562,556
Asia 1,8091,809

In FlipDB computed columns in a query are executed before the where clause. This is useful, as we may want to reference them in the where clause, and if they are row-dependent we may want them computed on the entire table, not just what the where clause includes. However, perhaps in this case, it would be useful if computed columns were to execute after the where clause. We could have two sets of computed columns, one executed before the where clause and one executed after, but that seems a bit overkill.

Debouncing

May 3, 2026

Consider a treeview and a corresponding panel on its right, as in a file explorer. If we click on an item in the treeview, a select event is generated which, typically, fires a callback function to update the panel on the right. We can be Olympic mouse champions but we can only move the mouse and click on another item so fast. Generally no matter how adept we are with the mouse, the system will be able to refresh the right-hand panel with no apparent lag.

Now consider using the down cursor key to scroll through the items of the tree. It takes no special skill to scroll through hundreds of items at lightning speed. Your cat can do this stepping on your keyboard. Each keypress yields a select event and a subsequent execution of the callback function to refresh the right-hand panel. If the refresh operation takes any significant time, the response will be sluggish, the user experience bad.

The solution to this problem is known as debouncing. Typically on each keypress a timer is created or reset, and only when the timer goes off is the callback executed. So if we hold down a key the timer is constantly being reset, and only when we release the key and pause for a moment does the timer have an opportunity to go off. Instead of running a hundred times, the callback executes only once, when we pause scrolling.

As far as we know, no UI frameworks build debouncing into components. Debouncing must be implemented by hand, on a case-by-case basis.

We attempt to build this functionality right into the Abacus TreeView component, and to make it a general solution for any event that needs debouncing.

This first problem we encounter is that a ⎕WC timer will not help us because we are multithreading and ⎕WC objects do not like to interact with different threads.

Fortunately the architecture of Abacus provides another way. Abacus has a wait loop where the document is waiting for a token with ⎕TGET from the main thread, signifying the user has taken some action in the browser. Previously we had not taken advantage of the left argument of ⎕TGET which allows for a timeout. If we want to debounce some event, we can signal to the system that we want a delay. We can set a timeout, exit ⎕TGET when the user pauses, and then handle the event. First, we add a few new system properties to the Document object:

     d.Debounce←⍬
     d.DefaultTimeout←2147483
     d.Timeout←d.DefaultTimeout

These are system properties; the programmer does not set them. The Debounce property tracks if there is an event being debounced. It is either an empty array, or a two element array containing the callback function and its argument. The DefaultTimeout property effectively specifies no timeout. The Timeout property will be used by ⎕TGET in the event loop. The TreeView OnSelect property (and potentially any event callback on any component) may be set to either a simple string with the name of the callback, or a namespace containing the callback function and a debounce delay:

    (CallbackFunction:'MyCallBack' ⋄ DebounceDelay:100)

That's all the programmer needs to do.

In the TreeView component, when a cursor key is pressed, we run DebounceSelect as a cover over FireSelect which actually fires the select event:

DebounceSelect←{
     ⍝ ⍺←TreeView
     d←⍺.Document
     l←⍺.OnSelect.DebounceDelay
     l=0:FireSelect ⍺
     d.Timeout←l÷1000
     d.Debounce←(A.FQP'FireSelect')⍺
     0
 }

This checks the value of DebounceDelay. If 0, the select event is fired immediately. Otherwise, the document Timeout property is set to the delay and the callback with its argument are assigned to the Debounce property, where they await execution when the ⎕TGET times out:

ThreadQueue←{
     ⍝ Queued Event Loop for Session/Document
     r←⍺.Timeout ⎕TGET ⎕TID
     0=≢r:⍺ ∇ ⍵⊣Debounce ⍺
     c←⊃r
     _←LogBrowserEvent c
     _←(⍎c.CurrentTarget⍎'On',c.Event)c
     _←PutDoneToken c
     ⍺ ∇ ⍵
 }

When a timeout occurs, Debounce is called, which executes a pending debounced event if there is one:

Debounce←{
     ⍝ ⍵ ←→ Document
     0=≢⍵.Debounce:0
     f a←⍵.Debounce
     ⍵.Debounce←⍬
     ⍵.Timeout←⍵.DefaultTimeout
     (⍎f)a
 }

What Could Go Wrong

At least two things can go wrong with this technique. A debounced event might not fire when it should, and a debounced event might fire when it should not.

For the first case, consider two treeviews (and associated panels), and a user scrolling through the first treeview. If the debounce delay is long enough, or the user quick enough, the user could tab to the second treeview and start scrolling there. The timeout on the first debounce would not happen, and then the next debounce would overwrite the first debounce, which would thus be lost. The first treeview would not update.

For the second case, again with a debounce delay long enough or a nimble user, the treeview could be deleted by some user action, and then the timeout occurs and the debounced event fires on a nonexistant element.

We can probably code around both of these problems, but as the delay should always be set as short as possible, they are not likely to occur.

More posts...