BQN: What Is a Primitive?

tosh1 pts0 comments

BQN: What is a primitive?

(github) / BQN / commentary

What is a primitive?

People sometimes wonder how the set of primitives in BQN was chosen. Outsiders to array programming might assume that the "big idea" of APL is just to take the most common tasks and write them with symbols instead of names—even Dijkstra said something like this, calling APL a "bag of tricks"! I don't think this is quite right, so I'd like to explain my personal view on why it makes sense to call a few special operations "primitives" and give them dedicated symbols. While I think this overlaps some with the ideas of other array designers, I am speaking only for myself here.

Names versus symbols

Much of this text will be about why various functions shouldn't be considered primitives, so it makes sense to start with a discussion of the disadvantages of symbols and why we wouldn't want everything to use them.

Words convey meaning more precisely

Words can be modified or combined in more sophisticated ways

There are many more words with established meanings

Words are easier to input and pronounce

A broad theme is that words—that is, language rather than notation—offer more possibilities and shades of meaning. If there are two related concepts, it's often possible to give them names that make both the relation and the difference clear (say, KeepBefore and KeepUpTo). With symbols this is more difficult: only a few broad relations like mirror reflection (which is error prone!) and juxtaposition are usable. But nuanced words can also be a liability. If there is really only one possibility, then a well-chosen symbol might indicate it better and be easier to remember.

In BQN, syntactic role is also a factor. Letter casing and underscores allow any word to be written with any role, while a primitive has a fixed role. If a value might be either called as a function or passed as an argument, this will be easier if it's named.

Primitive philosophy

Is language design a process of discovery or invention? A bit of both, of course, but I think primitive design should be mostly discovery. That is, if a function feels invented—or worse, engineered—it's not a good fit for a BQN primitive. Discovery means that the thing in question is in some sense external to the person who describes it, so that if two different people discover something then it will be the exact same thing. An invented thing will always bear marks of its inventor, from the little decisions that could have been made differently.

It's not always so clear cut, and there's a little irony in that the collection of all the primitives in a language is definitely engineered. While I think it's a good idea to give symbols to things that are more primitive-like and words to things that are less primitive-like, this leaves room to use symbols relatively more as in APL, or words more as in Python, to focus on different functionality to provide, or to select different primitives when there are a few that provide similar functionality.

I do think some acts of engineering are okay, if the purpose is only to make the underlying primitive functionality more accessible. For example, Range (↕) combines two primitive functions with disjoint domains, and Rank (⎉) sticks a few numbers into an arbitrarily-ordered list. I still view these as a little unfortunate, but acceptable for added convenience.

Primitive practice

Are there primitives out there waiting to be discovered? I think so: fundamental concepts like addition, mapping, or joining one list to another would be reinvented by any society that could develop programming, and would have exactly the same meaning. But this is sort of a vague assertion, and I have no intention of getting into the question of whether anything "actually" exists. A more practical approach is to start with some properties that I associate with high-quality tools of thought:

Simple mathematical description (or better, more than one)

Simple specification in terms of constraints (ditto)

Can be used to implement other operations

Few or no useful variations are possible

These properties tend to go together: while none of them necessarily implies another, it seems rare for a function to only fit half of them. And there are arguments why we might expect this to be the case, in addition to the empirical observation. For example, if the results of a function could be specified by either of two independent constraint sets, it probably doesn't make sense to define a variation that's different on some arguments, because it wouldn't follow those same constraints. And usually complex things are implemented in terms of simpler ones, so a function that gives a nice implementation of another will tend to be simpler.

More surprising, operations that fit those criteria often have some other benefits:

Fewer edge cases to worry about

Relevant across multiple domains

Efficient implementation (theoretically and in hardware)

Combinations of primitives give useful...

primitive words primitives symbols like think

Related Articles