Dialect or Derivative?
Many features described here are currently just planned.
One question I ask myself a lot is whether TinyAPL is an APL dialect or derivative. While I don't have a definitive answer, I think I lean more on the derivative side. Why didn't I call it CRP or ZOL (finally, an APL for your shoes) or , or HAP, or TinyArrayLanguage, or something like that? The first reason is historical: when I began the project I had no idea of what it would become, and also I think that I break off from traditional APL in less ways.
I suppose not being "an APL" in the strictest sense makes TinyAPL a complete misnomer, given how many think it's not tiny at all either.
None of the differences I list below are enough to make something "not an APL dialect" alone, but I believe the combination of them is.
New types
TinyAPL has a few new types, scalars and nouns.
Not only does it have the normal characters, numbers and boxes (which are a bit of a hybrid of a flat-model box and a nested-model "enclosure"), but also first-class functions through wraps (which appear in Kap and in BQN in its based-model approach), first-class modifiers (which BQN has but Kap doesn't), structs (mutable collections of pairs of identifiers and nouns/functions/modifiers, which are similar, but not identical, to Dyalog's namespaces, and will be more similar with APLAN :fingers_crossed:), potentially symbols (seen in Kap).
The biggest innovation is probably breaking the traditional APL concept of "everything [that's an argument to functions] is an array", slightly modifying it to "everything is a noun", where nouns are either the traditional arrays or dictionaries. Kap has maps, but they're scalars, not "arrays". Uiua has maps, but they act more like normal vectors with an extra tag on each element: ⊏1
(1⊇
) selects the second element of the map, which is just the second in insertion order. In my associative arrays, 1⊇
means "get the key 1". This breaks assumptions that an APL programmer might have in a few ways.
Context-free parsing
Context-free parsing is not the difference itself, of course, but what it entails is.
The most obvious difference is the naming convention. Just like in BQN, names that refer to arrays always start with a lowercase letter (abc
), names that refer to functions always start with an uppercase letter (Abc
), names that refer to adverbs always start (but not end) with an underscore (_Abc
but also _abc
), names that refer to a conjunction always start and end with an underscore (_Abc_
). BQN takes this further to an interesting point, where each name can take on all four classes and is "cast" to the type you ask for: a←3
and then A 9
"calls" a
, which when cast to a function becomes a constant function (like if you did 3˙
/3⍨
). TinyAPL doesn't do this.
The next glaring difference is the separation of dfns/dadvs/dconjs. Dfns (anonymous functions) look like {⟃code⟄}
, dadvs (anonymous adverbs) look like _{⟃code⟄}
, dconjs (anonymous conjunctions) look like _{⟃code⟄}_
. This, of course, mirrors the convention for names. Not only that, but in dfns you refer to the arguments as ⍵
(the right argument) and ⍺
(the left argument, if provided); that's fine. But in dmods, there's an issue. There have traditionally been two ways to refer to operands in APL: ⍺⍺
/⍵⍵
and ⍶
/⍹
. I would've gone with the latter, I like it more, and it means you can write ⍺⍺
instead of ⍺ ⍺
if you somehow need to refer to the left argument twice in a row. But there's an issue. Remember how names always have a set class? Well, operands can be either functions or arrays. So there need to be two names for each operand. I went with ⍺⍺
/⍵⍵
for array operands and ⍶⍶
/⍹⍹
for function operands. Yeah, I don't like it either. BQN solves the dfn/dmod distinction by looking inside the code of the block and seeing if it refers to 𝔽
and 𝔾
, its operand names. I don't really like this solution, so I went with specifying it outside.
Other choices that I did under the name of context-free parsing, which did make parsing somewhat simpler but were mostly stylstic, belong to the concept of one meaning per glyph. In Dyalog, .
is both the decimal separator and namespace member acces; I use .
for the former and →
for the latter. In most APLs, E
is both a valid identifier and the exponent notation symbol; for the latter I chose ⏨
. Same applies to J
for complex numbers, where I went with ᴊ
.
Trains
The other difference that's quite obvious for someone coming from, say, Dyalog, is trains.
The context-free parsing part is just that, because I allow modifier trains, you specify the type of a train just like for names. (see, there's a pattern!)
But the obvious part is not that, it's that trains aren't invisible. That is, in Dyalog you write "union without intersection" (aka §
Symmetric Difference) as ∪~∩
, in TinyAPL it's ⦅∪⋄~⋄∩⦆
. Quite different. This does allow for a much larger set of possible tines of trains, and easier chains of atops, as documented in the train documentation page.
Literal arrays
From the first APLs, the way to write vectors of numbers has always been juxtaposition: 1 2 3
. This was later changed to support any expression: ("hello")(9+17*2)
. There are two issues with this system. The first is that the binding strength of stranding is unclear: some dialects bind it tighter than function application and some looser. The second is that you can't write two values next to each other, even if they're meant to be used in different contexts: in Dyalog ⊖⍤¯1 2↑y
parses as ⊖⍤(¯1 2)↑y
, not as (⊖⍤¯1)2↑y
. This is very annoying, especially if you use a lot of Rank like you do with a leading-axis oriented language like TinyAPL. BQN solves this problem by making stranding explicit, using the tie character ‿
. TinyAPL takes this feature exactly.
So, the way to write a (non-singleton) vector in traditional APL is to put the elements next to each other, with as many parentheses as needed. How about creating a matrix, or a 3D tensor? Historically, there's never been a way. More recently, APLAN was invented. It introduces an alternative syntax for vectors (elem1♢elem2)
as well as a syntax for "high rank" arrays, which builds them up from major cells: [cell1⋄cell2]
. Single-element vectors and high rank arrays are also possible by writing (elem⋄)
and [cell⋄]
. There's also a syntax for namespaces: (name1:val1⋄name2:val2)
. BQN slightly changes array notation by using ⟨
and ⟩
instead of (
and )
for vectors, which means you don't need trailing ⋄
s for one-element vectors and one-cell arrays: ⟨elem⟩
and [cell]
. BQN's namespace notation is a special case of block syntax, when you write a ⇐
for assignment: { name1⇐val1 ⋄ name2⇐val2 }
. TinyAPL uses BQN's syntax for arrays but changes namespace notation to its struct notation: ⦃ name1←val1 ⋄ name2←val2 ⦄
.
Bracket indexing
A feature that, as far as I can tell, appears in most APL dialects and implementations, no matter how old, is bracket indexing: arr[ind1;ind2]
, which extracts the ind1
-row, ind2
-column item from arr
, which has to be a matrix. The main issue with this is again binding. Should ⊖x[2 3;]
bind as ⊖(x[2 3;])
, or (⊖x)[2 3;]
? More recent developments in APL introduced the ⌷
Indexfunction, and then later ⊇
From, which solves some usability issues with Index. I chose to only support the last two, dropping bracket indexing entirely.
Leading-axis
Just like BQN, TinyAPL drops last-axis primitives entirely. There is no reverse last, there is just reverse (first) on vectors, ⊖⍤1
. It also removes function axis, an outdated way to specify what axes a function should apply to, which is ad-hoc and not extendable.
Extra arguments
Some languages extend the concept of function axis to dfns, using the identifier χ
. While this is interesting, I already decided I'd drop function axis, so I came up with something slightly different: extra arguments. I've described them multiple times as function axis meets Variant meets Python kwargs. They look like this: ⍳⦋"origin":0⦌
. Instead of passing any array, like Variant, which often ends up being key-value pairs anyways through nested vectors, you pass a dictionary. You can write ⍳⦋⟨"origin":0⟩⦌
, but there's a shorthand for that. You can access them in dfns via ɛ
and dmods via ɛɛ
. Because they're dictionaries, all primitives already work with them. They're designed to be lax, so if you want to write a dfn that allows for both origin setting and comparision setting you can write {... ⍳⦋ɛ⦌ ... =⦋ɛ⦌ ...}
, and then specify any combination of extra args when calling. The short syntax also allows for always using a certain setting in a scope, even if it requires some care from the programmer: s←⟨"origin":1⟩
and then ⍳⦋s⦌
. I like this more than the scoped configuration special variables ⎕io
et cetera.