Chapters

  1. Introduction
  2. Structure
  3. Packaging
  4. Logging
  5. Configuration
  6. Debugging EXEs
  7. Handling errors
  8. Testing
  9. Documentation
  10. Make
  11. Providing help
  12. Scheduled Tasks
  13. Windows Services
  14. Windows Event Log
  15. Windows Registry
  16. Creating SetUp.exe
  17. Regular Expressions
  18. Acre
  19. GUI
  20. Git

Appendices

  1. Windows environment vars
  2. User commands
  3. aplcores & WS integrity
  4. Development environment
  5. Special characters

Misc

In this chapter we consider your choices for making your program available to others, and for taking care of the source code, including tracking the changes through successive versions.

To follow this, we’ll make a very simple program. It counts the frequency of letters used in one or multiple text files. (This is simple, but useful in cryptanalysis, at least at hobby level.) We’ll put the source code under version control, and package the program for use.

Some of the things we are going to add to this application will seem like overkill, but keep in mind that we use this application just as a very simple example for all the techniques we are going to introduce.

Let’s assume you've done the convenient thing. Your code is in a workspace. Everything it needs to run is defined in the workspace. Maybe you set a latent expression, so the program starts when you load the workspace.

We shall convert a DWS to some DYALOG scripts and introduce a DYAPP script to assemble an active workspace from them.

Using scripts to store your source code has many advantages: you can use a traditional source code management system rather than having your code and data stored in a binary blob.

Changes that you make to your source code are saved immediately, rather than relying on you to remember to save the workspace at some suitable point in your work process.

Finally, you don’t need to worry about crashes in your code or externally called modules and also any corruption of the active workspace which might prevent you from saving it.

Corrupted workspaces

The workspace (WS) is where the APL interpreter manages all code and all data in memory. The Dyalog tracer/debugger has extensive edit-and-continue capabilities; the downside is that these have been known to corrupt the workspace occasionally.

The interpreter checks WS integrity every now and then; how often can be influenced by setting certain debug flags; see the “Appendix 3 — aplcores and WS integrity” for details. More details regarding aplcores are available in the appendix “Aplcores”.

Could not be simpler. If your user has a Dyalog interpreter, she can also save and send you the crash workspace if your program hits an execution error. But she will also be able to read your code – which might be more than you wish for.

Crash workspaces

A crash workspace is a workspace that was saved by a function that was initiated by error trapping, typically by setting ⎕TRAP. It’s a snapshot of the workspace at the moment an unforeseen problem triggered error trapping to take over. It’s usually very useful to analyse such problems.

Note that a workspace cannot be saved when more than one thread is running.

If she doesn’t have an interpreter and you are not worried about her someday getting one and reading your code, and you have a Run-Time Agreement with Dyalog, you can send her the Dyalog Run-Time interpreter with the workspace. The Run-Time interpreter will not allow the program to suspend, so when the program breaks the task will vanish, and your user won’t see your code. All right so far. But she will also not have a crash workspace to send you.

If your application uses multiple threads, the thread states can’t be saved in a crash workspace anyway.

You need your program to catch and report any errors before it dies, something we will discuss in the chapter Handling errors.

This is the simplest form of the program to install because there is nothing else it needs to run: everything is embedded within the EXE. You export the workspace as an EXE, which can have the Dyalog Run-Time interpreter bound into it. The code cannot be read. As with the workspace-based runtime above, your program cannot suspend, so you need it to catch and report any errors before dying.

We’ll do that!

Let’s start by considering the workspace you will export as an EXE.

The first point is: PCs have a lot of memory relative to your application code volume. So all your Dyalog code will be in the workspace. That’s probably where you have it right now anyway.

Your workspace is like your desktop – a great place to get work done, but a poor place to store things. In particular it does nothing to help you track changes and revert to an earlier version.

Sometimes a code change turns out to be for the worse, and you need to undo it. Perhaps the change you need to undo is not the most recent change.

We’ll keep the program in manageable pieces – ‘modules’ – and keep those pieces in text files under version control.

For this, there are many source-control management (SCM) systems and repositories available. Subversion, Git and Mercurial are presently popular. These SCMs support multiple programmers working on the same program, and have sophisticated features to help resolve conflicts between them.

Source code management with acre Desktop

Some members of the APL community prefer to use a source-code management system that is tailored to solve the needs of an APL programmer, or a team of APL programmers: acre Desktop.

APL code is very compact, teams are typically small, and work on APL applications tends to be oriented towards functions rather than modules such as classes.

acre Desktop can be used as a source-code management system in its own rights= together with acre Server, but it can use other code management systems like Git or SubVersion as well. Both acre Desktop and acre Server are available as open source software. We will discuss acre in its own appendix.

Whichever SCM you use (we used GitHub for writing this book and the code in it) your source code will comprise class and namespace scripts (DYALOGs) for the application. The help system will be an ordinary – non-scripted – namespace. We use a build script (DYAPP) to assemble the application and the development environment.

You’ll keep your local working copy in whatever folder you please. We’ll refer to this working folder as Z:\ but it will of course be wherever suits you.

We suppose you already have a workspace in which your program runs. We don’t have your code to hand so we’ll use ours. We’ll use a very small and simple program, so we can focus on packaging the code as an application, not on writing the application itself.

So we’ll begin with the LetterCount workspace. It’s trivially simple but for now it will stand in for your code. You can download it from the book’s web site: https://cookbook.dyalog.com.

On encryption

Frequency counting relies on the distribution of letters being more or less constant for any given language. It is the first step in breaking a substitution cypher.

Substitution cyphers have been superseded by public-private key encryption, and are mainly of historical interest, or for studying cryptanalysis. But they are also fun to play with.

We recommend The Code Book: The secret history of codes & code-breaking by Simon Singh and In Code by Sarah Flannery as introductions if you find this subject interesting.

In real life, you will produce successive versions of your program, each better than the last. In an ideal world, all your users will have and use the current version. In that ideal world, you have only one version to maintain: the latest.

In the real world, your users will have and use multiple versions. If you charge for upgrading to a newer version, this will surely happen. And even in your ideal world, you have to maintain at least two versions: the current and the next.

What does it mean to maintain a version? At the very minimum, you keep the source code for it, so you could recreate its EXE from scratch, exactly as it was distributed. There will be things you want to improve, and perhaps bugs you must fix. Those will all go into the next version, of course. But some you may need to put into the released version and re-issue it to current users as a patch.

So in The Dyalog Cookbook we shall develop in successive versions until we manage to create an installer that is capable of installing the application on any machine running Windows 10. What’s needed to achieve that is discussed in the chapters 1-16. Later chapters are independent.

Our ‘versions’ are not ready to ship, so are probably better considered as milestones on the way to version 1.0. You could think of them as versions 0.1, 0.2 and so on. But we’ll just refer to them as Versions 1, 2, and so on.

Our first version won’t even be ready to export as an EXE. It will just create a workspace MyApp.dws from scripts: a DYAPP and some DYALOGs. We’ll call it Version 1.

Load the LetterCount.dws workspace from the code\foo folder on the book website. Again, this is just the stand-in for your own code. Here’s a quick tour.

Let’s load the workspace LetterCount and investigate it a bit.

Function TxtToCsv takes the filepath of a TXT and writes a sibling CSV [1] containing the frequency count for the letters in the file. It uses function CountLetters to produce the table.

      ∆←'Now is the time for all good men'
      ∆,←' to come to the aid of the party.'
      CountLetters ∆
N 2
O 8
W 1
I 3
S 1
T 7
H 3
E 6
M 3
F 2
R 2
A 3
L 2
G 1
D 2
C 1
P 1
Y 1
Information

Note that we use a variable here. Not exactly a memorable or self-explaining name. However, we use whenever we collect data for temporary use.

CountLetters returns a table of the letters in ⎕A and the number of times each is found in the text. The count is insensitive to case and ignores accents, mapping accented to unaccented characters:

      Accents
ÁÂÃÀÄÅÇÐÈÊËÉÌÍÎÏÑÒÓÔÕÖØÙÚÛÜÝ
AAAAAACDEEEEIIIINOOOOOOUUUUY

That amounts to five functions. Two of them are specific to the application: TxtToCsv and CountLetters. The other three –– toUppercase, join and map — are utilities of general use.

Note that we have some functions that start with lowercase characters while others start with uppercase characters. In a larger application you might want to be able to tell data from calls to functions and operators by introducing consistent naming conventions. Which one you settle for is less important than choosing something consistent. And remember to put it into a document any programmer joining the team can read.

toUppercase uses the fast case-folding I-beam introduced in Dyalog 15.0 (also available in 14.0 & 14.1 from revision 27141 onwards).

TxtToCsv uses the file-system primitives ⎕NINFO, ⎕NGET, and ⎕NPUT introduced in Dyalog 15.0.

To expand this program into distributable software we’re going to add features, many of them drawn from the APLTree library. To facilitate that we’ll first organise the existing code into script files, and write a build script to assemble a workspace from them.

Information

The APLTree library is an open-source project hosted on GitHub. It offers solutions for many every-day problems a Dyalog APL programmer might run into. In the Cookbook we will use many of its members. For details see https://github.com/aplteam/apltree/wiki.

Start at the root namespace (#). We’re going to be conservative about defining names in #. Why? Right now the program stands by itself and can do what it likes in the workspace. But in the future your program might become part of a larger APL system. In that case it will share # with other objects you don’t know anything about right now.

So your program will be a self-contained object in #. Give it a distinctive name, not a generic name such as Application or Root. From here on we’ll call it MyApp. (We know – almost as bad.)

But there are other objects you might define in #. If you’re using classes or namespaces that other systems might also use, define them in #. For example, if MyApp should one day become a module of some larger system, it would make no sense for each module to have its own copy of, say, the APLTree class Logger.

With this in mind, let’s distinguish some categories of code, and how the code in MyApp will refer to them.

General utilities and classes
For example, the APLTreeUtils namespace and the Logger class. (Your program doesn't yet use these utilities.) In the future, other programs, possibly sharing the same workspace, might use them too.
Your program and its modules
Your top-level code will be in #.MyApp. Other modules and MyApp-specific classes may be defined within it.
Tools and utility functions specific to MyApp
These might include your own extensions to Dyalog namespaces or classes. Define them inside the app object, e.g. #.MyApp.Utils.
Your own language extensions and syntax sweeteners
For example, you might like to use functions means and else as simple conditionals. These are effectively your local extensions to APL, the functions you expect always to be around. Define your collection of such functions into a namespace in #, eg #.Utilities.

The object tree in the workspace might eventually look something like:

#
|-⍟Constants
|-⍟APLTreeUtils
|-⍟Utilities
|-○MyApp
| |-⍟Common
| |-⍟Engine
| |-○TaskQueue
| \-⍟Utils
\-○Logger
\-⍟UI
Information

denotes a namespace, a class. These are the characters (among others) you can use to tell the editor what kind of object you wish to create, so for a class )ed ○ Foo. Press F1 with the cursor on )ed in the session for details.

Note that we keep the user interface (UI) separate from the business logic. This is considered good practice because whatever you believe right now, you will almost certainly one day consider exchanging a particular type of UI (say .NET Windows forms) for a different one (say HTML+JavaScript). This is difficult enough, but a bit easier when you separate them right from the start. However, our application is so simple that we collect all its code in a namespace script MyApp in order to save one level in the namespace hierarchy.

If this were to be a serious project then you would not do this even if the amount of code is small: applications change and grow over time, sometimes significantly. Therefore you would be better prepared to have, say, a namespace MyApp that contains, say, a namespace script engine with all the code.

The objects in # are ‘public’. They comprise MyApp and objects other applications might use; you might add another application that uses #.Utilities. Everything else is encapsulated within MyApp. Here’s how to refer in the MyApp code to these different categories of objects.

  1. log←⎕NEW #.Logger
  2. queue←⎕NEW TaskQueue
  3. tbl←Engine.CountLetters txt
  4. status←(bar>3) #.Utilities.means 'ok' #.Utilities.else 'error'

The last one is pretty horrible. It needs some explanation.

Many languages offer a short-form syntax for if/else, eg (JavaScript, PHP, C…)

status = bar>3 ? 'ok' : 'error' ;

Some equivalents in Dyalog:

What style you prefer is mainly a matter of personal taste, and indeed even the authors do not necessarily agree on this. There are however certain rules you should keep in mind:

status←(bar>3) U.means 'ok' U.else 'error'

In this approach two user-defined functions are called. Not much overhead but don’t go for this if the line is, say, executed thousands of times within a loop.

The authors have done pair programming for years with end users being the second party. For a user a statement like:

taxfree←(dob>19491231) U.means 35000 U.else 50000

is easily readable despite it being formed of APL primitives and user-defined functions. This can be a big advantage in an agile environment where the end user reviews business logic with the implementors.

For classes, however, there is another way to do this: include the namespace #.Utilities. In order to illustrate this let’s assume for a moment that MyApp is not a namespace but a class.

:Class MyApp
:Include Utilities
…
:EndClass

This requires the namespace #.Utilities to be a sibling of the assumed class MyApp. Now within the class you can do

status←(bar>3) means 'ok' else 'error'

yet Shift+Enter in the Tracer or the Editor still works, and any changes made to the utilities would go into #.Utilities.

More about :Include

When a namespace is included, the interpreter will execute functions from that namespace as if they had been defined in the current class. However, the actual code is shared with the original namespace. For example, this means that if the code of means or else is changed while tracing into it from the MyApp class those changes are reflected in #.Utilities immediately (and any other classes that might have included it).

Most of the time, this works as you expect it to, but it can lead to confusion, in particular if you were to )COPY #.Utilities from another workspace. This will change the definition of the namespace, but the class has pointers to functions in the old copy of #.Utilities, and will not pick up the new definitions until the class is fixed again.

If you were to edit these functions while tracing into the MyApp class, the changes will not be visible in the namespace. Likewise, if you were to )ERASE #.Utilities, the class will continue to work until the class itself is edited, at which point it will complain that the namespace does not exist.

Let’s assume that in a WS C:\Test_Include we have just this code:

:Class Foo
:Include Goo
:EndClass

:Namespace Goo
∇ r←Hello
    :Access Public Shared
      r←'World'
    ∇
:EndNamespace

Now we do this:

Foo.Hello
world
      )Save
Saved...
      ⎕EX 'Goo'
      Goo
VALUE ERROR
      Foo.Hello
world
)copy c:\Test_Include Goo
copied...

If at this stage you were to edit Goo and change 'world' to 'Universe', and then again call Foo.Hello it would still print world to the session.

If you encounter this, re-fix your classes (in this case Foo). Rebuilding the WS from the source files would be even better.

The :If - :Then - :else solution could have been written this way:

:If bar>3 ⋄ status←'ok' ⋄ :Else ⋄ status←'error' ⋄ :EndIf

There is one major problem with this: when executing the code in the Tracer the line will be executed in one go. If you think you might want to follow the control flow and trace into the individual expressions, you should spread the control structure over 5 lines.

In general: if you have a choice between a short and a long expression then go for the short one – unless the long one offers an incentive such as improved readability, better debugging or faster execution speed.

Diamonds can be useful in some situations. In general it’s better to avoid them.

Diamonds

In some circumstances diamonds are quite useful:

  • To ensure no thread switch occurs between two statements. Something like
    tno←filename ⎕nTIE 0 ⋄ l←⍴⎕nread tno 80 (⎕nsize tno) ⋄ ⎕nuntie tno

    is sure to be executed as a unit. In some circumstances this really matters.

  • Make multiple assignments on a single line as in ⎕IO←1 ⋄ ⎕ML←3 ⋄ ⎕PP←20. Not for variable settings, just system stuff.
  • Assignments to ⎕LX as in ⎕LX←#.FileAndDirs.PolishCurrentDir ⋄ ⎕←Info.
  • To make dfns more readable as in {w←⍵ ⋄ ((w='¯')/w)←'-' ⋄ ⍵}. There is really no reason to make this a multi-line dfn.

    (Note that from version 16 onwards you can achieve the same result with {'-'@(⍸⍵='¯')⊣⍵})

  • You cannot trace into a one-line dfn. This can be quite useful. For example, this function:
    OnConfigure←{(4↑⍵),((⊃⍺){(0∊⍴⍺):⍵ ⋄ ⍺⌈⍵}⍵[4]),((⊃⌽⍺){(0∊⍴⍺):⍵ ⋄ ⍺⌈⍵}⍵[5])}

    ensures a GUI Form (window) does not shrink below a minimum size defined by .

    You don’t want to have a multi-line dfn here: you want to be able to trace into any ⎕DQ (or Wait) statement; and the number of configuration events is simply overwhelming. Thanks to the we can solve the task on a single line and prevent the Tracer from ever entering the dfn.

⎕PATH tempts us. We could set ⎕PATH←'#.Utilities'. The expression above could then take its most readable form:

status←(bar>3) means 'ok' else 'error'

Trying to resolve the names means and else, the interpreter would consult ⎕PATH and find them in #.Utilities. So far so good: this is what ⎕PATH is designed for. It works just fine in simple cases but quickly leads to confusion about which functions are called or edited, and where new names are created. Avoid ⎕PATH if reasonable alternatives are available.

If your own application is already using scripted namespaces and/or classes then you can skip this, of course.

Download the WS and save it as Z:\code\v00\LetterCount.

Everything in that WS lives in #. We have to move it into a single namespace MyApp. Execute the following steps:

  1. Start an instance of Dyalog
  2. Execute )ns MyApp to create a namespace MyApp in the workspace.
  3. Execute )cs MyApp to make MyApp the current namespace.
  4. Execute )copy Z:\code\v00\LetterCount to copy all the functions and the single variable into the current namespace, #.MyApp.
  5. Execute )copy Z:\code\v00\LetterCount ⎕IO ⎕ML

    This ensures we really use the same values for important system variables as the WS does by copying their values into the namespace #.MyApp.

  6. Execute ]save #.MyApp Z:\code\v01\MyApp -makedir -noprompt

The last step will save the contents of the namespace #.MyApp into Z:\code\v01\MyApp.dyalog. If the folder v01 or any of its parents do not already exist the -makedir option will create them. -noprompt makes sure that ]save does not ask any questions.

This is how the script would look like:

:Namespace MyApp
⍝ === VARIABLES ===

Accents←2 28⍴'ÁÂÃÀÄÅÇÐÈÊËÉÌÍÎÏÑÒÓÔÕÖØÙÚÛÜÝAAAAAACDEEEEIIIINOOOOOOUUUUY'


⍝ === End of variables definition ===

(⎕IO ⎕ML ⎕WX ⎕PP ⎕DIV)←1 1 3 15 1

 CountLetters←{
     ⍝ Table of letter frequency in txt
     {⍺(≢⍵)}⌸⎕A{⍵/⍨⍵∊⍺}(↓Accents)map toUppercase ⍵
 }

∇ noOfBytes←TxtToCsv fullfilepath;NINFO_WILDCARD;NPUT_OVERWRITE;tgt;files;path;stem;txt;enc;nl;lines;csv
     ⍝ Write a sibling CSV of the TXT located at fullfilepath,
     ⍝ containing a frequency count of the letters in the file text.
 NINFO_WILDCARD←NPUT_OVERWRITE←1 ⍝ constants
 fullfilepath~←'"'
 csv←'.csv'
 :Select 1 ⎕NINFO fullfilepath
 :Case 1 ⍝ folder
     tgt←fullfilepath,'\total',csv
     files←⊃(⎕NINFO⍠NINFO_WILDCARD)fullfilepath,'\*.txt'
 :Case 2 ⍝ file
     (path stem)←2↑⎕NPARTS fullfilepath
     tgt←path,stem,csv
     files←,⊂fullfilepath
 :EndSelect
     ⍝ assume txt<<memory
 (txt enc nl)←{(⊃,/1⊃¨⍵)(1 2⊃⍵)(1 3⊃⍵)}⎕NGET¨files
 lines←','join¨↓⍕¨CountLetters txt
     ⍝ use encoding and NL from first source file
 noOfBytes←(lines enc nl)⎕NPUT tgt NPUT_OVERWRITE
     ⍝Done
∇

 join←{
     ⍺←⎕UCS 13 10
     (-≢⍺)↓⊃,/⍵,¨⊂⍺
 }

 map←{
     (old new)←⍺
     nw←∪⍵
     (new,nw)[(old,nw)⍳⍵]
 }

 toUppercase←{1(819⌶)⍵}

:EndNamespace

There might be minor differences, depending on the version of the ]save user command and the version of SALT you are using.

This is the easiest way to convert an ordinary workspace into one or more scripted namespaces.

Now we start improving it.

We’ll raid Project Gutenberg for some texts to read.

We’re tempted by the complete works of William Shakespeare but we don’t know that letter distribution stayed constant over four centuries. Interesting to find out, though, so we’ll save a copy as Z:\texts\en\shakespeare.dat. And we’ll download some 20th-century books as TXTs into the same folder. Here are some texts we can use.

      ↑⊃(⎕NINFO⍠'Wildcard' 1) 'z:\texts\en\*.txt'
z:/texts/en/ageofinnocence.txt
z:/texts/en/dubliners.txt
z:/texts/en/heartofdarkness.txt
z:/texts/en/metamorphosis.txt
z:/texts/en/pygmalion.txt
z:/texts/en/timemachine.txt
z:/texts/en/ulysses.txt
z:/texts/en/withthesehands.txt
z:/texts/en/wizardoz.txt

We’ll first make MyApp a simple 'engine' that does not interact with the user. Many applications have functions like this at their core. Let’s enable the user to call this engine from the command line with appropriate parameters. By the time we give it a user interface, it will already have important capabilities, such as logging errors and recovering from crashes.

Our engine will be based on the TxtToCsv function. It will take one parameter, a fully qualified filepath for a folder or file. If it is a file it will write a sibling CSV. If it is a folder it will read all the TXTs in the folder, count the letter frequencies and write them as a CSV sibling to the folder. Simple enough. Here we go.

In your text editor open a new document.

You need a text editor that handles Unicode. If you’re not already using a Unicode text editor, Windows’ own Notepad will do for occasional use. (Set the default font to APL385 Unicode)

For a full-strength multifile text editor Notepad++ works well but make sure that the editor converts Tab into space; by default it does not, and Dyalog does not like Tab characters.

You can even ensure Windows calls Notepad++ when you enter notepad.exe into a console window or double-click a TXT icon: Google for “notepad replacer”.

Here’s how the object tree will look like:

#
|-⍟Constants
|-⍟Utilities
\-⍟MyApp

We’ve saved the very first version as z:\code\v01\MyApp.dyalog. Now we take a copy of that and save it as z:\code\v02\MyApp.dyalog. Alternatively you can download version 2 from the book’s website.

Note that compared with version 1 we will improve in several ways:

The file tree will look like this:

z:\code\v02\Constants.dyalog
z:\code\v02\MyApp.dyalog
z:\code\v02\Utilities.dyalog
z:\code\v02\MyApp.dyapp

MyApp.dyapp looks like this if we take the simple approach:

Target #
Load Constants
Load Utilities
Load MyApp

This is the Constants.dyalog script:

:Namespace Constants
    ⍝ Dyalog constants
    :Namespace NINFO
        ⍝ left arguments
        NAME←0
        TYPE←1
        SIZE←2
        MODIFIED←3
        OWNER_USER_ID←4
        OWNER_NAME←5
        HIDDEN←6
        TARGET←7
        :Namespace TYPES
            NOT_KNOWN←0
            DIRECTORY←1
            FILE←2
            CHARACTER_DEVICE←3
            SYMBOLIC_LINK←4
            BLOCK_DEVICE←5
            FIFO←6
            SOCKET←7
        :EndNamespace
    :EndNamespace
    :Namespace NPUT
        OVERWRITE←1
    :EndNamespace
:EndNamespace

Note that we use uppercase here for the names of the ‘constants’. (They are of course not really constants but ordinary variables.) It is a common programming convention to use uppercase letters for constants.

Information

Later on we’ll introduce a more convenient way to represent and maintain the definitions of constants. This will do nicely for now.

This is the Utilities.dyalog script:

:Namespace Utilities
      map←{
          (old new)←⍺
          nw←∪⍵
          (new,nw)[(old,nw)⍳⍵]
      }
    toLowercase←0∘(819⌶)
    toUppercase←1∘(819⌶)
:EndNamespace

Finally the MyApp.dyalog script:

:Namespace MyApp

   (⎕IO ⎕ML ⎕WX ⎕PP ⎕DIV)←1 1 3 15 1

⍝ === Aliases

    U←##.Utilities ⋄ C←##.Constants

⍝ === VARIABLES ===

    Accents←↑'ÁÂÃÀÄÅÇÐÈÊËÉÌÍÎÏÑÒÓÔÕÖØÙÚÛÜÝ' 'AAAAAACDEEEEIIIINOOOOOOUUUUY'

⍝ === End of variables definition ===

      CountLetters←{
          {⍺(≢⍵)}⌸⎕A{⍵⌿⍨⍵∊⍺}(↓Accents)U.map U.toUppercase ⍵
      }

    ∇ noOfBytes←TxtToCsv fullfilepath;csv;stem;path;files;lines;nl;enc;tgt;tbl
   ⍝ Write a sibling CSV of the TXT located at fullfilepath,
   ⍝ containing a frequency count of the letters in the file text.
      fullfilepath~←'"'
      csv←'.csv'
      :Select C.NINFO.TYPE ⎕NINFO fullfilepath
      :Case C.TYPES.DIRECTORY
          tgt←fullfilepath,'total',csv
          files←⊃(⎕NINFO⍠'Wildcard' 1)fullfilepath,'\*.txt'
      :Case C.TYPES.FILE
          (path stem)←2↑⎕NPARTS fullfilepath
          tgt←path,stem,csv
          files←,⊂fullfilepath
      :EndSelect
      (tbl enc nl)←{(⊂⍪⊃⍵)1↓⍵)}(CountLetters ProcessFiles) files
      lines←{⍺,',',⍕⍵}/⊃{⍺(+/⍵)}⌸/↓[1]tbl
      noOfBytes←(lines enc nl)⎕NPUT tgt C.NPUT.OVERWRITE
    ∇

    ∇(data enc nl)←(fns ProcessFiles) files;txt;file
   ⍝ Reads all files and executes `fns` on the contents. `files` must not be empty.
      data←⍬
      :For file :In files
          (txt enc nl)←⎕NGET file
          data,←⊂fns txt
      :EndFor
    ∇

:EndNamespace

This version comes with a number of improvements. Let’s discuss them in detail:

Warning

If you see any namespaces called SALT_Data ignore them. They are part of how SALT manages meta data for scripted objects.

We have converted the saved workspace to text files, and made a DYAPP that builds the workspace from the DYALOGs. But we have not saved a workspace: we will always build a workspace from scripts.

Launch the DYAPP by double-clicking on its icon in Windows Explorer. Examine the active session. We see

- Constants
  - NINFO
    - NAME
    - ...
    - TYPES
      - NOT_KNOWN
      - DIRECTORY
      - ...
  - NPUT
    - OVERWRITE
- MyApp
  - Accents
  - C
  - CountLetters
  - TxtToCsv
  - U
- Utilities
  - map
  - toLowercase
  - toUppercase

Note that MyApp contains C and U. That shows the code in the script got executed when the WS was built – otherwise they wouldn’t exist. This is nice: when you type #.MyApp.C. then autocomplete pops up and suggests all the names contained in Constants.

We have reached our goal:


Footnotes

  1. With version 16.0 Dyalog has introduced a system function ⎕CSV for both importing from and exporting to CSV files.