Tool of Thought

APL for the Practical Man

"Vibe coding since 1987"

ProgressBar Component Revisited

October 24, 2025

Way back in May of 2024 we made a first attempt at a ProgressBar component.

No battle plan survives first contact with the enemy, so now that one intrepid user is attempting some real work with Abacus, it's time to make the ProgressBar actually work.

While the operator approach is interesting and indeed useful, it does not allow us to easily integrate a progress bar into an existing codebase, especially one with looping control structures like :For, :While, and :Repeat. For this we need a Create function to create a progress bar, an Update function to update it inside the loop and to check for user action, and finally a Close function to get rid of it when done. For example if we know the number of iterations ahead of time, we might write something like:

p←d A.ProgressBar.Create ⊂'Iterations' 25
:For i :In ⍳25
     r←p A.ProgressBar.Update'Doing iteration: ',⍕1+i
     :If r≢'Resume' ⋄ :Leave ⋄ :EndIf
     ⎕DL 0.2
:EndFor
p A.ProgressBar.Close''
:Select r
:Case 'Cancel'
     z←d A.Alert'You canceled the operation.'
:Case 'Resume'
     z←d A.Alert'The operation completed.'
:EndSelect

Here the looping and logic is exposed and explicit. The result of Update is examined inside the loop on each iteration to determine if we should continue or not, and again after the loop to determine the final action. Of course this logic can be implemented many different ways.

If we don't know the number of iterations, we might write code like:

d A.ProgressBar.Create ''
t←0
:Repeat
    r←A.ProgressBar.Update'Total time in seconds elapsed: ',⍕t
    :If r≢'Resume' ⋄ :Leave ⋄ :EndIf
t+←⎕DL 0.2
:Until t>20
A.ProgressBar.Close''

We still provide the Run operator which has been significantly simplified, with one operator covering both the determinate and indeterminate cases:

Run←{
     p←New 1⊃⍵
     _←⍺ A.ShowModal p
     r←p ⍺⍺{
         s←((2=≡⍺.Status)/1+⍺.Iteration)⊃⍺.Status
         c←⍺ Update s
         c≡'Cancel':1 ⍺.Result
         c≡'Truncate':2 ⍺.Result
         ⍺.Result,←⊂⍺ ⍺⍺ ⍵
         ⍺.Done:0 ⍺.Result
         ⍺.Iteration=⍺.Iterations-1:0 ⍺.Result
         ⍺ ∇ ⍵
     }0⊃⍵
     r⊣A.DeleteElement p
 }

Both Create and Run cover the New function (passing through the right argument) :

New←{
     w←(
         'Title' 'Progress'
         'Iterations' 0
         'Truncate' 0
         'UpdatePeriod' 1
         'Width' '30rem'
         'Status' 'Working...'
         'LanguageTable'(0 2⍴⊂'')
         'DefaultLanguageTable'[
             'Pause' 'Pause'
             'Cancel' 'Cancel'
             'Resume' 'Resume'
             'Truncate' 'Truncate'
             'Paused' 'Paused'
         ]
     )A.Default ⍵
     w.Iterations←(2=≡w.Status)⊃w.Iterations(≢w.Status)
     s←A.New'p'
     s.id←'progress-bar-status'
     p←A.New'progress'
     p.id←'progress'
     p.max←w.Iterations
     p.value←'0'
     fp←A.New'p'(w A.Translate'Paused')
     fp.id←'progress-paused-indicator'
     fp.class←'pulse invisible'
     w.BodyContent←s,((0<w.Iterations)/p),fp
     w.Buttons←1 1 w.Truncate/w A.Translate'Cancel' 'Pause' 'Truncate'
     w.OnClose←A.FQP'OnClose'
     w.OnClick←A.FQP¨1 1 w.Truncate/'OnCancel' 'OnPause' 'OnTruncate'
     d←A.DialogBox.New w
     d.id←'progress-bar'
     d.(Iteration Result)←¯1 ⍬
     d.(Done Paused Canceled Truncated)←0
     f←2⊃d.Content
     f.Unqueued←'click'
     f.Content.id←1 1 w.Truncate/'progress-'∘,¨'cancel' 'pause' 'truncate'
     d.Unqueued←'close'
     d.PauseToken←⎕TID
     d
 }

Here we can see the various options. The Iterations property defaults to 0, which indicates that we don't know the number of iterations ahead of time. If 0, then no actual graphical bar is displayed, only a status message.

The Truncate property indicates whether or not a Truncate option is offered in addition to Cancel and Resume when the work is paused.

The UpdatePeriod property determines how often the progress bar should be updated. It defaults to 1, meaning at every iteration. For loops with large numbers of iterations and small amounts of work in each iteration, it may make sense to set this higher.

The Status property is either a simple character vector or a vector of vectors. This property is only applicable when the Run operator is used, as in the case of Create and Close, the Update function is called explicitly by the programmer and passed the status as its argument. More on this below.

Let's look a little closer at the Run operator, which has changed significantly. It is called like so:

      rc rv←d f ProgressBar.Run s p 

Here f is the iteration function, s is an appropriate right argument to f, and p is an appropriate right argument to ProgressBar.New, and d is just a reference to the document. The result rc is a return code of 0 for complete, 1 for cancel, and 2 for truncate. The result rv is a vector of the results of f, one item for each iteration.

The iteration function f is passed a reference to the progress bar as its left argument. This lets f do and access a few important things. First, for the indeterminate case, allows f to set the Done property to 1, to indicate that no more iterations are necessary, and end the process. Second, it allows f to set the Status property to update the progress bar text. Finally, it allows f to reference the current iteration number.

The right argument to f may be anything, It might be a simple file tie number, or it could be a namespace. If the latter, in addition to providing a nice way for many named inputs to f, it provides storage for accumulating or aggregating results between iterations. Note that the progress bar itself, the left argument, can also provide storage that survives iterations.

The iteration function f must have a result. This explicit result may or may not be significant or useful, as the important result may be implicitly available in s.

As noted, the Status property may be set by f on each iteration. However, when the number of iterations is known, the status property may be set at startup to a vector of vectors, one item for each iteration. This defines the number of iterations and overrides the value of the Iterations property. The Run operator then automatically updates the status on each iteration.

There is an additional major change to the UI. Instead of a single Pause button that then presents a ConfirmBox with 'Cancel', 'Resume', and Truncate buttons, we provide Cancel, Pause, and Truncate buttons on the main progress element, avoiding the additional modal element. This means that Cancel and Truncate are always available and execute without additional confirmation. (Note that if using Create/Update/Close rather than Run, the programmer is in complete control of the loop and can add confirmation boxes when desired.) The Pause button switches to Resume and back as necessary. We generally don't like buttons that change captions, and we may change this, but in this case it seems reasonable.

Finally, with this version of the ProgressBar component, we introduce the LanguageTable property. This provides a way for the programmer to specify any or all of the visible text elements of the component. This may be used for language support, or simply to change the label of a particular element to something more appropriate for the task at hand.

You can see many examples of the ProgressBar component in the CSVEditor application.

Working With Components

August 29, 2025

Now that we have a few components, we need some convenient ways to manipulate them. Components are just nodes in the APL DOM with a Name property. Note the uppercase N, not to be confused with the HTML name attribute. Currently the existence of Name in an element is the only thing that identifies it as a component.

We can get a component by name, using the GetComponent function:

GetComponent←{
     ⍝ ⍺ ←→ APL DOM node
     ⍝ ⍵ ←→ Component Name (or already a component (pass through)
     9=⎕NC'⍵':⍵
     e←⍺ GetElementsWith'Name'
     i←e.Name⍳⊂⍵
     i=≢e:6 ⎕SIGNAL⍨'Component "',⍵,'" not found'
     i⊃e
 }

The left argument is a DOM node, perhaps the whole document, or more likely a <dialog> element. the right argument is the name of the component, or if a namespace, assumed to be a component already, and just passed though. This is a convenience for other functions that accept either a component or a component name as an argument. We simply find all the elements in the node with a Name property and pick the one we want.

We can get a namespace with all of the components in a node:

GetComponents←{
     ⍝ ⍵ ← APL DOM node
     ⍝ ⍺ ←→ 0 for namespace result, 1 for array
     ⍝ ← ←→ Namespace of components
     ⍺←0
     e←⍵ GetElementsWith'Name'
     ⍺:e
     0=≢⍵:()
     ()⎕VSET(↑e.Name)e
 }

This allows us to access all the components under a node by name with dot syntax (or get an array of components back).

We can set the value of a component using SetComponentValue:

SetComponentValue←{
     ⍝ ⍺ ←→ APL DOM Node
     ⍝ ⍵ ←→ (Name|Component) Value
     ⍺←⊢
     c←⍺ GetComponent 0⊃⍵
     c(⍎c.class).SetValue 1⊃⍵
 } 

Here the right argument is name or component itself, and the value. If the name is provided, then the left argument must be provided. If the component is provided, the left argument should be omitted. There is no need for a corresponding GetComponentValue function, as the value may be directly accessed with dot syntax.

We also have GetComponentValues and SetComponentValues, which we have introduced before but now renamed to reflect that they work on components. These functions take or return a namespace of values. Finally we have SetComponentDisabled for making a component, in ⎕WC terms, active or inactive:

SetComponentDisabled←{
     ⍝ ⍺ ←→ DOM node
     ⍝ ⍵ ←→ Name or Component name, Boolean 1/0
     ⍺←⊢
     c←⍺ GetComponent 0⊃⍵
     e←Elements c
     d←e/⍨e.Tag∊'button' 'fieldset' 'optgroup' 'option' 'select' 'textarea' 'input'
     0=≢d:0
     0⊣d{⍺ SetBoolean'disabled'⍵}¨1⊃⍵
 }

The explicit list of HTML elements are those that take the disabled attribute.

Let's play around with the functions. Consider a dialog box with a few input fields:

OnFileSettings←{
     d←⍵.Document
     s←A.FieldSet.New''
     i←s A.DropList.New'Separator' 'Separator:' ''('Comma' 'Pipe' 'Tab')
     i←s A.DropList.New'TextQualifier' 'Text qualifier:'('Options'('DoubleQuote' 'Quote')
     i←s A.NumberInput.New'MaxRows' 'Maximum Rows:' 256 'CI15'
     i←s A.CheckBox.New'AutoConvert' 'Auto convert'
     _←d.FileSettings A.InitValues s
     p←⎕NS''
     p.Title←'CSV Import Settings'
     p.Contents←s
     p.OnOK←A.FQP'OnFileSettingsOK'
     b←A.DialogBox.New p
     d A.ShowModal b
 }

When the OnFileSettingsOK callback fires we are given the <dialog> element as an argument. Let's call this d. Then we can grab a component and see its value:

      c←d A.GetComponent 'Separator'
      c.Value
Pipe

Then set its value, updating the APL DOM and the browser:

      A.SetComponentValue c 'Comma' 
0     
      c.Value
Comma

A namepsace of components:

      A.GetComponents d
      p←A.GetComponents d
      p.MaxRows.Value
1024
      p.AutoConvert.Value
1

Get a namespace of values, make some changes, refresh the screen:

      v←A.GetComponentValues d 
      ⎕JSON v
{"AutoConvert":1,"MaxRows":1024,"Separator":"Comma","TextQualifier":"DoubleQuote"}
      v.(AutoConvert MaxRows)←1 1234567
      v A.SetComponentValues d
0
      v←A.GetComponentValues d
      ⎕JSON v
{"AutoConvert":0,"MaxRows":1234567,"Separator":"Pipe","TextQualifier":"Quote"}

An array of components:

      a←1 A.GetComponents d
      a.Name
 Separator  TextQualifier  MaxRows  AutoConvert 
      a.Value
 Pipe  Quote  1234567 0

Deactivate all the components:

      A.SetComponentDisabled¨a,¨1 
0 0 0 0

We have used these verbose functions names, all including the word component, to distinguish them from the functions used to operate on plain elements, as everything sits all together in the #.Abacus.Main namespace. It would be nice to just use properties with real classes and dispense with exposing all these functions, and we may do that in the future. We are going to see how many functions we will need to work with components over the coming months.

On Arguments

August 15, 2025

In APL we can have at most two arguments to a function, and , left and right.

This is good, because we should never have more than two argument to a function. In fact that is usually one too many. However real life gets in the way, and we often find ourselves in need of more. With nested arrays, namespaces, and simply the interpretation of, say, a simple numeric vector of length 3, the meaning of "one" is in the eye of the beholder.

If we have a vector of expenses as the right argument to a functions that sums them up, we say the right argument, , is a vector of expenses. It's one thing. On the other hand, if we have a function that computes the level payment of a mortgage, it takes a term, a balance, and a rate. Three things. If we pass these as the right argument , we say the function takes three arguments. This is not strictly true. It takes one argument, a vector of three items. Informally we might often speak of 3, 4, 5 or more arguments but we really mean, usually, are distinct items of a possibly nested vector.

Let's use the word parameter to refer to an item of the argument when the argument consists of unique, identifible, nameable, elements.

In our hypothetical mortgage function the first line might look like:

      (b r t)←⍵

Here we have unpacked the argument into 3 parameters b, r and t.

Often we want to have some of the less important parameters be optional and default to a given value. To make the term optional and default to 360 we might do:

      (b r t)←3↑⍵,360

With two or three parameters this technique is manageable, but with more it becomes unwieldy. First, we get a proliferation of usually ad hoc named local variables. Second we can only default trailing parameters.

What can we do about this?

We can take a page from the design of Dyalog's venerable ⎕WC and use named parameters. This is what we have done in Abacus for components. This technique is useful for the pulbic API of libraries that will be used by other programmers. It's probably overkill for private functions.

This technique has many benefits:

We take a strict approach to a vector argument: the tally is always the number of parameters provided. Therefore when providing only one parameter it usually must be enclosed, the exception being a scaler valued parameter provided with no name.

Consider this contrived example:

Sum←{
     p←(
         'One' 1
         'Two' 2
         'Three' 3
         'Four' 4
         'Five' 5
     )Default ⍵
     +/p.(One Two Three Four Five)
 }

First we layout all the parameters, each on its own line, in order, with their default values, using V20 array notation. It is important that we use an array rather than a namespace for the default values, as order matters. This array is passed as the left argument to our Default utility function, which takes the user provided parameters as its right argument:

Default←{
     ⍝ ⍺ ←→ Default name/value pairs
     ⍝ ⍵ ←→ Given argument
     ⍝ ← ←→ A new space with ⍺ overiddden by ⍵
     d←()⎕VSET ⍺
     9=⎕NC'⍵':d ⎕NS ⍵
     n←' '~⍨¨(≢⍵)↑⊃¨⍺
     p←n{(2=≢⍵)∧1≠≡⍵:⍵
         ⍺≡'':⍺
         ⍺ ⍵}¨⍵
     d ⎕NS()⎕VSET p~⊂''
 }

The first thing we do is create a new namespace d with all of the default name/value pairs . This will be our result in all cases.

Then, if we are given a namespace as the right arg, we inject the user supplied names over the default names, and return the space d; we are done. (This uses another nice feature of V20, which finally allows a reference as the left argument to ⎕NS.) Otherwise is a vector where each item is either a value or a name/value pair. If an item is only a value, we assume the name based on its position. These names are then injected into d, overriding defaults, and d is returned.

We can call the Sum function in all of the following ways (We have inserted ⎕←⎕JSON p to see what is going on):

      Sum ''
{"Five":5,"Four":4,"One":1,"Three":3,"Two":2}
15
      Sum 100 ('Five' 50)
{"Five":50,"Four":4,"One":100,"Three":3,"Two":2}
159
      Sum  ('Three' 333)  ('DoesNot' 'Exist')
{"DoesNot":"Exist","Five":5,"Four":4,"One":1,"Three":333,"Two":2}
345
      Sum  (Three:333 ⋄ Two:222)
{"Five":5,"Four":4,"One":1,"Three":333,"Two":222}
565

This Default function does no error checking. Mispelled names (not useful) or additional names (often useful) are happily are accepted. There is no type checking, and no checking for whether or not a parameter is optional. This is fine for the purposes of Abacus, where we assume consenting adults are using the library.

Some checking can be done from the name alone. But if we want to check for mandatory parameters or types, we need to add more items to our default vector. To check for mandatory parameters we can add an optional Boolean:

p←(
         'One' 1 1
         'Two' 2 1
         'Three' 3
         'Four' 4
         'Five' 5
     )

Here we have specified that parameters One and Two are required. Now we need to add some code to the default function to do some checking. We take the approach of adding a new function, rather than mucking up our existing code too much:

Default←{
     ⍝ ⍺ ←→ Default name/value pairs  (Optional 1 for required)
     ⍝ ⍵ ←→ Given argument
     ⍝ ← ←→ A new space with ⍺ overiddden by ⍵
     d←()⎕VSET 2↑¨⍺
     9=⎕NC'⍵':d ⎕NS ⍺ Verify ⍵
     n←' '~⍨¨(≢⍵)↑⊃¨⍺
     p←n{(2=≢⍵)∧1≠≡⍵:⍵
         ⍺≡'':⍺
         ⍺ ⍵}¨⍵
     d ⎕NS ⍺ Verify()⎕VSET p~⊂''
 }

where the Verify function is:

Verify←{
     (n v r)←↓⍉↑3↑¨⍺,¨0
     m←⍵.⎕NL ¯2
     ~∧/m∊⍨r/n:11 ⎕SIGNAL⍨'Required parameter: ',⊃r/n
     0≠≢m~n:11 ⎕SIGNAL⍨'Invalid parameter name: ',⊃m~n
     ⍵
 }

Additional type information could be specified and checked for. Here we begin to run into meta problem of needing parameter names for our parameter specification. Ugh. Let's stay away from that.

More posts...