Dicts¶
Other resources: kx docs, oK docs, nsl: Lists are Maps
k has a beautiful dict implementation, which should be the envy of any APLer. A k dict is a key-value lookup table that can map anything to anything. To construct a dict in k, we have several options, the easiest is to join a vector of keys to a vector of values with dyadic !
:
keys:`bob`adam`sue`rita
vals:23 54 12 82
keys!vals / construct a dict
`bob`adam`sue`rita!23 54 12 82
We can list the keys using !
and the values with .
: [try it]
d:keys!vals
!d / get keys
.d / get values
`bob`adam`sue`rita
23 54 12 82
and indexing a dict follows the normal k conventions: [try it]
d[`sue]
d@`rita
d`rita`sue
12
82
82 12
Updating a dict, unlike updating a vector, has upsert semantics: [try it]
d[`sue`adam`eric`frank]:1 2 3 4 / mixture of updates and inserts
d
`bob`adam`sue`rita`eric`frank!23 2 1 82 3 4
Concatenating dicts also has upsert semantics:
d1:`bob`adam`sue`rita!23 54 12 82
d2:`adam`eric`bob`frank!99 66 44 77
d1,d2
`bob`adam`sue`rita`eric`frank!44 99 12 82 66 77
Unlike dicts found in many other languages, a dict in k is guaranteed to retain its insertion order. This is a consequence of its implementation: adding an element to a dict appends the key to the key vector, and the value to the value vector.
This means that we can take and drop items from the beginning and end, for example, taking the last two k-v pairs:
-2#`bob`adam`sue`rita`eric`frank!23 2 1 82 3 4 / take from the end
`eric`frank!3 4
As we stated above, both keys and values can be any valid k-type. For example, we frequently need values to be vectors:
sales:`Mon`Tue`Wed`Thu`Fri!(1 4 8 9 4 5;3 6;7 0 0 9;2 8 0 9 9 7 6 7;0 6 2)
sales
`Mon`Tue`Wed`Thu`Fri!(1 4 8 9 4 5;3 6;7 0 0 9;2 8 0 9 9 7 6 7;0 6 2)
In k, a dictionary is treated like any other vector when it comes to function application. Eaching a monad over a dict applies to the values, but leaves the keys, just like eaching a monad over a vector applies to a vector’s elements, but leaves its indexes. This can be extraordinarily useful. If we say that the above sales
dict holds product ids sold each day, how many sales did we make per day? [try it]
#'sales / apply monadic # to each value, retaining keys
`Mon`Tue`Wed`Thu`Fri!6 2 4 8 3
If we internalise the concept that a dict is just like a vector, but with a different domain, a lot of clever things follow. For example, we know that in k we can find the indexes of 1s in a boolean vector with &
:
&1 0 0 1 1 0 0 1 0
0 3 4 7
So what will happen if we apply &
to a dict with a boolean range?
&`rita`bob`sue`adam`frank!0 0 1 0 1 / get all keys which have a value of 1
`sue`frank
Similarly, we can use ?
– which for vectors finds the index of the first occurrence of its right argument in its left – to do a reverse lookup: find the key for a given value:
(`bob`adam`sue`rita!23 54 12 82)?12 / lookup key by value
`sue
If there were several keys holding the same value, we’d only get the first such key. This follows from the behaviour of ?
for vectors, and the fact that dicts retain the insertion ordering. If we wanted ALL matching keys, we could do
&5=`bob`adam`sue`rita!5 1 5 3 / All keys having value 5
`bob`sue
What happens if we reduce a dict? Again, thinking of a dict as a vector with a range and domain, let’s find the max of a dict:
|/`rita`bob`sue`adam`frank!12 7 87 32 11 / find the max value in a dict
87
In other words, reductions (and scans) apply to the values of a dict.
It should come as no surprise that any function application that makes sense on a vector can also be applied to a dict. Keys odd values? Sure (and thus demonstrating three different meanings of !
):
&2!`a`b`c`d`e`f`g`h!!8 / keys of odd values
`b`d`f`h
We could apply filter
directly to the dict, too, which operates on the values:
(2!)#`a`b`c`d`e`f`g`h!!8 / filter values
`b`d`f`h!1 3 5 7
The #
is special-cased for dicts if the left argument is a vector of symbols (or a string in oK and ngn/k), where it becomes take
:
`b`c#`a`b`c`d`e`f`g`h!!8 / take the `b and `c pairs
`b`c!1 2
If you take
keys that don’t exist in the dict, you get a null place-holder, as dictated by the prototype element:
`b`Q`c#`a`b`c`d`e`f`g`h!!8 / take non-existent key `Q gives a 0N as values are ints
`b`Q`c!1 0N 2
and, as a final spin on #
, we can replicate
(ngn/k-specific):
{0 1 1 0 0 0 1 1}#`a`b`c`d`e`f`g`h!!8 / pick pairs by bool map
`b`c`g`h!1 2 6 7
Perhaps we can now guess how the grades (monadic <
and >
) apply to dicts. Grading a vector results in the order by which we’d need to select from the vector in order to sort it. This holds true for dicts, too:
dict:`a`b`c`d`e`f!8 9 1 2 5 4
<dict / grade down
dict[<dict] / sort by selecting on the grade
`c`d`f`e`a`b
1 2 4 5 8 9
Monadic =
becomes group
. If you know APL, this is similar to key (⌸
). It takes a vector and creates a mapping from each unique element in order of occurrence to a vector of its indexes of occurrence:
="a cat sat on a mat"
"a ctsonm"!(0 3 7 13 16;1 5 9 12 14;,2;4 8 17;,6;,10;,11;,15)
Group
can be applied to any vector, for example
=1 2 1 1 3 8 9 9 9 1 1 8
1 2 3 8 9!(0 2 3 9 10;,1;,4;5 11;6 7 8)
If we instead wanted frequency counts, we can use the each-length
approach we used before,
#'=1 2 1 1 3 8 9 9 9 1 1 8 / lengths of groups
1 2 3 8 9!5 1 1 2 3
When using group
like this, occasionally there is the need to ensure that all keys from some set are present, defaulting to some value should they not be present in the data. For example, if we’re calculating letter frequencies in a paragraph of text, we might want to make sure that any letters that aren’t present in the text are still entered into the table, but with a value of 0. Here’s one way we can achieve that using the take
form of #
and a “null-fill” with ^
:
abc:`c$"a"+!26 / generate lower-case alphabet
0^abc##'="hereisaparagraphoftexts"
"abcdefghijklmnopqrstuvwxyz"!4 0 0 0 3 1 1 2 1 0 0 0 0 0 1 2 0 3 2 2 0 0 0 1 0 0