Hello, Habr! I'd like to share my experience developing such a system.
The defining parameters of a domain-specific system are:
Domain and characteristic data structures
Set of methods/functions for processing and manipulating data
Means for creating functions and writing scripts that control execution
Domain - processing and visualization of matrices and their sequences (stacks).
Methods - matrix/vector operations, linear algebra, regression, eigenvalue and eigenvector calculations, SVD decomposition ...
The system (a set of console applications) and the library are implemented in Java. Batch files were initially used as control scripts. Recent versions suggest using the jj-preprocessor - "java-JAVA" macro processor for this purpose (see below).
A brief overview of creating console applications and a brief description of the jj-preprocessor are provided. This is the author's translation of the article: https://habr.com/ru/articles/974038/
Data format: ###-format
The unit of data storage is a file, which can contain: a matrix, a stack (a sequence of matrices/"frames"), or multiple stacks. For a matrix to be read by the system, a header must be added:
### name [ M, N, K ] - where: M is the number of rows, N - number of columns, K - "frames"
Minimum header: ###[] - the system will automatically calculate the number of columns in the first row and the number of rows until the end of the file or the next header.
For large objects, the binary ###-format is supported. CSV files can be converted to ###-format.
Java and Python libraries are provided for ###-format input/output.
Methods/functions = system commands = console applications
All system commands are executable files ( .exe on Windows ).
There are several commands that aren't console applications, they're related to graphics:
2D-graphics based on JAVA Swing, and 3D-graphics based on JavaFX.
Stack input/output (a matrix = a stack with one "frame") is sent to the standard stream, which can be redirected to a file or fed as input to the next command. This allows command calls to be combined into pipelines. While the pipeline is running, intermediate files exist in memory, but they can be saved to disk if needed.
Commands can accept the standard stream( stdin ), files, and parameters as input. A typical pipeline:
cmd1 a b | cmd2 c - @file | cmd3 d - d:3 -:2 ... | cmdN - p > File
where: '-' denotes the standard stream: the first stack of stdin or '-:1';
-:k - the k-th stack in stdin ;
d:k - the k-th stack in file d;
@file - saves stdout ( the intermediate file ) to file: save.
The set of commands/applications is not fixed. The user can extend the functionality in any convenient programming language.
Creating a console application in JAVA
The system consists of a set of applications ( currently >60 ) and a library - set of jar-files.
The application consists of a launcher and compiled code. The launcher contains the classpath and start parameters of the JAVA Virtual Machine ( options... ).
All launchers have the same code, which is taken from the reference code( size: 16 KB for Windows, 14 KB for Linux ). The block of parameters( size 272 bytes ) is encrypted. It is entered into the launcher when it is created. The distribution contains only the reference code. Upon installation, all launchers are created according to the configuration file in 1 second.
Launcher functions:
Receive parameters, prepare them for transmission to the application ( parameters with spaces are enclosed in quotation marks, and a JVM system call is constructed );
Launch the JVM with the following parameters: classpath, options, etc. ;
Terminate the launcher or wait for the application to terminate (specified in the configuration).
This technology has proven very convenient. A new application/command is created by calling
a single command:
crex App.java # CReate EXecutable App, steps:
Compile Java code: App.java (JDK/javac must be present on the comp.);
Place App.class in pik3d/lib/usr.jar;
Create a launcher configuration file;
Run the _Launch.exe program, which writes the parameter block to the reference code;
Launcher ( exe file ): App.exe is saved in the current directory.
In fact, you can create a launcher for any Java program or custom configuration.
See the documentation: pik3d_DOC.html.
jj - preprocessor("java - JAVA"): App.jj --> App.java
Almost all applications have the same structure( the name: App will be used for the examples ):
"header":
import
class App
main( arg[] )
"subject":
App ( arg[] ) //constructor
[additional classes, methods, declarations...]
All functionality ("subject") is implemented in the last two points.
The idea of a "small" JAVA ( only "subject") and a "java - JAVA" preprocessor
inevitably comes to mind. "header" can be generated automatically based on the application name.
App.jj looks like this:
{
java code // "subject" uses arg[] from main()
}
Essentially, this is the constructor code: App( String[] arg ).
If needed, specific imports and comments can be added "at the top" and nonpublic classes, methods and properties( may be static, final ...) - "at the bottom."
An example of a command that generates a stack (a sequence of matrices) in reverse order: Reverse.jj :
/* Reverse Stack: > jj Reverse Stk > ktS ! save reverse stack
> jj Reverse Stk | s3d - ! display in reverse */
{
cmd("Reverse", arg, "Stk > ktS"); // check input arg[]
var stk = inStack( arg[0] ); // stk[ kk ][ii][jj]
int k = stk.length;
while(k-->0) ttFrame( stk[k], arg[0]+"_"+(k+1)); // output in reverse
}jj-preprocessor converts Reverse.jj --> Reverse.java
package usr;
import java.io.*;
import java.util.*;
import pik.*;
/* Reverse Stack: > jj Reverse Stk > ktS ! save reverse stack
> jj Reverse Stk | s3d - ! display in reverse */
public class Reverse extends pik.io
{
public static void main( String[] arg ){
try{new Reverse(arg);}
catch(Exception e){e.printStackTrace();eos();}
clott();
}
Reverse( String[] arg ) throws Exception
{
cmd("Reverse", arg, "Stk > ktS"); // check input arg[]
var stk = inStack( arg[0] ); // stk[ kk ][ii][jj]
int k = stk.length;
while(k-->0) ttFrame( stk[k], arg[0]+"_"+(k+1)); // output in reverse
}
}steps of the jj-preprocessor :
adds a "header", creates a class ( Reverse.jj --> Reverse.java ) with a method: main() ;
creates a constructor: Reverse( String[] arg ), builds the correct bracket structure: {{...}} ;
passes the java-file for execution. Java executes it as a "single source file".
If the classpath is specified, a program of any complexity (more than one file) can be called.
Since the jj-preprocessor is part of the system, it knows the classpath of Java and libraries.
When calling Java, the appropriate parameters are substituted. The jj-preprocessor can execute both App.java and App.jj ( the intermediate App.java can be saved ):
> jj File.java [ arg0 arg1 ... ] # execute a Java program
> jj File[ .jj ] [ arg0 arg1 ... ] # execute a jj-script
Furthermore, jj can execute a sequence of Java commands directly from the command line:
> jj "cmd1; cmd2; ..." ( > console prompt)
The sequence of Java commands is converted to Noname.jj:
{
cmd1; cmd2; ...
}
then to Noname.java and passed to Java for execution as a "single source file".
If necessary, you can specify a different name for the Java file and save it (key: /n=file).
Examples:
> jj "tt("Hello World !");" # tt()=System.out.println() > jj "for(int i=0;i<9;i++) tt(\" i=\"+i)" /n=p123 # print i=0/i=1.../i=8 # save: p123.java
Passing Parameters to the Application and the Argument File
When calling: > jj App arg0 arg1 ... Arguments are available at runtime as: arg[0], arg[1] ...
You can use an Argument File ( AF ), in which case AF strings will be passed as arguments.
The call takes the form: > jj App File.arg # App.jj is implied
Extension: .arg - is mandatory. AF serves as a configuration file and may contain comments.
AF may not contain all arguments. For example: the first 5 are the configuration, and the rest are the parameters of the current call: > App File.arg arg5 arg6 ...
Another important feature of the FA is that it can contain parameters consisting of multiple lines.
This can be used for macro substitution of a code fragment.
More details are available in the documentation: pik3d_DOC.html.
Macro Substitution and Query
The text of a call's arguments can be used for Macro Substitution ( MS ).
MS replaces the k-th macro parameter with the text of the k-th argument. If the argument is missing or empty, the macro parameter is replaced with the default value or deleted( if default is absent ).
Macro parameter ( simple and with default ): #C#, #C/ default #,
where: C is the character: 0 | 1 | 2 ... 9 | A | B ... Z - corresponds to the argument number
( a | b ... z can be used), for a total of 10 + 26 = 36.
Query: #? query text/ default #. The query text is displayed on the console, along with the default ( if specified ), and the MS of the entered text or default ( if the input is empty ) is performed.
It's important to clearly distinguish between obtaining arg[ k ] at runtime and
MS modifying the jj-script before converting to JAVA and compilation.
MS allows you to modify code fragments. For example, you can substitute the analytical formula of a function right before sending the jj-script for execution. Example: when constructing a regression, you can specify a set of base functions analytically on the command line.
The standard set of the system includes jj-scripts for generating function values defined analytically:
> jj Fx "exp(-x/7)*sin(x)" 0,20,.2 | vic - 1 2,11 #generate values( Fx.jj ) and display( vic )
Executing OS commands in batch mode
Writing system scripts with complex control logic for shells like Windows: cmd and Linux: Bash
is not an easy task. Java code can be simpler and more efficient. Furthermore, it is the same on Windows and Linux ( except for system calls ).
An application or pipeline call in jj-script is preceded by "$ " :
Windows: $ app1 a0 a1 a2...| app2 ... > F1 & app3 p0 p1 > F2
Linux : $ app1 a0 a1 a2...| app2 ... > F1 && app3 p0 p1 > F2
Shell functions :
cmd-shell: dir, cd, echo, md, rd, copy, del, ren, type ...
Bash-shell: ls, cd, echo, cat, cp, mv, rm, mkdir, touch ...
are called with # . Example (creating a vector in ### format): $ #echo ###[] / 1 2 3 4 5 > vect
jj call: $ cmd1 | cmd2 ... is converted to Java text block: sys( """ cmd1 | cmd2 ... """ );
The launcher creation command: crex ( see above ) makes extensive use of the sys() method.
crex is a compiled jj-script, placed in cmd.jar and called by the launcher: crex.exe .
Comparison of batch-file and jj-scripts with the same functionality, but jj-script can be converted to eigenV.exe :
rem file: eigenV.bat | {// file: eigenV.jj
stk 77 9| noise -| ata -| eig - >V | $ stk 77 9| noise -| ata -| eig - >V
fmt V 1011.5g | $ fmt V 1011.5g
fmt V:2 1011.5g | $ fmt V:2 1011.5g
| }
> eigenV # run batch-file | > jj eigenV # run jj-script
| > crex eigenV # create: eigenV.exe
| > eigenV # run launcher
Conclusion
This article provides a brief overview of the technology for building a domain-specific system
based on console Java applications. This approach is justified for :
building such systems ;
data processing ;
creating an initial prototype ;
testing and debugging algorithms ;
jj-preprocessor can be useful when learning Java programming .
The system is open source. It is compiled in JAVA-21 using javaFX.
Distributions are available for Windows and Linux( tested on Linux Mint-21 ).
Full description, demo examples, source code and distribution: https://github.com/pik3d/19en