Vectors everywhere

k is a vector language. In practice, what this means is that the vector is the only data structure available out of the gates. Well, it does have dicts, too, but they’re really just a pair of vectors under the hood (we’ll get to those in a bit).

We can make a vector by simply listing a bunch of elements, separated by space:

1 4 7 2 3 9 0         / a simple numeric vector
Copy to clipboard
1 4 7 2 3 9 0
Copy to clipboard

The elements of a vector can be anything, including other vectors (at any level of nesting):

(1 2 3;4 5 6;7 8 9)   / a nested vector - nested elements separated by semi-colon
Copy to clipboard
(1 2 3
 4 5 6
 7 8 9)
Copy to clipboard

We can also place characters and symbols in vectors,

("Hello world"; `h`e`l`l`o)
Copy to clipboard
("Hello world"
 `h`e`l`l`o)
Copy to clipboard

Indeed, k, like APL, does not have a separate data representation for strings – they’re just vectors of characters. And, yes, we sneakily introduced another fundamental k-type there, the symbol. A symbol in k is denoted by a leading back-tick, like so:

`sym
Copy to clipboard
`sym
Copy to clipboard

and, as we saw already, a vector of symbols requires no spaces:

`a`b`c`d
Copy to clipboard
`a`b`c`d
Copy to clipboard

We can assign a vector to a variable. K uses the colon : to denote assignment:

a:1 4 7 2 3 9 0
b:"some text here"
Copy to clipboard

Note that this time we got nothing back. This is because k’s assignment does not return a value. To see the value of a variable, we simply refer to it by name:

var:23 45 56
var
Copy to clipboard
23 45 56
Copy to clipboard

or we can use monadic :, dex, which is a verb that returns its right argument, like APL’s right tack, . “Dexter” means “right” in Latin. We’ll use that to make variable assignments visible in output occaionally:

:var:23 45 56    / dex the assignment for output
Copy to clipboard
23 45 56
Copy to clipboard

K supports scalars, too, unsurprisingly:

5
Copy to clipboard
5
Copy to clipboard

A scalar isn’t in fact a 1D vector with a single element, which presents us with a bit of a conundrum: if I do want a vector with a single element, how do I create that? For this we use a monadic comma, ,, called enlist:

,5                   / a vector containing the element 5
Copy to clipboard
,5
Copy to clipboard

Count

Count, monadic #, gives the cardinality of its argument:

#3 4 5 1 23 5        / a vector containing 6 elements
#(1 2 3;3 2 1)       / a nested vector containing 2 elements
#,5                  / a vector containing 1 element                 
#5                   / a single scalar
Copy to clipboard
6
2
1
1
Copy to clipboard

Hold on! Didn’t you just say that a scalar isn’t a single-element vector? If so, why is 1 = #5? The # is cardinality, not shape. But it’s a fair question, one which has either a dirt simple, or a deep, complex answer.

Python, for example, has a different opinion than k. Well, it would, wouldn’t it?

>>> len([3, 4, 5, 1, 23, 5])    # length of a vector
6
>>> len(5)                      # length of a scalar is an error
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: object of type 'int' has no len()
Copy to clipboard

APL has a tally primitive, too, . APL thinks like k in this regard:

 ≢,5     ⍝ tally a vector with one element
 
1
 
 5      ⍝ tally a scalar
 
1
Copy to clipboard

The dirt simple explanation is that, in k, monadic # simply returns the “number of things to the right”, which for a scalar is 1. This should be a sufficient explanation for practical programming purposes. The book Q for Mortals, for example, leaves it at that:

Observe that the count of an atom is 1 even though an atom is not a list.

If you’re OK with that for now, feel free to skip the next bit, with sanity intact.

For the more complex explanation, we need to consider the fundamental nature of vectors and scalars in k. Adám Brudzewsky explained it in APL-terms in the chat room APL Orchard:

A set of coordinates is simply a list of positions along each of the axes of the array, in the canonical order of axes.

  • In a 3D array, we need exactly 3 coordinates, e.g. (3,1,4) to address an element.

  • In 2D array (a matrix/table), we need exactly 2 coordinates.

  • In a 1D array (a vector/list), we need exactly 1 coordinate.

  • In a 0D array (a scalar), we need exactly 0 coordinates.

So the number 5 is just a 0-dimensional array where the sole element has the coordinates [] (in JSON).

That explanation holds true for k, too: a scalar can be viewed as a 0D vector of one element which is accessed with an empty coordinate vector.

@ngn opined:

@xpqz the fact that a scalar has empty shape and exactly 1 item is not controversial. it’s just that 1=≢scalar doesn’t sound as convincing to some people.

Don’t worry too much if that does feel controversial.

But the question remains - how do you distinguish between a scalar and a 1D vector with 1 element? They are different. There are a couple of ways in k to check if something is a scalar, for example by matching with the first element. We can find the first element of something with monadic *, which you by now should expect works for both scalars and vectors:

*5             / first element of scalar 5
*1 2 3 4 5     / first element of vector 1 2 3 4 5
Copy to clipboard
5
1
Copy to clipboard

Match, dyadic ~, is basically “deep equals” – true if both the structure and contents of the two things being compared are the same.

scalar:5
vector:1 2 3 4 5
scalar~*scalar
vector~*vector
Copy to clipboard
1
0
Copy to clipboard

Reshape

Dyadic # is reshape, cribbed wholesale from APL’s dyadic . It lets you create nested vectors of arbitrary depth:

3 5#3 1 2 4 5 8 6 9 8 4 5 2 3 4     / a 3x5 matrix
Copy to clipboard
(3 1 2 4 5
 8 6 9 8 4
 5 2 3 4 3)
Copy to clipboard

The left argument lists the length of each axis; in our case, rows and columns. K constructs this structure for us, and uses the elements to the right, in order, to fill each row in turn. If we haven’t given the perfect count of elements to match the axis specification, we’ll either start from the beginning again, or finish early:

3 5#1 2 3           / too few elements; repeat
Copy to clipboard
(1 2 3 1 2
 3 1 2 3 1
 2 3 1 2 3)
Copy to clipboard
3 3#1 2 3 4 5 6 7 8 9 10 11 12 / too many, truncate
Copy to clipboard
(1 2 3
 4 5 6
 7 8 9)
Copy to clipboard

Axes are numbered from the left. If we want a cuboid, that is something with a z-axis, depth becomes the first axis:

2 3 5#!30
Copy to clipboard
((0 1 2 3 4;5 6 7 8 9;10 11 12 13 14)
 (15 16 17 18 19;20 21 22 23 24;25 26 27 28 29))
Copy to clipboard

So now we have two layers, each forming a 3x5 matrix.

A supremely useful aspect of # is that you can set axes to “null” (0N) and it will be taken to mean “to the max”. An example might help: let’s say that you have a simple vector that you want to “fold” pairwise to form a 2-column table. One way you could do this is to divide its length by 2 to get the number of rows, and then reshape. It certainly works:

v:2 5 4 2 6 5 3 7 5 3 7 5
rows:-2!#v                   / -i! is div: integer division
(rows;2)#v
Copy to clipboard
(2 5
 4 2
 6 5
 3 7
 5 3
 7 5)
Copy to clipboard

…but really–that’s beyond fugly. This is quite a common thing to do, so we can instead just state that we want the first axis to contain as many items as the data allow, whilst keeping the length of each row to 2:

v:2 5 4 2 6 5 3 7 5 3 7 5
0N 2#v
Copy to clipboard
(2 5
 4 2
 6 5
 3 7
 5 3
 7 5)
Copy to clipboard

This is a feature I wish APL would steal.

Indexing

K has several ways in which to index into things (although not as many as APL, thankfully), but the key idea is that indexing and functional application are syntactically identical. Or you can flip it to say that k has several ways of calling a function, and indexing is functional in k. We’ve already seen how we can call functions – we simply list the arguments as a vector. In terms of indexing, the following expressions do the same thing: select row 0, row 1, row 0, row 1 from the matrix called m: [try it]

If m had been a verb instead of a vector, the effect would have been to call the verb m with the argument vector 0 1 0 1.

But what if we want to index ‘at depth’ in our vector, say picking out an individual element? Well, we have a few options. As indexing can be seen as functional application, we could select the 11 at row 2, col 1 by

m:3 5#!15
m[2][1]         / first select row 2, then select element 1 from that
Copy to clipboard
11
Copy to clipboard

However, indexing at depth is a pretty common thing to want to do, so there is a convenient short-hand for this, also borrowed from function application:

m:3 5#!15
m[2;1]          / index at depth
Copy to clipboard
11
Copy to clipboard

Note that the semi-colon-separated expression inside the square brackets is not a vector, it’s a function application with two arguments. It’s worth taking a minute to internalise this difference:

m:3 5#!15
m[2 1]          / function call with the single argument vector 2 1: select row 2 and row 1
m[2;1]          / function call with two arguments, 2 and 1: select element at row 2, col 1
Copy to clipboard
(10 11 12 13 14
 5 6 7 8 9)
11
Copy to clipboard

It might help comparing with Python:

m([2, 1])  # call function m with a single argument, the list [2, 1]
m(2, 1)    # call function m with two arguments, 2 and 1
Copy to clipboard

Indexing at depth is also a convenient way to slice arrays. For example, we can pick a particular layer from a multi-dimensional vector:

c:2 3 5#!30
c[1;;]                / layer 1, all rows, all columns
Copy to clipboard
(15 16 17 18 19
 20 21 22 23 24
 25 26 27 28 29)
Copy to clipboard

The bracket indexing expression lists the axes in order, separated by semi-colon. Omitting an axis means “all”. It follows that we can drill down to any depth. If we want a single element we need to give a full specification:

c:2 3 5#!30
c[1;1;1]                  / 21
Copy to clipboard
21
Copy to clipboard

If we wanted to pick column 3 from both layers we’d say

c:2 3 5#!30
c[;;3]
Copy to clipboard
(3 8 13
 18 23 28)
Copy to clipboard

An additional quirk that might not be obvious: if we’re indexing with a vector, it can be of an arbitrary shape. The shape of the argument vector dictates the shape of the result:

m:3 3#!9
m[(2 2;1 1)]              / note: this is NOT indexing at depth!
Copy to clipboard
((6 7 8;6 7 8)
 (3 4 5;3 4 5))
Copy to clipboard

Mutation

We can modify values in existing vectors in a couple of ways in k. We’ll get to the funkier ones (amend and dmend) later on, but assignment via bracket indexing should feel familiar:

v:1 5 3 2 8 7 0
v[4]: -2                  / change element as pos 4 to -2
v
Copy to clipboard
1 5 3 2 -2 7 0
Copy to clipboard

As we saw earlier, we can bracket index with vectors, and this idea extends to assignment, too:

v:1 5 3 2 8 7 0
v[0 6 4]: -2              / change elements as pos 0 6 and 4 to -2
v
Copy to clipboard
-2 5 3 2 -2 7 -2
Copy to clipboard

For nested vectors, we need to use indexing at depth:

m:2 3 5#!30
m[1;1;1]: -2              / change element at layer 1, row 1, col 1 to -2
m
Copy to clipboard
((0 1 2 3 4;5 6 7 8 9;10 11 12 13 14)
 (15 16 17 18 19;20 -2 22 23 24;25 26 27 28 29))
Copy to clipboard

You cannot, however, use multiple bracket pairs to set values at depth:

m:2 3 5#!30
m[1][1][1]: -2            / NB: error!
Copy to clipboard
'compile
m[1][1][1]: -2            / NB: error!
          ^
Copy to clipboard

This is because the intermediate vectors that each bracket pair returns are ephemeral copies, rather than references. It would have worked in Python.

Atomics

A fundamental aspect of k is that many of its verbs are atomic, or pervasive. In simplified terms, this means that a verb that takes scalar arguments will drill through into vectors of any shape and apply at the relevant level. For example, we can add a scalar to a vector:

23+76 43 29 87 65 12
Copy to clipboard
99 66 52 110 88 35
Copy to clipboard

Indeed, we can add a scalar to any shape vector, even ragged ones: [try it]

We can add two vectors:

26 34 57 98+76 53 48 10
Copy to clipboard
102 87 105 108
Copy to clipboard

…provided they’re of the same length:

26 34 57 98+76 53 48       / 'length error
Copy to clipboard
'length
26 34 57 98+76 53 48       / 'length error
           ^
Copy to clipboard