from fastcore.test import *J
basedpl.j runs the J language inside the Python process. It loads libj, the engine library in every J installation, so no separate J process runs. The module provides the J session class, the %%j and %j magics, and a Jupyter kernel for J. Importing it does not start J.
Install J with pip install jlanguage, or from jsoftware.com. The Python layer also needs IPython.
Finding J
find_j
def find_j():Locate the J binary directory: the one containing libj and profile.ijs
find_j checks for libj beside jconsole, resolving symlinks first. The jlanguage package installs a jconsole script without libj beside it, so find_j asks the jlang module for that installation’s path. It also checks common installation directories.
jbin = find_j()
assert (jbin/_LIBJ).exists()
jbin.parts[-2:]('jlang', 'bin')
Sessions
J
def J(
jbin:NoneType=None
):A J session: an engine from libj, with J’s standard library loaded
J starts an engine, then runs profile.ijs to load J’s standard library. Pass jbin to choose a J installation other than the one find_j returns.
J.__call__
def __call__(
code
):Run code, returning displayable output, or None if there is none
J.run
def run(
code
):Run code (one or more lines of J), returning its output; raises JError on J errors
run returns J’s output as text. Calling the session returns the same text as AplOut, which displays verbatim, or None when there is no output. Names persist between calls:
j = J()
j('m =: 2 3 $ 10 * 1 + i. 6\nm')10 20 30 40 50 60
test_eq(j.run('+/ , m'), '210\n')
test_is(j('m2 =: 10 * m'), None)Lines that open a multi-line definition take the following lines as its body:
j('mean =: 3 : 0\n(+/ y) % # y\n)\nmean 1 2 3 4')2.5
A J error raises JError. Its message is the session’s output, including J’s error display:
with expect_fail(JError, contains='domain error'): j.run("m + 'x'")Extended precision works, as in 25 factorial:
j('*/ 1 + i. 25x')15511210043330985984000000
Python values
getm reads a named J noun as Python data. Characters become a string. Booleans, integers and floats become a number, or nested lists for an array. Boxed, extended and rational values raise JError.
J.getm
def getm(
name
):Read noun name into Python: str for characters; int/float scalars and (nested) lists otherwise
test_eq(j.getm('m'), [[10,20,30],[40,50,60]])
j('x =: 1r3')
with expect_fail(JError, contains='unsupported J type'): j.getm('x')j[expr] evaluates an expression and returns its value as Python data. It assigns the result to pytmp, reads it, then erases that name.
J.__getitem__
def __getitem__(
expr
):J.pyval
def pyval(
expr
):Evaluate expr and return the result as a Python value
test_eq(j['m > 25'], [[0,0,1],[1,1,1]])
test_eq(j['+/ % # 1 2 3 4'], 0.25)
j["'py' , 'val'"]'pyval'
j[name] = value assigns Python data. A string becomes a J character list. A number or nested list becomes an integer array, or a float array if any item is a float.
J.__setitem__
def __setitem__(
nm, v
):Assign Python value v (scalar, string, or nested list) to noun nm
j['q'] = [[1,2],[3,4.5]]
j['s'] = "it's"
test_eq(j['s'], "it's")
j['+/ , q']10.5
A ragged list has a different number of items than its shape implies, so assigning it raises JError:
with expect_fail(JError, contains='shape'): j['r'] = [[1,2],[3]]fn turns a J verb into a Python callable. Pass one argument for monadic use, or two for dyadic use with the left argument first.
J.fn
def fn(
code
):A Python callable applying J verb code monadically or dyadically (left argument first)
sq = j.fn('*:')
test_eq(sq([1,2,3]), [1,4,9])
test_eq(j.fn('+/')([1,2,3]), 6)
j.fn('{.')(2, [5,6,7])[5, 6]
Interrupting
interrupt stops a running sentence with a J attention interrupt. Call it from another thread, because the thread running J cannot run Python code until J returns. The session stays usable after the interrupt.
J.interrupt
def interrupt():Stop the running sentence with an attention interrupt; call it from another thread
import threading, time
from concurrent.futures import ThreadPoolExecutorj('spin =: 3 : 0\nn =. 0\nwhile. n < 1e9 do. n =. n + 1 end.\n)')
t0 = time.time()
threading.Timer(0.5, j.interrupt).start()
with expect_fail(JError, contains='attention interrupt'): j('spin 0')
assert time.time()-t0 < 5
j('2+2')4
interrupt is the only method that works from another thread. J sets its recursion limit from the stack of the thread that created the session. So a session runs J only on that thread, and raises JError on any other:
with expect_fail(JError, contains='thread'): ThreadPoolExecutor().submit(j.run, '2+2').result()Exit requests and closing
close frees the engine. A with block calls close when it ends. A closed session cannot run code.
J.__exit__
def __exit__(
*args
):J.__enter__
def __enter__():J.close
def close():Free the engine; the session cannot run code afterwards
exit 7 asks J to exit with code 7. The session records the code in exited and runs no more lines, without raising an error. The host decides what an exit request means.
J.exited
def exited():The exit code requested by exit, or None
with J() as j2:
test_is(j2.exited, None)
j2('exit 7')
test_eq(j2.exited, 7)
test_is(j2('2+2'), None)Magics
Run %load_ext basedpl.j to register two magics in IPython or Jupyter:
%%jruns a cell and displays its output verbatim. A trailing;hides the output.%j exprreturns the value ofexpras Python data, asj[expr]does.
The magics share one session, which starts on the first call.
JMagic
def JMagic(
jbin:NoneType=None
):IPython %j/%%j magics, driving a lazily-started J session
load_ipython_extension runs when IPython loads the extension. It calls create_j_magic, which registers a JMagic with any IPython shell:
load_ipython_extension
def load_ipython_extension(
ipython
):Register the j magics: %load_ext basedpl.j
create_j_magic
def create_j_magic(
shell:NoneType=None
):Create a JMagic and register its j line/cell magic with shell, returning it
%%j
m3 =: 3 3 $ i. 9
m3 +/ . * m315 18 21 42 54 66 69 90 111
%j returns Python data, which you can assign:
z = %j m3
z[[0, 1, 2], [3, 4, 5], [6, 7, 8]]
A trailing ; hides a cell’s output:
%%j
big =: 1000 1000 $ i. 5
big + big;The J kernel
The J kernel runs J cells in Jupyter clients such as JupyterLab, nbclient, and agents using clikernel. Names persist across cells. An interrupt stops a computation and keeps the session. Install the kernel with python -m basedpl.j install. Jupyter then lists it as J.
install_j_kernel
def install_j_kernel(
prefix:NoneType=None, # Install under `prefix/share/jupyter/kernels` if given, else in the user Jupyter directory
):Register the J kernel with Jupyter as kernelspec j, returning its directory
install_j_kernel writes a kernelspec named j. It starts the kernel with python -m basedpl.j CONNECTION_FILE, using the current Python. Pass prefix to install into an environment, such as sys.prefix, instead of your user Jupyter directory.
run_j_kernel serves the kernel. It runs J on a Rust thread, through the same engine as J. Before the first request, it runs basedpl/startup.ijs from your XDG configuration directory if that file exists. The default path is ~/.config/basedpl/startup.ijs.
run_j_kernel
def run_j_kernel(
connection_file
):Serve a J kernel on the Jupyter connection in connection_file
Running the module serves a kernel, or installs one when its argument is install:
Limitations
%jandj[expr]return booleans, integers, floats and characters as Python data. Boxed, extended and rational values raiseJError. Use%%jto display them.- macOS and Linux are supported. The libj binding is untested on Windows.
Learning J
Start with Learning J and the J wiki. Its NuVoc page is the reference for every primitive.