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
1 4 7 2 3 9 0
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
(1 2 3
4 5 6
7 8 9)
We can also place characters and symbols in vectors,
("Hello world"; `h`e`l`l`o)
("Hello world"
`h`e`l`l`o)
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
`sym
and, as we saw already, a vector of symbols requires no spaces:
`a`b`c`d
`a`b`c`d
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"
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
23 45 56
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
23 45 56
K supports scalars, too, unsurprisingly:
5
5
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
,5
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
6
2
1
1
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()
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
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
5
1
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
1
0
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
(3 1 2 4 5
8 6 9 8 4
5 2 3 4 3)
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
(1 2 3 1 2
3 1 2 3 1
2 3 1 2 3)
3 3#1 2 3 4 5 6 7 8 9 10 11 12 / too many, truncate
(1 2 3
4 5 6
7 8 9)
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
((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))
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
(2 5
4 2
6 5
3 7
5 3
7 5)
…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
(2 5
4 2
6 5
3 7
5 3
7 5)
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]
:m:3 5#!15 / an example matrix
(0 1 2 3 4
5 6 7 8 9
10 11 12 13 14)
m 0 1 0 1 / index by 'raw' vector
(0 1 2 3 4
5 6 7 8 9
0 1 2 3 4
5 6 7 8 9)
m@0 1 0 1 / index @ single argument vector
(0 1 2 3 4
5 6 7 8 9
0 1 2 3 4
5 6 7 8 9)
m[0 1 0 1] / bracket index
(0 1 2 3 4
5 6 7 8 9
0 1 2 3 4
5 6 7 8 9)
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
11
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
11
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
(10 11 12 13 14
5 6 7 8 9)
11
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
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
(15 16 17 18 19
20 21 22 23 24
25 26 27 28 29)
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
21
If we wanted to pick column 3 from both layers we’d say
c:2 3 5#!30
c[;;3]
(3 8 13
18 23 28)
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!
((6 7 8;6 7 8)
(3 4 5;3 4 5))
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
1 5 3 2 -2 7 0
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
-2 5 3 2 -2 7 -2
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
((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))
You cannot, however, use multiple bracket pairs to set values at depth:
m:2 3 5#!30
m[1][1][1]: -2 / NB: error!
'compile
m[1][1][1]: -2 / NB: error!
^
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
99 66 52 110 88 35
Indeed, we can add a scalar to any shape vector, even ragged ones: [try it]
:m:(3 1 2;5 4 3;(8 1 2;2 3 4)) / ragged array
(3 1 2
5 4 3
(8 1 2;2 3 4))
m+55 / add 55 to each atom
(58 56 57
60 59 58
(63 56 57;57 58 59))
We can add two vectors:
26 34 57 98+76 53 48 10
102 87 105 108
…provided they’re of the same length:
26 34 57 98+76 53 48 / 'length error
'length
26 34 57 98+76 53 48 / 'length error
^