aplnb adds %apl and %%apl magics to Jupyter and IPython, which
execute code in Dyalog APL. It talks to the interpreter over the RIDE
protocol,
the same protocol Dyalog’s own IDE uses, so nothing needs to be loaded
into your workspace, output looks exactly like a Dyalog session, and
errors are detected reliably. See
core for a step by
step walkthrough of the protocol and of how aplnb is built on it.
First, install Dyalog APL. Dyalog provides a basic license for free. aplnb is developed against Dyalog 20.0; 18.2 and later may work but are untested. Once Dyalog is installed, install aplnb with:
pip install aplnb
Once that’s complete, you can install the magics to all IPython and Jupyter sessions automatically by running in your terminal:
aplnb_install
The Dyalog interpreter itself only starts the first time you run an
apl magic, so having aplnb installed everywhere costs nothing when you
don’t use it.
After first running an apl magic in a notebook, the APL language
bar by Adám Brudzewsky is
automatically added to the current page. (aplnb bundles a modified copy
of Adám’s lb.js; the file header lists the changes. The most visible
ones: type a backtick twice in a row to enter triple backticks, get a
⋄ glyph with backtick-q, and use the ▲/▼ button beside the close
button to choose whether the bar pushes the page down or overlays it,
remembered per site.)
The cell magic (%%apl) runs APL code and displays the session’s own
output, rendered in Adám’s SAX2 APL
font, so results look exactly as they do in Dyalog:
%%apl
m←3 3⍴⍳9
m×10Javascript(// APL language bar by Adám Brudzewsky: https://abrudz.github.io/lb (source: https://github.com/abrudz/lb)
// MIT License, Copyright (c) 2011-2020 Nikolay G. Nikolov and Adam Brudzevski. This is a modified copy bundled with aplnb.
// Changes from upstream: double backtick composes ```; insertion via insertText so undo and input events work;
// Monaco editor support (incl. EditContext mode); dark mode; overlay/push-down toggle persisted per site;
// idempotent injection; ResizeObserver-driven layout; @font-face with dead url() removed; skipped on quarto-rendered pages.
; (_ => {
if (document.querySelector('.ngn_lb')) return
if (document.querySelector('meta[name=generator][content^=quarto]')) return //no bar on rendered docs pages
let hc = { '<': '<', '&': '&', "'": ''', '"': '"' }, he = x => x.replace(/[<&'"]/g, c => hc[c]) //html chars and escape fn
, tcs = '<-←xx×/\\×:-÷*O⍟[-⌹-]⌹OO○77⌈FF⌈ll⌊LL⌊T_⌶II⌶|_⊥TT⊤-|⊣|-⊢=/≠L-≠<=≤<_≤>=≥>_≥==≡=_≡7=≢Z-≢vv∨^^∧^~⍲v~⍱^|↑v|↓((⊂cc⊂(_⊆c_⊆))⊃[|⌷|]⌷A|⍋V|⍒ii⍳i_⍸ee∊e_⍷' +
'uu∪UU∪nn∩/-⌿\\-⍀,-⍪rr⍴pp⍴O|⌽O-⊖O\\⍉::¨""¨~:⍨~"⍨*:⍣*"⍣oo∘o:⍤o"⍤O:⍥O"⍥[\'⍞\']⍞[]⎕[:⍠:]⍠[=⌸=]⌸[<⌺>]⌺o_⍎oT⍕o-⍕<>⋄^v⋄on⍝->→aa⍺ww⍵VV∇v-∇--¯0~⍬' +
'AA∆^-∆A_⍙^=⍙[?⍰?]⍰:V⍢∇"⍢||∥ox¤)_⊇_)⊇V~⍫\'\'`'
, lbs = ['←←\nASSIGN', ' ', '++\nconjugate\nplus', '--\nnegate\nminus', '××\ndirection\ntimes', '÷÷\nreciprocal\ndivide', '**\nexponential\npower', '⍟⍟\nnatural logarithm\nlogarithm',
'⌹⌹\nmatrix inverse\nmatrix divide', '○○\npi times\ncircular', '!!\nfactorial\nbinomial', '??\nroll\ndeal', ' ', '||\nmagnitude\nresidue',
'⌈⌈\nceiling\nmaximum', '⌊⌊\nfloor\nminimum', '⊥⊥\ndecode', '⊤⊤\nencode', '⊣⊣\nsame\nleft', '⊢⊢\nsame\nright', ' ', '==\nequal', '≠≠\nunique mask\nnot equal',
'≤≤\nless than or equal to', '<<\nless than', '>>\ngreater than', '≥≥\ngreater than or equal to', '≡≡\ndepth\nmatch', '≢≢\ntally\nnot match', ' ', '∨∨\ngreatest common divisor/or',
'∧∧\nlowest common multiple/and', '⍲⍲\nnand', '⍱⍱\nnor', ' ', '↑↑\nmix\ntake', '↓↓\nsplit\ndrop', '⊂⊂\nenclose\npartioned enclose', '⊃⊃\nfirst\npick', '⊆⊆\nnest\npartition', '⌷⌷\nmaterialise\nindex', '⍋⍋\ngrade up\ngrades up',
'⍒⍒\ngrade down\ngrades down', ' ', '⍳⍳\nindices\nindices of', '⍸⍸\nwhere\ninterval index', '∊∊\nenlist\nmember of', '⍷⍷\nfind', '∪∪\nunique\nunion', '∩∩\nintersection', '~~\nnot\nwithout', ' ',
'//\nreplicate\nReduce', '\\\\\n\expand\nScan', '⌿⌿\nreplicate first\nReduce First', '⍀⍀\nexpand first\nScan First', ' ', ',,\nravel\ncatenate/laminate',
'⍪⍪\ntable\ncatenate first/laminate', '⍴⍴\nshape\nreshape', '⌽⌽\nreverse\nrotate', '⊖⊖\nreverse first\nrotate first',
'⍉⍉\ntranspose\nreorder axes', ' ', '¨¨\nEach', '⍨⍨\nConstant\nSelf\nSwap', '⍣⍣\nRepeat\nUntil', '..\nOuter Product (∘.)\nInner Product',
'∘∘\nOUTER PRODUCT (∘.)\nBind\nBeside', '⍤⍤\nRank\nAtop', '⍥⍥\nOver', '@@\nAt', ' ', '⍞⍞\nSTDIN\nSTDERR', '⎕⎕\nEVALUATED STDIN\nSTDOUT\nSYSTEM NAME PREFIX', '⍠⍠\nVariant',
'⌸⌸\nIndex Key\nKey', '⌺⌺\nStencil', '⌶⌶\nI-Beam', '⍎⍎\nexecute', '⍕⍕\nformat', ' ', '⋄⋄\nSTATEMENT SEPARATOR', '⍝⍝\nCOMMENT', '→→\nABORT\nBRANCH', '⍵⍵\nRIGHT ARGUMENT\nRIGHT OPERAND (⍵⍵)', '⍺⍺\nLEFT ARGUMENT\nLEFT OPERAND (⍺⍺)',
'∇∇\nrecursion\nRecursion (∇∇)', '&&\nSpawn', ' ', '¯¯\nNEGATIVE', '⍬⍬\nEMPTY NUMERIC VECTOR', '∆∆\nIDENTIFIER CHARACTER', '⍙⍙\nIDENTIFIER CHARACTER']
, bqk = ' =1234567890-qwertyuiop\\asdfghjk∙l;\'zxcvbnm,./q[]+!@#$%^&*()_QWERTYUIOP|ASDFGHJKL:"ZXCVBNM<>?~{}'.replace(/∙/g, '')
, bqv = '`÷¨¯<≤=≥>≠∨∧×⋄⍵∊⍴~↑↓⍳○*⊢∙⍺⌈⌊_∇∆∘\'⎕⍎⍕∙⊂⊃∩∪⊥⊤|⍝⍀⌿⋄←→⌹⌶⍫⍒⍋⌽⍉⊖⍟⍱⍲!⍰W⍷R⍨YU⍸⍥⍣⊣ASDF⍢H⍤⌸⌷≡≢⊆⊇CVB¤∥⍪⍙⍠⌺⍞⍬'.replace(/∙/g, '')
, tc = {}, bqc = {} //tab completions and ` completions
for (let i = 0; i < bqk.length; i++)bqc[bqk[i]] = bqv[i]
for (let i = 0; i < tcs.length; i += 3)tc[tcs[i] + tcs[i + 1]] = tcs[i + 2]
for (let i = 0; i < tcs.length; i += 3) { let k = tcs[i + 1] + tcs[i]; tc[k] = tc[k] || tcs[i + 2] }
let lbh = ''; for (let i = 0; i < lbs.length; i++) {
let ks = []
for (let j = 0; j < tcs.length; j += 3)if (lbs[i][0] === tcs[j + 2]) ks.push('\n' + tcs[j] + ' ' + tcs[j + 1] + ' <tab>')
for (let j = 0; j < bqk.length; j++)if (lbs[i][0] === bqv[j]) ks.push('\n` ' + bqk[j])
lbh += '<b title="' + he(lbs[i].slice(1) + (ks.length ? '\n' + ks.join('') : '')) + '">' + lbs[i][0] + '</b>'
}
let ovl; try { ovl = localStorage.getItem('ngn_lb_overlay') === '1' } catch (e) { ovl = !1 } //overlay mode: bar covers the top instead of pushing the page down
let d = document, el = d.createElement('div'); el.innerHTML =
`<div class=ngn_lb><span class=ngn_x title=Close>❎</span><span class=ngn_o title="Toggle overlay/push-down">${ovl ? '▼' : '▲'}</span>${lbh}</div>
<style>
.ngn_lb{position:fixed;top:0;left:0;right:0;background-color:#eee;color:#000;cursor:default;z-index:2147483647;
font-family:"DejaVu Sans Mono",monospace;border-bottom:solid #999 1px;padding:2px 2px 0 2px;word-wrap:break-word;}
.ngn_lb b{cursor:pointer;padding:0 1px;font-weight:normal}
.ngn_lb b:hover,.ngn_bq .ngn_lb{background-color:#777;color:#fff}
.ngn_x,.ngn_o{float:right;color:#999;cursor:pointer;margin-top:-3px}
.ngn_o{margin-right:6px}
.ngn_o:hover{color:#00d}
.ngn_x:hover{color:#f00}
@media (prefers-color-scheme:dark){
.ngn_lb{background-color:#222;color:#ddd;border-bottom-color:#555}
.ngn_lb b:hover,.ngn_bq .ngn_lb{background-color:#bbb;color:#000}
.ngn_x,.ngn_o{color:#666}
}
</style>`
d.body.appendChild(el)
let t, lb = el.firstChild, bqm = 0 //t:textarea or input, lb:language bar, bqm:backquote mode
let pd = x => x.preventDefault()
let ev = (x, t, f, c) => x.addEventListener(t, f, c)
let med = _ => { try { return window.monaco?.editor?.getEditors?.().find(e => e.hasTextFocus()) } catch (e) { } } //focused Monaco editor, if any
let ins = (t, s, del = 0) => { //insert s at caret (replacing selection, or del chars before it), keeping undo & input events
let m = med()
if (m) {
if (del) {
let p = m.getPosition()
m.executeEdits('lb', [{ range: { startLineNumber: p.lineNumber, startColumn: p.column - del, endLineNumber: p.lineNumber, endColumn: p.column }, text: s }])
} else m.trigger('keyboard', 'type', { text: s })
return
}
if (!t || t.selectionStart == null) return
if (del) t.selectionStart = t.selectionStart - del
if (!(d.execCommand && d.execCommand('insertText', !1, s))) {
let i = t.selectionStart
t.value = t.value.slice(0, i) + s + t.value.slice(t.selectionEnd)
t.selectionStart = t.selectionEnd = i + s.length
t.dispatchEvent(new Event('input', { bubbles: !0 }))
}
}
ev(lb, 'mousedown', x => {
if (x.target.classList.contains('ngn_x')) { lb.hidden = 1; upd() }
else if (x.target.classList.contains('ngn_o')) {
ovl = !ovl
x.target.textContent = ovl ? '▼' : '▲'
try { localStorage.setItem('ngn_lb_overlay', ovl ? '1' : '0') } catch (e) { }
upd()
} else if (x.target.nodeName === 'B') {
let s = x.target.textContent, m = med()
if (m) { m.focus(); ins(t, s) }
else if (t && t.selectionStart != null) { t.focus(); ins(t, s) }
}
pd(x) //always: clicking the bar must never steal focus
})
let fk = x => {
let t = x.target, m = med(), i, v
if (m) { let p = m.getPosition(); i = p.column - 1; v = m.getModel().getLineContent(p.lineNumber) }
else { i = t.selectionStart; v = t.value }
if (bqm) {
let c = bqc[x.key]
if (x.key === '`') {
ins(t, '```')
if (m) { let p = m.getPosition(); m.setPosition({ lineNumber: p.lineNumber, column: p.column - 2 }) }
else t.selectionStart = t.selectionEnd = i + 1
bqm = 0
d.body.classList.remove('ngn_bq')
pd(x)
return !1
}
if (x.which > 31) { bqm = 0; d.body.classList.remove('ngn_bq') }
if (c) { ins(t, c); pd(x); return !1 }
}
if (!x.ctrlKey && !x.shiftKey && !x.altKey && !x.metaKey) {
if ("`½²^º§ùµ°".indexOf(x.key) > -1) {
bqm = 1; d.body.classList.add('ngn_bq'); pd(x); // ` or other trigger symbol pressed, wait for next key
} else if (x.key == "Tab") {
let c = i >= 2 && tc[v.slice(i - 2, i)]
if (c) { ins(t, c, 2); pd(x) }
}
}
}
let ff = x => {
let t0 = x.target, nn = t0.nodeName.toLowerCase()
if (nn !== 'textarea' && (nn !== 'input' || t0.type !== 'text' && t0.type !== 'search')) return
t = t0; if (!t.ngn) { t.ngn = 1; ev(t, 'keydown', fk) }
}
let upd = _ => { d.body.style.marginTop = ovl ? '' : lb.clientHeight + 'px' }
upd(); (window.ResizeObserver ? new ResizeObserver(upd).observe(lb) : ev(window, 'resize', upd))
ev(d, 'focus', ff, !0); let ae = d.activeElement; ae && ff({ type: 'focus', target: ae })
ev(d, 'keydown', x => { if (!x.target.ngn && x.target.closest?.('.monaco-editor')) fk(x) }, !0) //EditContext-mode Monaco has no textarea for ff to register
})();
)
10 20 30 40 50 60 70 80 90
Assignments are shy, just like in the Dyalog session, so the m← line
above printed nothing. The line magic (%apl) instead evaluates one
expression and returns it as a Python value:
%apl 3×⍳4[3, 6, 9, 12]
%apl ⎕A'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
Because the line magic returns a value, you can store it in a Python variable. Scalars come back as numbers or strings, vectors as lists, and higher-rank arrays as nested lists:
z = %apl m
z[[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
To use numpy, just pass the result of %apl into np.array:
import numpy as npa = %apl m
np.array(a)array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
The fibonacci sequence:
%apl {⍵,+/¯2↑⍵}⍣15⊢1 1[1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597]
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
Prime number sieve:
%%apl
primes ← {⍵×2=+⌿0=⍵∘.|⍵}⍳
(primes 50)~02 3 5 7 11 13 17 19 23 29 31 37 41 43 47
Explanation:
⍳50generates integers 1 to 50⍵∘.|⍵creates a 50x50 matrix of divisibility (1 if divisible, 0 if not)0=inverts the matrix (1 for non-divisible)+⌿sums columns, counting non-divisors for each number2=checks if count equals 2 (prime property)⍵×multiplies result with original numbers, keeping primes~0removes zero from the result
The magics are a thin layer over the
Apl class, which
you can use directly in scripts, tests, and other tooling. Calling the
session runs code and returns the output exactly as Dyalog formats it
(or None if there’s no output); APL errors raise
AplError:
from aplnb import Apl
apl = Apl()
apl('3 3⍴⍳9')1 2 3 4 5 6 7 8 9
Square brackets move values between Python and the workspace, in both directions, and take any expression:
apl['v'] = [3,1,4,1,5]
apl['{⍵[⍋⍵]}v'][1, 1, 3, 4, 5]
fn lifts an APL function into a Python callable (one argument applies
it monadically, two dyadically):
mean = apl.fn('{(+/⍵)÷≢⍵}')
mean([1,2,3,4])2.5
Sessions shut themselves down at process exit; use close, or a
with Apl() as apl: block, to do it sooner:
apl.close()- Keyboard input through
⎕or⍞can’t work in a notebook, so it raises an error. The session survives. - A cell that ends inside an unfinished block, such as an unclosed
:If, wedges the interpreter with no way back (Dyalog/ride#1401). aplnb detects this, tells you, and starts a fresh session, but workspace state is lost when it happens. %apltransfers values with⎕JSON, so it’s limited to arrays and scalars that JSON can represent, serializing to at most 32767 characters. For bigger data, write a file from APL instead.- Windows isn’t supported yet: aplnb configures Dyalog through environment variables, which is the Unix convention. macOS and Linux are supported.
To start learning APL, follow the 17 video
series run by
Jeremy Howard, and have a look at the study
notes. The ] user
commands mentioned there, such as ]Help ≠, work in aplnb too.