流沙河鎮

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

q言語基礎_1_atomの概要

# 以下は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

Atom

今回はq言語のデータ型について説明する。ふだん言語の入門書を読んでいると、大抵の場合第一章はデータ型の紹介から始まる。型の情報を列挙されても覚えられないし、退屈なので後から参照しようと読み飛ばしてしまうことが多いのだが、いざ自分がそういったものを書こうとすると、どうしても型の紹介から始めざるを得ないことに気付く。というのも、書き手は読み手が文章を始まりからおしまいまで直線的に読み下すという"幻想"に立脚して論旨を展開しがちなものであり、そうである以上まっさらな情報しか保持していない読者が順を追って与えられた情報を積み上げていくことで最終的な理解へ至るような章立てがしたい欲望に駆られる。言語においては、データ型を理解することがその先の章で学ぶあらゆる文法のベースなので、そこから始めずにはいられないというわけだ。
従って、読者諸兄にあっては本章の個別の情報を一生懸命覚える必要はなく、先ずはざっと読み飛ばして後から適宜振り返っていただければと思う。また、出来るだけ情報の羅列ではなく、Tipsやコンソールから試すことが出来るサンプルを示すようにしたので、一緒に手を動かしながら本章を楽しんで欲しい。

それでは本題に入ろう。
q言語における全てのデータ構造はAtomのデータ型から構成される。Atom文字通りqのコンテクストにおいてそれ以上ブレークダウンされないデータ形式だ。
具体的には以下がAtomである。(q for mortalsより抜粋)

Type Size CharType NumType Notation Null Value
boolean 1 b 1 1b 0b
byte 1 x 4 0x26 0x00
short 2 h 5 42h 0Nh
int 4 i 6 42i 0Ni
long 8 j 7 42j 0Nj
real 4 e 8 4.2e 0Ne
float 8 f 9 4.2 0n
char 1 c 10 "z" " "
symbol s 11 `zaphod `
timestamp 8 p 12 2015.01.01T00:00:00.000000000 0Np
month 4 m 13 2006.07m 0Nm
date 4 d 14 2006.07.21 0Nd
(datetime) 8 z 15 2006.07.21T09:13:39 0Nz
timespan 8 n 16 12:00:00.000000000 0Nn
minute 4 u 17 23:59 0Nu
second 4 v 18 23:59:59 0Nv
time 4 t 19 09:01:02.042 0Nt
enumeration     20+ `sym$`kx  
table     98 ([] c1:`a`b`c; c2:10 20 30)  
dictionary     99 `a`b`v!10 20 30  
function     100 {x}  
nil item     101 ::  

表の見方を説明しよう。char typeは型を明示して変数を代入する際に用いる記号だ。
例えば、以下の場合hogeはintである。

q)hoge : 10i
q)type hoge
-6h

ここから分かる通り、qでは[変数名:値]とすることで変数を宣言する。
typeは[type 変数名]とすることで、データ型を返す関数だ。返り値は上記表のNumType(型はshort)の値となる。
ここではint=6なので、-6hが返っている。……当然の疑問として、なぜマイナスが付いている?末尾のhって何だろう?となるだろう。
hについては簡単で、type関数の返り値がshortなので、対応するsuffixであるhが付いているわけだ。
マイナスである理由はtype関数の仕様に関係していて、これは引数がatomであれば負の値、vector(リスト)であれば正の値を返す。

q)hoge : 1 2 3i
q)type hoge
6h

vectorについては次章で詳述するが、qの世界におけるあらゆるデータはAtomvectorのどちらかに大別されるものだという点を胸に留めておいて欲しい。

型として不適なsuffixを指定した場合エラーになる。

q)a: 10.22i
'10.22

型を明示せずデータを扱った場合、qは自動で型を割当てる。

q)hoge : 10
q)type hoge
-7h
q)hoge : 10.11
q)type hoge
-9h

Null Valueとは、各型に対応するNullの値だ。
?????
何を言っているのかと言えば、q言語にはnullに型が存在するのだ。
例えばintのNull Valueは0Niであるし、dateのNull Valueは0Ndだ。
当然、型が異なるnullは等価ではない。

q)0Ni ~ 0Ni    / ~(Match)関数は2つの変数が等価であるかを比較する
1b
q)0Ni ~ 0Nd
0b
q)0Ni = 0Nd    / =(Equal)関数も同様に2つの関数を比較するが、こちらは型を気にしない。
1b

ここで、0bはbooleanのfalse, 1bはbooleanのtrueである。
値がnullであるかは、null関数を用いることによっても検証可能である。

q)null 0Ni
1b
q)hoge: 0Ni
q)null hoge
1b

nullの話をしたので、ついでに無限の話もしておこう。
qの世界では、suffix”W”を付けることで無限の値を表現することが出来る。

q)0Wi /int型の正の無限
0Wi
q)-0Wi /int型の負の無限
-0Wi

無限を示す記号にWが選ばれたのは、∞に形が似ているためらしい。(q for mortalsに書いてある)
恐るべきことに、無限の値を足し引きすることすら可能だ。

q)0Wi - 0Wi /無限 - 無限 = 0 ←分かる
q)0Wi + 1i /無限 + 1 はnull ←分からない
0Ni
q)0Wi + 2i /無限 +2 はマイナス無限 ←???
-0Wi
q)0Wi + 3i /無限 + 3はintの最小値 ←?????
-2147483646i
0i

このように、qにはオーバーフローの概念が存在せず、循環する。
実務で無限を利用する機会として最もポピュラーなのは、Timerの終了時間を無期限にしたい場合に0Wzを指定するパターンと思われるが、Timerについて今は気にする必要はない。

.d.prcl.addFunctToTimer[`runJob; (); 0Nz; 0Wz; 1000i; 1b]

symbol型

symbolはqの中でもユニークかつ重要な型である。
symbolは文字列をatomとして一塊にしたもので、バッククウォートの後に任意の文字列を指定することで宣言できる。

q)`symboldayo
`symboldayo

先ずもって重要なポイントとして、symbolはStringとは全く別のデータ型である。

q)`hoge ~ "hoge"
0b

Stringは本質的にcharの配列であり、atomではない。

q)type "c"
-10h
q)type "String" 
10h /atomではないので、正の値が返る

しかし、symbolは複数の文字列であったとしても独立したatomである。

q)type `symbol
-11h

symbolの何が嬉しいかと言えば、データサイズを大幅に削減できることだ。
例えば、次のようなテーブルがあったとしよう。突然テーブル出てきたなオイと思われるかもしれないが、今は雰囲気で分かっていればよい。

q)t:([]col1:`HOGE`MOGE`FUGA`HOGE;col2:4?10)
q)t
col1 col2
---------
HOGE 8
MOGE 1
FUGA 9
HOGE 5

ここでは、col1カラムがsymbol型だ。ここで、HOGEが2回出てきているので、これを単純にデータとして保持してしまうと、ディスク/メモリのスペースが大変無駄であることに気付く。
そこで、symbol型はcol1の中身をenumerateすることで、スペースの無駄を省くことが出来る仕組みがある。
具体的には以下のような感じでやるのだが、細かいことは今は気にしなくてもよい。

q)`:testdb/t/ set .Q.en[`:testdb] t
`:testdb/t/
q)system "ls testdb"
"sym"
,"t"
q)system "ls testdb/t" /system "任意のコマンド"とすることで、qコンソール上からOSコンソールコマンドを実行できる
"col1"
"col2"

とにかく、これでenumerationが出来た。ついでにテーブルの中身をディスクに書き出した。
一度qコンソールを開き直して、早速ロードしてみよう。

q)\l testdb/t
`t
q)t
col1 col2
---------
0    8
1    1
2    9
0    5

概ねそれらしい中身になっているが、なんだかcol1がおかしい。HOGEとかFUGAが入っていたはずなのに、謎の数字になっている。
実は、col1の中身はsymという名前のファイルに保存されている。今度はsymファイルも纏めてロードしてみよう。

q)\l testdb/
q)t
col1 col2
---------
HOGE 8
MOGE 1
FUGA 9
HOGE 5
q)sym
`HOGE`MOGE`FUGA

お分かり頂けただろうか?要するに、enumeration後のcol1の中身は、HOGEであるとかFUGAであるとかの実体ではない。
別途切り出されたsymデータのindexを示しているに過ぎないのだ。
こうすることで、今後このテーブルに何度HOGEが出てきたとしても、その実体を保存するためにデータ量を増やす必要はなく、単にsymデータのindexを代入すればよいため、データサイズを削減できるという理屈だ。
従って、symbol型は会社名であるとか、証券コードと言った、テーブルにおいて繰り返し現れると思われるデータを保持する用途に適している。逆に、間違っても株価であるとか気温のような、毎回値が変わるようなデータをsymbolで保持してはいけない。symの恩恵を受けられない上に、symファイルのサイズが肥大化して大変なことになってしまう。

データ型ごとのTips

floatとrealのあれこれ

  • floatは8バイトの浮動小数点数で、他言語で一般にdoubleと呼ばれている型に等しく、realは8バイトの浮動小数点数型で、他言語で一般にfloatと呼ばれている型に等しい。大変に紛らわしい。。
  • 指数表記が使用可能 e.g. 1.23456789e-10
  • コンソール上ではデフォルトでは小数点以下6桁までで四捨五入して表示される。\P 表示桁数とすることで、表示桁数を調整可能。

大文字小文字の区別があり、意味が全く変わるので注意。(\p nとした場合、n番ポートをListenしようとする)

q)f12:1.23456789012
q)f12
1.234568
q)\P 12
q)f12
1.23456789012

boolean

  • qではTRUE, FALSEを示す予約語は存在しない
  • booleanと他の数値型は問題なく比較可能である
q)1i + 1b
2i
q)type 1i + 1b
-6h

GUID

guid型はqセッション全体で1意なIDを格納する型で、n?0Ngとすることで取得可能。
nが正の場合qセッションごとに同じシード、負の場合ランダムシードで生成される。
主にデータベースレコードの1意なIDとして用いる。

q)1?0Ng
,ddb87915-b672-2c32-a6cf-296061671e9d
q)2?0Ng
580d8c87-e557-0db1-3a19-cb3a44d623b1 2d948578-e9d6-79a2-8207-9df7a71f0b3b

データ型の変換

キャスト

[変換したいデータ型$変換対象のデータ]とすることで型変換できる。
変換したいデータ型の記法はいくつかあるが、変換したいデータ型をシンボル表記したもの(type symbol)を用いる方法が最も分かりやすく可読性も良いので、特別な理由がなければこれを使うのがいいだろう。

q)5h$32j / type numで指定するパターン
32h
q)6h$32j
32i
q)"h"$32j / type charで指定するパターン
32h
q)"i"$32j
32i
q)`short$32j / type symbolで指定するパターン
32h
q)`int$32j
32i

Listを渡すことでまとめてキャストすることも可能だ。これはテーブルから取得したデータをまとめて変換するときなどに重宝する。(Listについては次回詳述する)

q)`int$10 20 30j
10 20 30i

例えば何らかの事情でバイナリで文字列を保持しているテーブルtについて考えてみよう。

q)t:([]col1:`HOGE`MOGE`FUGA;col2:(0x48656c6c6f;0x576f726c64;0x21212121))
q)t
col1 col2
-----------------
HOGE 0x48656c6c6f
MOGE 0x576f726c64
FUGA 0x21212121

これをq sqlでselectした場合以下のような結果になり、全然意味が分からない。

q)select col2 from t
col2
------------
0x48656c6c6f
0x576f726c64
0x21212121

そこで以下のようにしてやることで、col2の中身がまとめてキャストされ、読めるようになる。

q)select `char$col2 from t
col2
-------
"Hello"
"World"
"!!!!"

一応、キャスト先のデータ型をListで指定することも出来る。使いどころはよく分からないが。

q)`short`int`long$32
32h
32i
32

StringからSymbolへの変換

通常のキャストと同じ要領で、charないしStringからSymbolへの変換が実行できる。

q)`$"a"
`a
q)`$"titibu"
`titibu

Symbolから他のデータ型への変換

逆も同じ要領…というわけにはいかず、どういうわけだか大文字のtype charをダブルクォートで囲うことで変換する。

q)"I"$"100"
100i
q)"D"$"07/06/2020"
2020.07.06

というのも、単純に他と同じ要領でキャストしようとすると、charからのキャストと区別がつかないからだ。
charは内部的にはASCIIコードの数字に過ぎないので、単純にキャストした場合以下のような感じになる。

q)`int$"A"
65i