Language principles

Why BPL’s notation differs from APL’s, and where each choice comes from

BasedPL (BPL) keeps APL’s glyphs and its right-to-left evaluation. It changes the notation where APL’s rules make code harder to read or to write. Most of the changes follow from one decision, to write lists in brackets. BasedPL stands for Based-array Programming Language, after the array model it takes from BQN.

Many of the notation choices answer problems that Marshall Lochbaum, BQN’s author, lists in his notes on BQN’s problems and on its first five years. Syntax and Evaluation state the rules without the reasons. BPL, APL and BQN side by side shows the choices on real code.

Lists are written in brackets

In APL, arrays written side by side form a list. 1 2 3, a b c and (x+1) y are all vectors. This is called stranding. It’s short, but it gives a space a different job in different places. Between arrays, a space separates the items of a list. Between functions it means nothing: +/÷≢ and + / ÷ ≢ are the same train. Between an operator and an array, it can do either.

That last case causes real trouble. An operator takes the single item to its right as its operand, after stranding has already joined that item to its neighbours. In APL, the operand in f⍤1 M is 1 M. You have to write f⍤1⊢M, where ⊢ is there only to break the strand. BQN’s documentation gives the example a +˝∘×⎉1‿∞ b, a matrix product. No rule decides well which of 1, ∞ and b should strand together. BQN marks its strands with ‿ instead.

In BPL, lists are written in brackets, [a b c], as in Python and JavaScript. Lochbaum notes that new BQN programmers “are always expecting [] to be the list notation, and it’d let you copy-paste JSON too”. A list of literals can still leave its brackets out, as in 1 2 3 and "no" "yes". Inside brackets, names and literals mix freely:

x←5
[1 x 3]
1 5 3

Outside brackets, arrays side by side form a list too, whatever the names hold. (+/x) 3 is a two-item list whether x is a vector or a matrix. Each run, a stretch of code with no spaces in it, is one item of the list. In a b+1 the items are a and b+1. A bracket list is one run like any other:

a←1 ⋄ b←2
a b+1
[1 2] 3
1 3
[[1 2] 3]

A bracket list is a vector, however many items it has. [1] is a one-item vector, not a number:

⍴[1]
[1]ₓ

Parentheses only group, as they do in maths. (1 2) is the same value as 1 2. To make a scalar, enclose with ⊂, as in BQN. ⊂[1 2] is a scalar holding a vector, with no axes:

⍴⊂[1 2]
⍬ₓ

A function after ⊂ makes a train instead, as in ⊂+. The scalar holding + is written +ᵘ.

Array notation extends brackets to higher ranks, as Dyalog’s does. Rows separated by ⋄, or written on separate lines, become major cells:

[1 2
 3 4]
1 2
3 4

The result has one more axis than each row. Rows that are numbers have no axes, and give a vector. Rows that are one-item vectors give a column:

[1 ⋄ 2]
[[1] ⋄ [2]]
1 2
1
2

[a ⋄ b] is the Mix of [a b] when a and b have the same shape.

In parentheses, rows form a new level of nesting instead of a new axis. Each row is one item of a vector, read as if written in brackets. The rows can have different lengths:

≢¨(4 ⋄ 4 5 ⋄ 4 5 6)
[1 2 3]ₓ

A row holding one string is still a vector of one item. ("ab" ⋄ "cd") holds two one-item vectors, while ["ab" "cd"] is the vector of two strings:

≢¨("ab" ⋄ "cd")
≢¨["ab" "cd"]
[1 1]ₓ
[2 2]ₓ

Items in brackets can be functions. [+/ ÷ ≢] holds three functions. Without the spaces, [+/÷≢] holds one, the mean:

≢[+/ ÷ ≢]
≢[+/÷≢]
3ₓ
1ₓ

Writing a function expression in brackets is one way to make it a value. In BQN this takes ⊑⟨Expr⟩ or a name, a problem Lochbaum lists as “Converting a function expression to a subject is tricky”. The other way is fᵘ, a scalar holding the function f.

Items in a list are evaluated left to right, as statements are. BQN chose the same order for its lists and strands. Here each item prints as it’s evaluated, before the list itself displays:

[⎕←1 ⎕←2]
1
2
1 2

Spaces group

Outside brackets, literals separated by spaces form a strand, as in 1 2 3. Every other space separates runs. A run is a sequence of tokens with no spaces between them. Each run is evaluated first, as if it were in parentheses:

c←3 ⋄ d←4
a+b×c+d
a+b × c+d
15
21

Runs also fix the operand problem. Because a run ends at a space, +/⍤1 m applies +/⍤1 to m with no ⊢ needed. A run ending in a dyadic operator takes the next run as its operand, as if no space came between them. A literal argument after a space still joins the operand’s strand, as in f⍤1 2 3, whose operand is 1 2 3. Write such an argument in brackets or parentheses. All three of these sum each row:

m←2 3⍴⍳6
+/⍤1 m
+/⍤ 1 m
+/⍤1(2 3⍴⍳6)
3 12
3 12
3 12

A space after an operator also lets a longer function be its operand without parentheses. ⌊⌾ 10× rounds down to one decimal place, with 10× as the operand of ⌾:

⌊⌾ 10× 1.24 3.57
1.2 3.5

Lochbaum describes the problem that runs solve, under “Modifiers look looser than trains without spaces”. In BQN’s ⋆∘-ט, the - looks closer to ⋆, but it binds to ט. Writing ⋆∘- ט shows the structure. He calls it “unfortunate that this is something the writer must do rather than something the notation encourages”. In BPL the space is part of the notation.

Under “Long trains are hard for humans to parse”, he notes that readers lose track of a train’s structure after four to eight parts. Runs let spaces show that structure. ⌊ +/÷≢ is the floor of the mean:

⌊ +/÷≢ 1 2 4
2ₓ

Code that is fully spaced, or not spaced at all, groups as in APL. Only mixed spacing changes the grouping. A reader used to APL reads 1+2 × 3 as 7, but here it’s 9:

1+2 × 3
9

A tool that normalises spaces would change a program’s meaning. Other languages give spaces meaning too. K reads a -1 differently from a - 1. Nim tried an experimental mode in which the spaces around an operator changed its precedence, and removed it in version 0.20.

A run that ends in a function is a train

A run that ends in a function has no argument yet. It’s a function, built as an APL train with one addition. A subject directly before a function binds to it, as 2 does in 2×. Here 32+1.8× converts Celsius to Fahrenheit:

f←32+1.8×
f100
212

The rest follows APL’s train rules, including Dyalog’s fork with a subject on the left, 0.5×⊢+÷. Binding covers the common case of fixing a left argument, without ↣. K has the same idea, called projection, in which 2* is a function.

+/÷≢ nums and +/÷≢nums differ only in the space before nums. The first applies the train +/÷≢, the mean, to nums. The second is one run ending in a subject, the expression +/(÷(≢nums)):

nums←1 2 3 6
+/÷≢ nums
+/÷≢nums
3
1ᵣ4

If both spellings meant the same, the space would mean nothing. In everyday arithmetic the two readings agree. When the functions have arrays between them, as in 32+1.8×, the train gives the same result as the expression. The readings differ only where a fork gets a function as its left part, as in 0.5×⊢+÷. APL has the same split, with nothing in the source to mark it. Removing the argument from APL’s 0.5×⊢+÷⍵ turns it into a fork with a different meaning. In BPL the space shows which reading you get.

Selection is a function

Index, ⌷, selects. I⌷Y takes one item of I for each leading axis of Y. In K, v 1 selects, because K treats a list as a function of its positions. In BPL v 1 is a strand, because subjects and functions stay apart. One literal position can be written more briefly as a subscript, as in v₁:

v←10 20 30 40
1⌷v
v₁
20
20

Selecting with a number drops that axis from the result. Selecting with an array of positions puts the array’s shape in place of the axis. [⊂1]⌷v gives a scalar holding the item, [[1]]⌷v a one-item vector, and [[2 0]]⌷v two items:

[⊂1]⌷v
[[1]]⌷v
[[2 0]]⌷v
⊂20
[20]
30 10

Dyalog has the same rule, but its v[1] looks like a one-item list and gives a scalar. A one-item result needs v[,1].

Because Index takes its positions on the left, its right argument can be any expression. With sales keyed by city and month, "NY"⌷+/sales selects New York’s total with no parentheses:

sales←["NY" "LA";"jan" "feb"]:[1 2 ⋄ 3 4]
sales
"NY"⌷+/sales
   jan feb
NY   1   2
LA   3   4
3

Counting from 0

Positions and axes count from 0, as in BQN, J, K, Python and NumPy. Negative positions count from the end.

  • A Boolean is a valid position, as in c⌷"no" "yes". With 1-origin, the position would be 1+c.
  • Positions run from ¯n to n-1 with no gap. With 1-origin, position 0 is an error that sits between the first item and the last.
  • n|i wraps a position round a vector of length n. ⌊i÷n and n|i give the row and column of a flat position.
  • The Python API returns positions from functions such as grade and where. Those positions must agree with Python and NumPy, which count from 0.

Here the comparison is the position:

(3>2)⌷"no" "yes"
yes

Counting idioms pay for this with a 1+, as in 1+⍳10 for the numbers 1 to 10. Code from APL books and libraries assumes 1-origin and needs converting.

Two rules follow J and BQN where 0-origin left a choice. A negative length counts down, as J’s i._3 does:

⍳¯3
2 1 0

Interval Index counts the boundaries at or below each value, as BQN’s does. Below the first boundary the count is 0. A position there would be ¯1, which selects the last item:

1 3 5⍸0 3 9
[0 2 3]ₓ

Based arrays

BPL uses BQN’s based array model. Numbers, characters and functions are atoms. An array is a rectangular collection of values. Atoms are not arrays. Enclosing always adds a layer: ⊂3 is a rank-0 array holding 3, a different value from 3. Enclosing it again gives depth 2:

⊂3 ≡ 3
≡⊂⊂3
$f
2ₓ

In APL’s nested array model, enclosing 3 gives 3 again. Under the based model, every value has one representation, whether you pass it to a function, return it, or store it in an array. Functions are atoms too. [+/ ÷ ≢] is a list of three of them. Lochbaum’s page on the model gives the longer argument.

Functions act on major cells

BPL follows BQN’s leading-axis model. A function that works on a whole array acts on its major cells, the cells along its first axis. The last-axis glyphs ⌽ / \ , are the leading-axis functions applied at rank 1. Their first-axis twins ⊖ ⌿ ⍀ ⍪ are the leading-axis functions themselves. Every function therefore treats a matrix by the same rule.

Search follows that rule. ⍳ ∊ ~ ∪ ∩ compare the major cells of the searched argument, which for a matrix are its rows. Here ~ removes every row that is 1 2, leaving a one-row matrix:

rows←[1 2
      3 4
      1 2]
rows~[1 2 ⋄ 5 6]
⍴rows~[1 2 ⋄ 5 6]
3 4
[1 2]ₓ

The other argument’s cells have the same rank. A unit has no major cells to search. To remove the spaces from a string, give ~ the string " ". Giving it the character ' ', a unit, is a RANK error:

"a b c"~" "
abc

Partition splits along the first axis too, into blocks of major cells. Here new blocks start at rows 0 and 2:

1 0 1⊂rows
[[1 2 ⋄ 3 4] [1 2 ⋄]]

⊂⍤1 partitions each row instead, and ⊂⍠1 splits a matrix into blocks of columns.

Encode and Decode put digits on the last axis, as J’s #: and #. do. Encode turns each number of a vector into a row of digits. Decode turns the rows back into numbers:

d←2 2 2⊤5 6
d
2⊥d
1 0 1
1 1 0
5 6

At rank 0, the function gets each atom as itself and each array item enclosed in a scalar. BQN’s ⎉0 encloses atoms as well. The depth of each 0-cell shows the difference:

{≡⍵}⍤0 [1 [2 3]]
[0 2]ₓ

As a result, ⊢⍤0 returns its argument unchanged, even when the argument is nested. A function under ⍤0 sees numbers as literals and selection give them. Each, ¨, gives the function the items themselves.