K Slices, K Dices

This article summarizes a lecture I performed several times over the years as part of 1010data’s summer intern training program, discussing indexing and function application. These features are among the great pearls of K, and an essential component of practical programming. Examples have been tweaked slightly to reflect the behavior of oK (which represents the k6 dialect) rather than k3, but most of the discussion applies to all K dialects.

Indexing, Four Ways

K offers several ways to access an element from a list or vector by index:

  t:11 22 33 44 55 66;

  t[4]     / subscripting
55

  t 4      / juxtaposition
55

  t@4      / the 'at' verb
55

  t . 4    / the 'dot' verb
55

(Take special note of the need for whitespace between . and 4: without the space, K would interpret .4 as a decimal number!)

At first glance, these all seem equivalent. How are they different? Indexing in K generalizes to vectors:

  t[1 2 3 2 1]
22 33 44 33 22

  t 1 2 3 2 1
22 33 44 33 22

  t@1 2 3 2 1
22 33 44 33 22

This is important for the common idiom @< for sorting:

  u:33 27 19 14 99 50;
  
  <u
3 2 1 0 5 4

  u@<u
14 19 27 33 50 99

However, . doesn’t seem to work like the other approaches when indexing by a vector:

  t . 1 2 3 2 1

rank error
t . 1 2 3 2 1
  ^

Let’s focus on bracket-indexing, for the sake of consistency, and come back to the alternatives later.

Consider indexing into a matrix. K matrices are a list of lists, so the “outer” dimension is the row and the “inner” dimension is the column:

  m:("ABCD"
     "EFGH"
     "IJKL"
     "MNOP")

  m[1]         / row
"EFGH"

  m[1;3]       / row ; column
"H"

  m[1 2]       / rows
("EFGH"
 "IJKL")

  m[1 2;0 2]   / rows ; columns
("EG"
 "IK")

  m[0 2 0 2;1 3 1 3]
("BDBD"
 "JLJL"
 "BDBD"
 "JLJL")

When indexing with brackets, an empty dimension is a “wildcard”:

  m[;1 2]   / all rows, columns 1 and 2
("BC"
 "FG"
 "JK"
 "NO")

  m[1 2;]   / equivalent to m[1 2]
("EFGH"
 "IJKL")

In general, juxtaposition of a value and an index is equivalent to the verb @ (apply):

  m 1 2
("EFGH"
 "IJKL")

  m@1 2
("EFGH"
 "IJKL")

Square brackets are equivalent to the verb . (apply-at-depth):

  m[1 2;1 3]
("FH"
 "JL")

  m . (1 2;1 3)
("FH"
 "JL")

And @ is equivalent to . if we enclose its argument, restricting it to operate on the outermost dimension:

  m@1 2
("EFGH"
 "IJKL")

  m . ,1 2
("EFGH"
 "IJKL")

Hopefully this makes the error from t . 1 2 3 2 1 clear: the value t does not have a sufficient rank (count of dimensions) to index-at-depth through 5 dimensions!

Application, Four Ways

You may have noticed that brackets are also used for calling functions. What gives?

  f:{x+100*y};

  f[4;9]
904

In point of fact, we can call a function in the same four ways we can index a list:

  {2+x}[4]    / subscripting
6

  {2+x}4      / juxtaposition
6

  {2+x}@4     / the 'at' verb
6

  {2+x}. 4    / the 'dot' verb
6

In K, indexing and calling functions (application) are identical ideas, so they have identical syntax. This is a profound symmetry!

What happens if we use a “wildcard” index on a function or don’t specify every argument? We get back a “projected” function which remembers the arguments that have been supplied already but still needs values for any empty slots:

  f[4]
{[x;y]x+100*y}[4;]

  f[;9]
{[x;y]x+100*y}[;9]

  (f[;9])4
904

In functional languages the process of producing a new function from an existing one by supplying a fixed value for some arguments is called partial application. The behavior of K’s bracket syntax and projection provides an unusually flexible mechanism for applying any subset of a function’s arguments in any order.

Note that this works on built-in verbs, too:

  ,[;"foo"]
,[;"foo"]

  ,[;"foo"]"bar"
"barfoo"

Consider the parallels between a “partially sliced” matrix:

  b:{x{x+2*y}/:\:x}[!4]
(0 2 4 6
 1 3 5 7
 2 4 6 8
 3 5 7 9)

  b[1]
1 3 5 7

  b[;2]
4 5 6 7

  b[1;2]
5

And a partially-applied dyadic function:

  f:{x+2*y};

  f[1]'!4
1 3 5 7

  f[;2]'!4
4 5 6 7

  f[1;2]
5

In a very real sense, K data structures are functions, and functions are (a shorthand for) data structures! In the same way that we can project a column out of a table, we can project a specialized function out of a general one:

customerNames:customers`name

getSales:getTable[(user;password;sessionid);`sales;]

Most dialects of K do not feature lexical closure for functions; functions only have access to global variables, their own local variables, and their own direct arguments. Projections offer an explicit way to package a function with arguments such that they travel together. By projecting a complex function down to just one or two missing arguments, it becomes easy to use with K adverbs, many of which expect to manipulate a monad or dyad.

Which Form, When?

K is highly parsimonious in its featureset, so the fact that it offers four ways to handle indexing suggests how important these alternatives are!

Juxtaposition is the least visually “noisy”, and is typically preferred when it can do the job:

bar[foo[23]]    / bracketed (unnecessarily nested)
bar@foo@23      / 'at' verb (somewhat less readable)
bar foo 23      / juxtaposed (best)

The @ verb is often useful as an argument to an adverb, or when juxtaposition would be ambiguous:

  (#:;*:)@\:/:("Apple";"Banana";"Cherry")
((5;"A")
 (6;"B")
 (6;"C"))

(11 22 33)44 55 66?x    / juxtaposition of two adjacent vector literals requires parens
11 22 33@44 55 66?x     / the 'at' verb is more concise

Bracket-indexing is the most natural way to handle multiple function arguments or indexing dimensions. In older dialects of K (k2, k3), projection was syntactic, and only bracket-indexing produced this effect. Newer dialects (k5, k6) tend to make projection semantic: any form of application which leaves “missing slots” will produce a projection:

  {x+100*y}2
{[x;y]x+100*y}[2;]

  {x+100*y}@2
{[x;y]x+100*y}[2;]  

  {x+100*y}.(;2)
{[x;y]x+100*y}[;2]

The . verb is the least common of the above in everyday programming. Sometimes it’s handy as a suffix when you’d like a monadic function that consumes a tuple and immediately destructures it into individual arguments:

p2c: {x*(sin y;cos y)}.  / polar (r,a) to cartesian (x,y) coordinates

As always in K, aim for formulations which are concise, flat, and internally consistent.

Summing Up

Further Reading

back