~subsetpark/jnj

cde8a674 — Zach Smith 26 days ago
j-value -> j-array
cab71b92 — Zach Smith 26 days ago
remove second header
d49462b5 — Zach Smith 26 days ago
Update docs

clone

read-only
https://git.sr.ht/~subsetpark/jnj
read/write
git@git.sr.ht:~subsetpark/jnj

You can also use your local clone with git send-email.

#JNJ (J iN Janet)

JNJ provides bindings to the J language to be used in Janet code.

J is a powerful array programming language in the APL lineage. It has a few features that might make it useful to expose from within a more general-purpose language:

  • Extremely terse and expressive notation for a very wide range of mathematical procedures
  • Highly optimized numeric code
  • Native support for arbitrary-dimensioned arrays

#Installation

JNJ requires the presence of libj.{so,dylib,dll}, provided by the J Language. You might need to install an -devel package for J depending on your package manager.

It will attempt to load libj.so at runtime. If libj.so is not present in your search path for dlopen(), you can specify the path when you install jnj:

LIBJ_DIR=/directory/containing/sofile jpm install https://git.sr.ht/~subsetpark/jnj

#Usage

See eval/eval* and j/j* for details.

jnj is designed to allow you to use J from within Janet. Thus, it has two main functions:

  • using Janet to drive evaluation of J commands
  • conversion back and forth between Janet and J datatypes.

To do so, jnj introduces two main abstract types:

  • a J Engine instance
  • a J Array object

All J computations take place within an instance of the J Engine. For convenience, a default instance is created at import time and available at jnj/j-engine. The eval and j commands both use this default instance, while eval* and j* allow you to specify your own.

All of the above functions have the same basic signature (modulo the presence or absence of an engine argument): they take a verb and 0, 1, or 2 arguments.

The difference is that j and j* will evaluate their J command and return native Janet datatypes. eval and eval*, on the other hand, will return the other abstract type, a J Array.

J Arrays can be inspected for rank, shape, etc., but they can also be passed as arguments to further eval/j calls. Thus, they're an efficient way to perform more complex procedures, involving multiple calls to the J interpreter, without having to convert data in and out of the J representation. They are also the most efficient and convenient way to create multi-dimensional arrays, as opposed to nesting multiple Janet array/tuples.

#eval/j

The verb can be any J sentence, verb or verb sequence. It can be a symbol or a string.

If there are no arguments, the sentence will be evaluated and returned.

repl:3:> (jnj/j "3 4 $ i. 10")
((0 1 2 3) (4 5 6 7) (8 9 0 1))

Arguments should be native Janet terms or a J Array. If they're present, the verb will be applied to them. If there's a single argument Y, it will be evaluated in the form <VERB> <Y>. If there are two arguments, X and Y, they will be evaluated in the form <X> VERB <Y>.

repl:4:> (jnj/j "$" [3 4] (range 10))
((0 1 2 3) (4 5 6 7) (8 9 0 1))
repl:5:> (def mat (jnj/eval "$" [3 4] (range 10)))
<jnj/jarray float [2/12]>
repl:6:> (jnj/j '$ mat)
(3 4)

In the first example, we evaluate a J sentence using native datatypes and return the result as native datatypes. In the second, we evaluate the same command and return the result as a J Array, which we can then use as an argument in subsequent commands.

#Advanced Usage

#let-j

The let-j/let-j* convenience macros provide a simple wrapper around eval and j which is more expressive and efficient.

The above example could also be written as:

repl:1> (let-j [mat ("$" [3 4] (range 10))]
          ('$ mat))
(3 4)

#to-j-array

As noted above, JNJ provides the J Array abstract type as a way to efficiently store J values. J provides an efficient and terse way to construct multi-dimensional arrays; however, if you have some existing Janet data which you want to use as an argument to J, you can convert it:

repl:2:> (to-j-array [[1 1 1][2 2 2][3 3 3]])
<jnj/jvalue float [2/9]>
repl:3:> (j "+/" _)
(6 6 6)

#jnj

eval, eval*, from-j-array, j, j*, j-engine, jnj-primitives/init, let-j, let-j*, to-j-array

#eval

function | source

(eval verb & args)

Evaluate verb and args with the default J engine instance (see eval* for details).

#eval*

function | source

(eval* je verb & args)

Evaluate verb with arguments args in the context of je. Returns a j-array abstract type which can be used for further evaluations, or converted into a tuple matrix.

#from-j-array

function | source

(from-j-array res)

Turn a j-array abstract type result into a Janet term of the appropriate shape:

  • a string
  • a scalar value
  • a matrix of nested tuples

#j

function | source

(j verb & args)

Evaluate verb with args in the default J engine instance (see j* for details).

#j*

function | source

(j* je verb & args)

Evaluate verb with args in the context of je, returning a native Janet datatype:

  • a matrix of nested tuples for anything with rank > 0
  • a Janet atom (string, number) otherwise

#j-engine

nil | source

The default J instance, used by jnj/j.

#jnj-primitives/init

function | source

(jnj-primitives/init &)

#let-j

macro | source

(let-j bindings expr)

Evaluate let-j bindings and expr in the default J engine instance (see let-j* for details).

#let-j*

macro | source

(let-j* je bindings body)

Use a series of intermediate bindings to compute a complex J value and convert it back to Janet.

bindings should be, like with let, alternating binding keywords and J expressions.

A binding keyword is any keyword, which can then be referred to in subsequent expressions.

A J expression is a tuple of the form that can be passed to eval* or j* - an arbitrary symbol-or-string sentence followed by 0, 1 or 2 args. In addition to the arg types understood by eval* and j*, an arg can also be a keyword, in which case it will refer to the result bound to that keyword earlier in the bindings form.

Finally, body is the J expression to be evaluated - again, a sentence followed by 0, 1 or 2 arguments, where the arguments can include any keywords specified in the bindings.

Here's an example that creates a 4x3 matrix, gets its shape, and then sums the shape:

> (let-j* je
>  [x ("$" [3 4] [0 1])
>   y ("$" x)]
>   ('+/ y))
7

Because the intermediate expressions in let-j only consist in the J engine, it can also be used to handle datatypes not yet understood by JNJ, like boxes:

> (let-j* je
>  [box ("<" "foo")
>   arglist (";" box "bar")]
>   ("#" arglist))
2

This same fact makes let-j operations somewhat more efficient than multiple eval calls, as the intermediate values don't need to be copied back into the J runtime.

#to-j-array

function | source

(to-j-array matrix)

Turn an arbitrarily nested array/tuple of numbers into a J Array.

NB: This function performs some validation on its input to assert that it can be transformed into a J Array. So if the data is not already in Janet form, it might be more efficient to generate it using jnj/eval.