aplnb brings bAsedPL to Jupyter and IPython. Use %%apl for APL session output and %apl for native bAsedPL values in Python. Both share a persistent workspace.
See core for the implementation. For the J language, see the sibling project jnb.
Install aplnb and its bAsedPL runtime:
pip install aplnbNo separate APL installation is needed. To load the magics automatically in IPython and Jupyter, run:
aplnb_installOr run %load_ext aplnb in an individual notebook. The interpreter starts on the first use of a magic.
The line magic %apl returns a native basedpl.Array. Its immutable data stays in bAsedPL, retaining nesting, exact numbers and empty-array prototypes. No NumPy conversion is needed:
v = %apl 1 2 4
v[1., 2., 4.]
Python operators use bAsedPL’s array semantics. Python determines precedence, so this multiplies before adding:
v * 2 + 1[3., 5., 9.]
Indexing is one-based, as in APL. A full : selects an axis. Iteration yields rows of a matrix rather than individual elements:
a = %apl 2 3⍴⍳6
a.shape, a[1, :], a[:, 2], list(a)((2, 3), [1., 2., 3.], [2., 5.], [[1., 2., 3.], [4., 5., 6.]])
bAsedPL also exposes functions by their word names. plus.reduce is +/; adding .each applies it to each nested element:
from basedpl import Array, plus, tallynested = %apl (1 2)(3 4 5)
nested[Array([1., 2.]), Array([3., 4., 5.])]
%%apl
n←(1 2)(3 4 5)
+/¨n3 12
The same Each/reduction pattern works with division:
%%apl
÷/¨n0.5 3.75
In Python, chain the corresponding word functions:
plus.reduce.each(nested)[3., 12.]
f = plus.reduce.each
f(nested)[3., 12.]
Python integers become exact values. Functions compose too: plus.reduce / tally builds the mean function +/÷≢. Its result is still a native array, even when scalar:
exact = Array([1, 2, 4])
mean_native = plus.reduce / tally
mean_native(exact)Array((7, 3))
The native representation uses Python numeric notation: 2 is exact and 2. is approximate. .apl returns the APL display as a string:
v.apl, exact.apl('1 2 4', '1x 2x 4x')
Let’s create a function to list primes.
A prime has exactly two positive divisors. |⌝ forms an outer product of remainders; ⍨ supplies the same argument on both sides. Row i below marks multiples of i:
%%apl
n←⍳10
0=|⌝⍨n1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 0x 1x 0x 1x 0x 1x 0x 1x 0x 1x 0x 0x 1x 0x 0x 1x 0x 0x 1x 0x 0x 0x 0x 1x 0x 0x 0x 1x 0x 0x 0x 0x 0x 0x 1x 0x 0x 0x 0x 1x 0x 0x 0x 0x 0x 1x 0x 0x 0x 0x 0x 0x 0x 0x 0x 0x 1x 0x 0x 0x 0x 0x 0x 0x 0x 0x 0x 1x 0x 0x 0x 0x 0x 0x 0x 0x 0x 0x 1x 0x 0x 0x 0x 0x 0x 0x 0x 0x 0x 1x
+⌿ sums down the rows, counting each candidate’s divisors:
%%apl
+⌿0=|⌝⍨n1x 2x 2x 3x 2x 4x 2x 4x 3x 4x
2= marks primes. Where (⍸) returns their positions, which equal the candidates because n←⍳10:
%%apl
⍸2=+⌿0=|⌝⍨n2x 3x 5x 7x
Substitute ⍳50 to list the primes up to 50:
%%apl
⍸2=+⌿0=|⌝⍨⍳502x 3x 5x 7x 11x 13x 17x 19x 23x 29x 31x 37x 41x 43x 47x
Finally, name it. The dfn takes the candidates as ⍵; the trailing ⍳ generates them:
%%apl
primes ← {⍸2=+⌿0=|⌝⍨⍵}⍳
primes 502x 3x 5x 7x 11x 13x 17x 19x 23x 29x 31x 37x 41x 43x 47x
The built-in prime glyph ℙ returns the nth prime, counting from one. Applied to ⍳15, it produces the same list directly:
%%apl
ℙ⍳152x 3x 5x 7x 11x 13x 17x 19x 23x 29x 31x 37x 41x 43x 47x
The fibonacci sequence:
fib = %apl {⍵,+/¯2↑⍵}⍣15⊢1 1
fib.nparray([1.000e+00, 1.000e+00, 2.000e+00, 3.000e+00, 5.000e+00, 8.000e+00,
1.300e+01, 2.100e+01, 3.400e+01, 5.500e+01, 8.900e+01, 1.440e+02,
2.330e+02, 3.770e+02, 6.100e+02, 9.870e+02, 1.597e+03])
Explanation:
1 1: Initial seed (first two Fibonacci numbers){⍵,+/¯2↑⍵}: Function that appends the sum of the last two elements⍣15: Apply the function 15 times⊢: Identity function, passes the initial argument (1 1) to the iteration
Arrays can carry keys for positions and names for axes, without becoming a separate table type. A keyed vector pairs each axis name with its position keys, in axis order. Here axes: labels the rows by city and the columns by month:
%%apl
axes←'city' 'month':('London' 'Paris' ⋄ 'Jan' 'Feb' 'Mar')
sales←axes:[10 20 30 ⋄ 40 50 60]
salesJan Feb Mar London 10 20 30 Paris 40 50 60
Brackets select positions by key. Function qualifiers select axes by name: summing over month leaves a total for each city, retaining its labels.
%%apl
sales['Paris';'Feb']
+/['month']sales50
('London':60 ⋄ 'Paris':150)
Native Python arrays retain both kinds of metadata:
sales = %apl sales
sales.axis_names, sales.axis_keys(('city', 'month'), (('London', 'Paris'), ('Jan', 'Feb', 'Mar')))
Arithmetic matches named axes and aligns their keys, rather than relying on order. This adjustment lists the months backwards but still adds 1 to January, 2 to February and 3 to March in each city. .df copies the result to pandas, preserving labels; install basedpl[pandas] to use it.
adjustment = Array([3, 2, 1], axis_keys=(('Mar', 'Feb', 'Jan'),), axis_names=('month',))
(sales + adjustment).df| month | Jan | Feb | Mar |
|---|---|---|---|
| city | |||
| London | 11.0 | 22.0 | 33.0 |
| Paris | 41.0 | 52.0 | 63.0 |
Unlabelled arrays keep their usual positional behaviour. See Axis keys for construction, updates and alignment rules.
CSV headers become keys on a vector of columns. Numeric columns use compact storage. These two orders total 101:
%%apl
nl←•ucs 10
orders←•csv 'price,qty',nl,'10.5,2',nl,'20,4'
+/orders.price×orders.qty101
JSON objects use the same keyed arrays. Parse a record and select a field:
%%apl
record←•json '{"name":"Ada","scores":[8,9,10]}'
record.name
+/record.scoresAda 27x
A dyadic call exports JSON. For files, compose a parser with •nget, e.g. •csv •nget 'orders.csv'. See files, CSV and JSON for dialect and file options.
%%apl
record •json ''{"name":"Ada","scores":[8,9,10]}
The magics use basedpl.Session. Calling a session returns a native array or function and prints explicit APL output. apl.eval(...) returns a Result with the native .value and captured .output without printing. Both suppress implicit APL display. Use .np or .py when you need a converted result:
import numpy as np
from basedpl import Sessionapl = Session()
apl('3 3⍴⍳9').nparray([[1., 2., 3.],
[4., 5., 6.],
[7., 8., 9.]])
Module and session attributes expose the builtins. Glyph names (add, dash, mul, div) accept either valence; operation names (plus, subtract, times, divide) curry a dyadic call. Thus add with one argument conjugates, while plus(2) binds the right argument:
apl.add(2+3j).py, apl.plus(2)(3).py, apl.times(2)([1, 2, 3]).py((2-3j), 5, array([2, 4, 6]))
Keyword arguments bind Python values in the workspace. Square brackets read an APL expression or assign a value:
apl(x=np.arange(1, 6))
apl['v'] = [3,1,4,1,5]
apl['{⍵[⍋⍵]}v'].nparray([1, 1, 3, 4, 5])
fn makes a composable Function from an APL function expression. Pass one argument for a monadic call or two for a dyadic call. Python integers stay exact, so .py converts this mean to a Fraction rather than a float:
apl('mean←{⍝ Mean of a vector\n(+/⍵)÷≢⍵}')
mean = apl.fn('mean')
mean([1,2,4]).pyFraction(7, 3)
Type mean? for its comment help and mean?? for APL source. The same information is available programmatically:
mean.source, apl.names('me')('{⍝ Mean of a vector\n(+/⍵)÷≢⍵}', ['mean'])
In APL input, use %apl ]help primes or a %%apl cell containing ]help primes -source. Tab completes user and system names. APL code can inspect names directly:
%%apl
•nc 'sales' 'primes'
'pr' •nl 32x 3x (primes)
See names and help for source lookup, erasure and inspection APIs.
fn is late-bound: apl.fn('foo') follows later redefinitions of foo. Use create_magic(session=apl) to share a Python session with the magics.
Use a session as a context manager (with Session() as apl:), or close it when finished:
apl.close()Hold left Alt/Option for bAsedPL’s glyph keyboard: Alt-h ←, Alt-minus ×, Alt-equals ÷, Alt-Shift-a ⍶. Right Option keeps its native behavior. Chords work in APL input, including strings and comments.
Type a backtick followed by a bAsedPL symbol name: `io then Tab inserts ⍳, and 2`times3 becomes 2×3. Suggestions appear beside the cursor as you type, including the REPL’s shortcut notation: h means Alt-h, Sa means Alt-Shift-a. Click a suggestion or keep typing to resolve an ambiguous name. Names and aliases come from bAsedPL’s REPL catalogue.
Completion is active in %%apl cells and on %apl lines, including x = %apl .... Ordinary Python and Markdown input is unchanged. Strings, comments and pasted text are not expanded. Tab explicitly completes an existing name. Enter accepts a unique match before the notebook’s normal newline or execution action. Escape or cursor movement cancels automatic expansion.
The first apl magic also adds a clickable symbol bar, based on Adám Brudzewsky’s APL language bar. Hover over a glyph to see its names and shortcut. The ▲/▼ button switches between pushing the page down and overlaying it. This choice is remembered per site.
The cell magic (%%apl) displays bAsedPL session output in Adám’s SAX2 APL font:
%%apl
m←3 3⍴⍳9
m×1010 20 30 40 50 60 70 80 90
Assignments are shy: the m← line printed nothing. The line magic returns a native array. Use .np to copy its values to NumPy:
v = %apl 3×⍳4
v.nparray([ 3., 6., 9., 12.])
text = %apl 'APL in Python'
text.py'APL in Python'
.py converts numeric atoms to Python numbers and character vectors to strings. Unkeyed numeric arrays become NumPy arrays, keyed vectors become dictionaries, and higher-rank keyed arrays become pandas DataFrames. Numeric scalars (rank-0 arrays) become zero-dimensional NumPy arrays. .np copies values to NumPy without labels:
z = %apl m
z.nparray([[1., 2., 3.],
[4., 5., 6.],
[7., 8., 9.]])
To suppress a cell’s output, end the last line with a ;:
%%apl
m×10;⎕← displays a value explicitly, which is how you show something that would otherwise be shy:
%%apl
v←2×⍳5
⎕←v2 4 6 8 10
Convert to NumPy to use its methods:
a = %apl m
a.np.sum(axis=0)array([12., 15., 18.])
•r compiles a Rust regex into functions for matching, positions, groups and replacement:
%%apl
codes←•r '([A-Z]+)-([0-9]+)'
codes.match 'AB-12 CD-3'
codes.position 'AB-12 CD-3'
'$2:$1' codes.replace 'AB-12 CD-3'(AB-12) (CD-3) 1x 7x 12:AB 3:CD
Construct a standard normal, then evaluate its CDF and quantiles. Distribution methods accept arrays:
%%apl
normal←•normal 0 1
normal.cdf ¯1 0 1
normal.quantile 0.025 0.5 0.9750.15865525394505725 0.5 0.8413447460549428 ¯1.9599639845400545 0 1.9599639845400538
Sampling takes a shape. These draws remain a native array until .np converts them:
draws = %apl normal.sample 2 3
draws.nparray([[ 1.13425439, 0.50212334, -0.69961591],
[ 2.16752983, -0.70488529, -1.72144746]])
For discrete distributions, density gives probability mass. A fair coin tossed twice has probabilities ¼, ½, ¼ for zero, one or two heads:
%%apl
coin←•binomial 2 0.5
coin.density 0 1 20.25 0.5 0.25
See distributions for the 17 families, and regex for captures and replacement options.
The basedpl package also installs the bAsedPL Jupyter kernel. In that kernel, write APL directly without %%apl. Shift-Tab inspects names and glyphs; ]help name and ]help name -source show help and source. Use aplnb’s Python kernel integration when mixing Python and APL in the same notebook.
Use aplnb.dyalog when you need Dyalog as an independent reference interpreter. Dyalog must be installed separately. This session API does not change the %apl or %%apl magics, which continue to use bAsedPL:
from aplnb.dyalog import Aplwith Apl() as dyalog: total = dyalog.pyval('+/⍳10')
total55
pyval returns JSON-converted Python values. run returns session output as text. See Dyalog sessions for assignment, function calls and error handling.
APL errors raise basedpl.AplError. The magics display output produced before the error. Incomplete input raises a syntax error without resetting the workspace.
Interrupt a calculation with the notebook’s stop button. Set a per-evaluation deadline with Session(timeout=seconds) or magic.session.timeout = seconds. Sessions use a Rust worker thread. Cooperative cancellation preserves the workspace and completed assignments. Native-library calls and individual BigInt operations can delay cancellation; the thread is never forcibly killed.
bAsedPL is an APL-derived array language, borrowing from J and BQN. See its language guide for glyphs, array rules and system functions.
To start learning APL, follow the 17 video series run by Jeremy Howard, and have a look at the study notes. These use Dyalog APL. Interpreter-specific features and user commands differ in bAsedPL.