流沙河鎮

情報技術系のこと書きます。

q言語基礎_2_List

# 以下は2020年頃に執筆した過去ブログのアーカイブです。現在メンテしておらず、一部の情報が古い可能性があります

へんてこデータベースもどき「kdb」と、知る人ぞ知るその実装言語「q」の基本を解説する本シリーズ。
本連載の目的はq言語の基本的な情報、実務で用いられる実践的なテクニックを日本語で分かりやすく提供することである。細かい情報や定義を知りたい時は適宜「q for mortals」及び「q Tips」を参照されたい

q for mortals
https://code.kx.com/q4m3/2_Basic_Data_Types_Atoms/

q Tips
https://www.amazon.com/Tips-Fast-Scalable-Maintainable-Kdb-ebook/dp/B00UZ8OMME

List

今回はAtomと並んでqの最も基本的なデータ型であるListについて解説する。
qはvector指向言語であるから、Listの振舞は様々なロジックの根幹を成しており、その意義は他言語と比較しても大きく、単なるいちデータ型として片づけられないほど重要だ。
然るにその本質は単に順序付けされた要素の集合に過ぎない。Listを構成するのは、AtomないしListである。
Listを宣言する時は以下のようにする。

q)hoge : (1 ;2 ;3) /simple list
q)type hoge
7h
q)hoge : 1 2 3 /上段と等価
q)type hoge
7h

q)hoge : (1;`a;1.1) /型が混ざっているパターン(general list)
q)type hoge
0h

q)hoge : (1 ; (2.1;2.2;2.3); 3 ) /Listの中にListがネストしているパターン(general list)
q)type hoge
0h

q)/Listはtype関数の返り値が正の値が返る。Atomは負の値になる。
q)type 1 
-7h
変数を使ったListの宣言

変数を構成要素とするListを宣言する場合、直感的には以下のように書きたくなると思うが、これではエラーになってしまう。

q)90 n 20
'Cannot write to handle 10. OS reports: Bad file descriptor / nがファイルハンドラと認識されてエラーになっている
  [0]  90 n 20

ではどうするかと言えば、joinを用いて以下のようにする。

q)n:10
q)90,n,20
90 10 20
Simple ListとGeneral List

ListはSimple ListとGeneral Listの2種類に分けられる。
Simple Listは全て同じ型のAtomで構成されたListであり、General Listは2種類以上のAtomか、ネストされたListを要素に含むListである。
両者は文法上の振舞に加えて処理効率も異なるため、明確に区別して扱う必要がある。

Simple List

Simple Listはデータサイズがgeneral Listに比べて小さく、処理速度が速いことが特徴である。*1 処理速度の観点から、kdbのテーブルを設計する上でもカラムは極力Simple Listにするべきだ。*2 特に、レコード数が非常に多くなるテーブルがGeneral Listを含む場合、リアルタイムデータを静的データへpersistanceする処理などが異常なまでに遅くなってしまうリスクがある。

  1. Simple Listのtype値

Simple Listでは、type値として内包するAtomの型を示す値が返る。

q)hoge : (1 ;2 ;3)
q)type hoge
7h // long
  • SymbolのList

SymbolのListを宣言する方法は若干特殊で、以下のようにする。

q)hoge:`HOGE`MOGE`FUGA /スペースを空けない
q)hoge
`HOGE`MOGE`FUGA
  • String

charのListがStringであり、簡略化された宣言方法が用意されている。

q)hoge:("s";"a";"m";"e")
q)moge:"same"
q)hoge ~ moge
1b
要素が1つだけのリスト

要素が1つだけのリストは以下のように宣言することが出来て、これはAtomとは別物である。

q)hoge : enlist 1
q)type hoge
7h
q)hoge : 1
q)type hoge
-7h
要素が1つもないリスト(Empty List`)
General Empty List

要素が1つもないListは以下のように宣言できる。これも一種のGeneral Listである。

q)hoge : ()
q)type hoge
0h
Typed Empty List

General Empty Listをキャストすることで、入るべきデータの型を明示したEmpty Listを作ることも出来る。

q)hoge : `int$()
q)type hoge
6h
q)hoge : `float$()
q)type hoge
9h
q)hoge,:1i
'type
  [0]  hoge,:1i /指定した型と異なるデータを追加しようとするとエラーになる
Listの操作

Listのデータにはindexを用いてアクセス出来る。

q)hoge: 1 2 3 4 5
q)hoge[0]
1
q)hoge[1]
2
q)hoge[1]:3 /2番目のデータを書き換え
q)hoge
1 3 3 4 5

ここで重要なポイントとして、対象がSimple Listである場合、型が異なるデータの挿入はエラーになる。
自動でGeneral Listに変換してくれるわけではないし、仮に対象がより広い数値型であったとしても挿入は出来ない。

q)hoge: 1 2 3 4 5
q)hoge[1]:`symboldayo
'type
  [0]  hoge[1]:`symboldayo

q)hoge:1.1 1.2 1.3
q)hoge[1]:1i
'type
  [0]  hoge[1]:1i

逆に、General Listのデータが書き換えられたことで型が統一された場合、そのListは自動的にSimple Listになる。
当然ながら、その場合別の型のデータを挿入することは不可能になる。。

q)hoge: (1 ;`HOGE ;3 ;4 ;5)
q)type hoge
0h      /General Listなので0hが返る
q)hoge[1]:2
q)type hoge
7h
q)hoge[1]:`HOGE /Long型のSimple ListになってしまったのでSimbolは挿入できない!
'type
  [0]  hoge[1]:`HOGE
              ^

また、List長を超えるindexを与えた場合、エラーにならずmissing valueという特殊なデータ型が返される。
OutBoundExceptionのような挙動はしないので、注意が必要だ。

q)hoge: 1 2 3 4 5
q)hoge[5]
0N
ネストされたList

ネストされたListへのアクセスは以下のように行う。

q)hoge: (1; (1;2;3);3) /ネストされた配列に対してはこのようにアクセスする
q)hoge[1]
1 2 3
q)type hoge[1] /ネストされたListも当然にListとして振舞う
7h

ネストされたListのデータを操作する方法は似て非なるやり方が2つあり、ややこしい。

  1. 再帰的なアクセス

1つはListに対して以下のように再帰的にindexを付与することで、目的のデータに辿り着く方法だ。
ポイントとして、この方法では対象のデータを書き換えることはできない。ここで扱っているのはiterativeに取得された中間データ(ephemeral data)であって、参照先エンティティの実体ではないためだ。

q)hoge: (1; (1;2;3);3)
q)hoge[1][0]
1
q)hoge[1][0]:12
'assign
  [0]  hoge[1][0]:12
                 ^
  1. Indexing at Depth(上手い日本語訳が思いつかない)

もう1つは以下のようにして、一気に目的のデータまでたどり着く方法である。この場合は対象データを書き換えることが出来る。

q)hoge: (1; (1;2;3);3)
q)hoge[1;0]
1
q)hoge[1;0]:12
q)hoge[1;0]
12

また、Indexing at Depthを応用したIndexingとして、Elided Indices(上手い日本語訳が思い付かない)という技法がある。
Indexing at Depthの一部を省略することで、Listに含まれるデータの一部を纏めて‘’スライス”することが出来る技法だ。

q)hoge:(1 2 3 4; 10 20 30 40 ; 100 200 300 400)
q)hoge[1;]
10 20 30 40
q)hoge[;1]
2 20 200

1つ目はhoge[1]と等価なので大した話ではないが、ここで特に目を引くのは2つ目の例だ。
こうすることで、hogeにネストされた各Listに含まれる2番目の要素を取り出すことが出来るわけだ。

ListのJoin

2つのListでJoinオペレータ(,)を挟むことで、左のListの末尾に右のListが接続された新たなListが返る。
この場合は各ListがSimple ListかGeneral Listであるかは問題にならない。

q)List1: 1 2 3
q)List2: 4 5 6
q)List1,List2
1 2 3 4 5 6

q)List1: 1 2 3
q)List2: (4 ;5 ;`HOGE)
q)List1,List2
1
2
3
4
5
`HOGE
応用的なListの操作

ここからは、vector指向言語たるqの本領を発揮するための、より発展的なListの操作方法を紹介する。

ListによるIndexing

Listのデータを参照するために用いるindex自体をListで与えることが出来る。

q)hoge: 1 2 3 4 5
q)hoge[0 1 2 3 4]
1 2 3 4 5
q)index:1 2 3
q)hoge index /当然、Listを格納した変数を用いても同じ
2 3 4

更に、List型のindexによるListの参照に対してListを代入することも出来てしまう。

q)hoge:0 1 2 3 4
q)hoge[0 1 2]: 10 20 30
q)hoge
10 20 30 3 4

一方で、List型のindexによるListの参照に対してAtomを代入すると、対象のデータ全てが1つの値で書き換わる。

q)hoge:0 1 2 3 4
q)hoge[0 1 2]: 10
q)hoge
10 10 10 3 4

上記を応用して、index自体を2次元配列にすることで、返り値に2次元配列を得ることも可能だ。

q)hoge[0 1 2 3 4]
1 2 3 4 5
q)hoge[(0 1;2 3)]
0 1
2 3

Listの探査

Find関数(?)を用いることで、任意のデータがListの要素に含まれているかを調べることが出来る。
存在する場合は対応する値のindexが返り、存在しない場合はListの配列数が返る。
また、検索対象のデータが複数存在する場合、一つ目、つまりList内で最も左にあるデータのindexだけが返るので注意。

q)hoge:10 20 30 20
q)hoge?20
1
q)hoge?111
4

Enumeration

ここで前回symbolの項で話題に出したEnumerationについて説明しておこう。
qにおけるEnumは、プロセスが扱うデータのうち、重複するデータを纏めて管理することで、データ量の削減と処理速度の向上を図るものだ。

基本的な考え方は、distinct関数を用いて説明できる。
distinct関数は、与えられたListを構成する要素から重複を除いたListを返す。

q)v:"mississippi"
q)u:distinct v
q)u
"misp"

これに先述したindexingとfind関数を組み合わせることで、以下の挙動を実現できる。

q)v:"mississippi"
q)u:distinct v
q)u
"misp"
q)k:u?v
0 1 2 2 1 2 2 1 3 3 1
q)u[k]
"mississippi"

これの何が嬉しいのかと言えば、”mississippi”をプロセスが保持する上でもはやその実体を扱う必要はなく、uとkさえ扱えばよくなった点だ。こうすることで、普段は軽量な形式でデータを扱い、必要な時のみ復号すればよくなったわけだ。

このテクニックの恩恵を受けるのは主にsymbol型なので、qのsymnoにはこれを簡略化して扱う便利な仕組みが用意されている。
[Listに登場する要素から重複を排除した集合$SymbolのList]とすることで、元のListをenumerationしてindexのListにしたデータ(上段の例におけるk)が生成される。しかも、これは表面的には元のListと同じ振舞いをする。

q)v:`m`i`s`s`i`s`s`i`p`p`i
q)u:distinct v
q)u
`m`i`s`p
q)ev:`u$v
q)ev
`u$`m`i`s`s`i`s`s`i`p`p`i /表面的にはvと同じに見えるが、その実uに対するindexの集合として保持されている‘
q)v=ev
11111111111b /表面的には元のList vと同じ振舞いをするため、=では同じに見える
q)v~ev
0b / データの実体はindexのListであるため、厳密に同じではない
q)ev?`s
2
q)where ev=`s /findやwhereも問題なく実行可能
2 3 5 6

evは索引uの値を参照しているので、データを書き換えたい際は、uを更新するだけでよい。

q)u
`m`i`s`p
q)ev
`u$`m`i`s`s`i`s`s`i`p`p`i
q)u[1]:`a
q)ev
`u$`m`a`s`s`a`s`s`a`p`p`a

しかし、既にenumeration済みのListに要素を追加する場合、やや話が複雑になる。
evに対して単純に要素を追加することはできないためだ。

q)ev
`u$`m`i`s`s`i`s`s`i`p`p`i
q)ev,:`o
'cast
  [0]  ev,:`o
         ^

エラーになる原因は、evにとっての索引であるuに`oなどという値が存在しないためだ。
従って、まず初めにuに値を追加しなければならない。

q)u
`m`i`s`p
q)ev
`u$`m`i`s`s`i`s`s`i`p`p`i
q)u,:`o
q)ev,:`o
q)ev
`u$`m`i`s`s`i`s`s`i`p`p`i`o

が、ここで別の問題が起きる。実際にListを扱う際に、追加しようとしているデータが既にuに存在するかをいちいち調べなければならないのはあまりにも面倒だ。

q)u
`m`i`s`p`o
q)count u
5
q)u?`o
4 /既に存在するので追加不要!
q)u?`a
5 / 存在しないので追加が必要!めんどくさい!

qにはこの悩みを解決する秘策が用意されている。
['対象のList?`存在しないときだけ足したい要素]とすることで、自動的に対象のListに要素が存在しないときにだけ値を追加する、という挙動を取るのだ。

q)u
`m`i`s`p`o
q)`u?`a
`u$`a
q)u
`m`i`s`p`o`a
q)`u?`i
`u$`i
q)u
`m`i`s`p`o`a

ちなみに、enumerationされたデータを元のListへ復元したい場合は、value関数が使える。

q)value ev
`m`i`s`s`i`s`s`i`p`p`i`o

*1:TODO 気が向いたらベンチマークを取る

*2:これはネタバレだが、kdbのテーブルとはListとDictionaryの集合体である