流沙河鎮

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

q言語基礎_5_関数とApplication

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

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

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, list, dictionary、本章まででq言語の根幹を成すデータ型を解説してきた。
そして本章では、これらのデータを処理する関数の作り方と振舞を説明する。

Functionの書き方

関数は以下のようにして定義する。引数及び処理が複数ある場合は;で区切る必要がある。
なお、q言語における関数の宣言は型を意識しない。実行時点で不正なデータが渡された場合に初めて'typeエラーとなる。

q){[n個の引数] m個の処理}
q){1+2} / 処理1つ(引数無し) 引数無しの場合冒頭の括弧は省略可
q){[x] x*x} / 引数1つと処理1つ
{[x] x*x}
q){[x;y] x+y} / 引数2つと処理1つ
{[x;y] x+y}
q){[x;y] a:x+y; a*a} / 引数2つと処理2つ
{[x;y] a:x+y; a*a}

基本的な使い方は以下の通り。

q){[x] x*x}[2]
4
q)hoge:{[x] x*x}
q)hoge[3]
9

複数の処理を含む関数は左から順に評価されていき、各処理はそれ以前の処理で為された変数への操作を”覚えて”いる。

q){[x] a:x+x; til a}[2]
0 1 2 3
返り値

原則として、qにおける関数の返り値は関数が最後に実行した処理の返り値となる。

q){[x] x+x}[2]
4
q)hoge:{[x] x+x}[3]
q)hoge
6

関数の最後の処理が返り値を返さなければ、その関数の返り値も"void"となる。
ちなみに、内部的には実はvoidを意味するnil(::)がリターンされているのだが、コンソール上はオミットされる。

q){[x] {}[] }[2] / 処理を持たない関数がネストされている。
q).Q.s1 {[x]{}[]}[2] /string変換することで返り値nilが見えるようになった
"::"
q)string {[x]{}[]}[2]
"::"
セミコロンによるリターンの抑止

ここで注意してほしいことがある。特別な理由がない限り、関数の最後にセミコロンを入れてはいけない。
手癖で入れてしまいがちだが、;は単なる処理の区切りであるだけではなく、直前の関数の返り値を"握り潰す"効果を持っている。
つまりは以下のようなことだ。

q){[x] x+x}[2]
4
q){[x] x+x;}[2] / 最後にセミコロンがいるので何も出ない
q)a:{[x] x+x;}[2]
q)a/ 何も入っていない

この関数の最後のセミコロン、なかなかの厄介者で、長大なプログラムの隙間にコッソリ紛れ込んで致命的なバグを引き起こす場合がしばしばある。
そういうわけで、筆者の考えでは、関数の返り値を使う必要がない場合はそもそも関数の使い手側が捨ててしまえばよいだけのことなので、基本的にはセミコロンを入れない方が安全牌である。

コロンによるリターン

実は、関数の途中に[:返り値]を仕込むことで、明示的なリターンを行うことも出来る。

q){[x;y] :x+x; y*y}[2;3] / :x+x でリターンする。この場合以降はデッドコード
4

なお、q for mortalsではコロンによるリターンを(恐らくコードを複雑にする且つ使わなくても同様のことが出来るため)「This style is discouraged.」だと警告している。

データとしてのfunction

functionはatom, list, Dictionaryに並ぶデータ型であり、type関数では100が返る。

q)hog:{[] 1+2}
q)type hog
100h
引数x, y, z

引数が3つまでの関数であれば、関数を宣言する際実は引数を省略することが出来る。
引数を省略した場合は、x, y, zが暗黙的に第一~第三引数として使用される。
本番環境で本格的に稼働するアプリケーションで使用する関数では、明示的な引数名の命名をするべきだが、ちょっと手元でスクリプトを書きたい場合などは大変に便利だ。

q){(x;y;z)}[2;3;4]
2 3 4
q){(y;x;z)}[2;3;4]
3 2 4
q){(z;y;x)}[2;3;4]
4 3 2
変数のスコープ
ローカル変数

ローカル変数の可視性とライフサイクルについてのルールは以下の通りだ。

  • ローカル変数は宣言されたブロックにおいてのみ有効であり、ブロックを抜けたら消えてしまう
  • ローカル変数はネストしたコードブロックの内部からは不可視である
  • ローカル変数はブロックの外からは不可視である(timerなどで参照する場合)
q){[]a:2}[]
2
q)a
'a
  [0]  a
q)f:{a:42; nested:{[p2] a*p2}; nested x}
q)f[2]
'a
  [2]  f@:{[p2] a*p2} /ネストされた関数内でaは不可視。fを宣言した時点ではエラーにならない。
グローバル変数

グローバル変数はローカルなブロックから参照可能であるが、通常のローカル変数と同じように値を変更することは出来ない。

q)global:"world"
q){[]global}[]
"world"
q){[]global:"local"}[]
"local"
q)global
"world"

グローバル変数を変更したい場合は、get,setを用いる。
このような具合だ。

q)get `global
"world"
q){[] `global set "local value"}[]
`global
q)get `global
"local value"
q)global
"local value"

別の方法として、::を使うことによってもグローバル変数を変更できるが、これは推奨されない。
理由は、同名のローカル変数が存在する場合、そちらが変更されてしまうからだ。じゃあどういう時に::を使うのか?それは知らない。*1

q)global:"global"
q){[] global::"local value"}[]
q)global
"local value"
q)global:"global"
q)global
"global"
q){[] global:"local" ;global::"local value"}[]
"local value"
q)global
"global"
Call-by-Name

通常、kdbにおける関数に渡す引数は値渡しである。関数が動作する時点で引数は元のデータのコピーに変換された上で処理される。
自前で関数を書く上でその仕組みを気にする必要は特にないのだが、一部のビルトイン関数は入力されるデータのサイズが大きい可能性があるため、参照渡しで呼び出す必要があることになっている。*2
参照渡しにする方法は、変数の名前をシンボルとして渡すことになる。
参照渡しで呼び出す必要があるビルトイン関数の最も代表的な例は、get,setである。

q)`hoge set 10
`hoge
q)get `hoge
10

関数の利用

引数の適用

もはや説明するまでもないだろうが、関数の引数は配列で渡すことも出来る。

q){x*x}2 3 4 5 6
4 9 16 25 36
q){x*x}(2 3 4;5 6 7)
4  9  16
25 36 49

dictionaryを渡すとこうなる。

q){x*x}`a`b`c!10 20 30
a| 100
b| 400
c| 900

関数を引数とする関数

関数への引数として渡せるのはatomやlist、dictionaryだけではない。関数自体も渡すことできる。
このテクニックを活用することで、以下のように関数の挙動をダイナミックに変えることが出来る。

q)x1:{x*x}
q)x2:{x*x*x}
q)excution:{[func;val] func[val]}
q)excution[x1;2]
4
q)excution[x2;2]
8

Projection(投影)

kdbには関数の”投影”という仕組みがある。これは、言うなれば”関数に引数の一部を渡した状態”を保持しておくテクニックである。
つまりはこういうことだ。

q)f:{x*y*z}
q)f[2;3;4]
24
q)p:f[2;3]
q)p
{x*y*z}[2;3]
q)p[4]
24
q)p:f[2;;4]
q)p
{x*y*z}[2;;4]
q)p[3]
24

また、同じ理屈で以下のような書き方も出来る。

q)f:{x*y*z}
q)f[2][3][4]
24
q)f[2]
{x*y*z}[2]
q)f[2][3]
{x*y*z}[2][3]
q)f[2][3][4]
24

先ほど紹介した関数の引数として関数を渡すテクニックと合わせて使えば、以下のように幾らか気の利いた使い方も出来る。

q)x1:{x*x}
q)x2:{x*x*x}
q)excution:{[func;val] func[val]}
q)f2:excution[x1;]
q)f2[2]
4
q)f2[3]
9

イテレータ

ところで、q言語における反復処理の実装では、一般的なプログラミング言語で用いるようなfor分やwhile文というのは用いない。
代わりに、多種多様なイテレータを用いて繰返し処理を実装することになる。
これが異常に複雑でうんざりさせられるのだが、q for mortals曰く
"Proficiency in the use of iterators is one skill that separates q pretenders from q contenders."
イテレータ利用の熟達度がエセq言語使いとq言語の求道者を見分けるポイントである。(筆者訳)
ということらしいので、まぁ気長に慣れて頂ければと思う。
qのイテレータを上手く使うコツは、q for moralsの言葉を借りれば、繰返し処理を「どうやってやるか」ではなく、「それが何であるか」を書くように意識することだ。
In functional programming, simply declare that you want to accumulate starting with an initial value of the accumulator. No counters, no tests, no loop. You say "what" to do rather than "how" to do it.
6. Functions - Q for Mortals

先ずはこれから紹介するイテレータの一覧を示す。これらのイテレータもまた、本質的にはqのビルトイン関数であり、関数と同様に振るまう。

Unary each functionをネストされたアイテムに適用する
each(') list aのi番目の項目と、list bのi番目の項目を引数として任意のbinary functionを実行する
Each Left \: 第一引数の各項目と、第二引数を引数として任意のbinary functionを実行する
Each Right /: 第一引数と、第二引数の各項目を引数として任意のbinary functionを実行する
Cross 第一引数の各項目に対して、第二引数の各項目をjoin(,)した上でrazeする
Over(/) 直前までの計算結果を第一引数、与えられたlistのn+1番目を第二引数として任意のbinary functionの結果をイテレーティブに実行して、最終結果のみを返す
Scan \ 任意のbinary functionの結果を蓄積して、過程をすべて返す
Each Prior (':) 与えられたlistのn+1番目を第一引数、n番目を第二引数として任意のbinary functionを実行する

個別に解説していこう。

Unary each

任意のfunctionを引数内でネストされたアイテムに適用する。
例えば、countを以下のように使用した場合、引数のリストが持つアイテムの個数を返すので、内部にネストされているリストのアイテムの個数を数える用途には使えない。

q)count (10 20 30;40 50)
2

そこでeachを使うことで、以下のように書くことが出来る。

q)count each (10 20 30;40 50)
3 2
q)each[count;(10 20 30;40 50)] / イテレータは本質的に関数なので、このように書くことも出来る
3 2
each(')

list aのi番目の項目と、list bのi番目の項目を引数として任意のbinary functionを実行する。
例えば、アイテムの数が同じである2つのリストの文字列を順繰りにjoinする場合以下のようになる。

q)hoge:("usada";"housyo";"iwai")
q)moge:("pekora";"marin";"katuhito")
q)
q)hoge , moge / 単純にjoinした場合
"usada"
"housyo"
"iwai"
"pekora"
"marin"
"katuhito"
q)hoge ,' moge /イテレータを使うとこうなる
"usadapekora"
"housyomarin"
"iwaikatuhito"

q)hoge: 1 2 3 / 第一引数ないし第二引数のアイテムが1つである場合は、他方の各アイテムにその値が適用される
q)moge: 2
q)hoge ,' moge
1 2
2 2
3 2
q)hoge: 1
q)moge: 2 3 4
q)hoge ,' moge
1 2
1 3
1 4
Each Left \:

第一引数の各項目と、第二引数を引数として任意のbinary functionを実行する。
例えばある文字列のリストにsuffixを付ける場合こうなる。

q)("usada";"housyo";"iwai") ,\: ">"
"usada>"
"housyo>"
"iwai>"
Each Right /:

Each Left \:の逆で、第一引数と、第二引数の各項目を引数として任意のbinary functionを実行する。
例えばある文字列のリストにprefixを付ける場合こうなる。

q)"<" ,/: ("usada";"housyo";"iwai")
"<usada"
"<housyo"
"<iwai"
q)"<" ,/: ("usada";"housyo";"iwai")  ,\: ">" / Each Left \:と合わせてこんな芸当も可能
"<usada>"
"<housyo>"
"<iwai>"
Cross

第一引数の各項目に対して、第二引数の各項目をjoin(,)した上でrazeする。
これは説明するより見たほうが早そうだ。

q)("yamada";"suzuki";"sato") cross ("kent";"taro";"zyun") / 各項目を総当たりでjoinするイメージ
"yamadakent"
"yamadataro"
"yamadazyun"
"suzukikent"
"suzukitaro"
"suzukizyun"
"satokent"
"satotaro"
"satozyun"

q)raze ("yamada";"suzuki";"sato") ,\:/: ("kent";"taro";"zyun") /←はcrossと等価
"yamadakent"
"suzukikent"
"satokent"
"yamadataro"
"suzukitaro"
"satotaro"
"yamadazyun"
"suzukizyun"
"satozyun
Over(/)

直前までの計算結果を第一引数、与えられたlistのn+1番目を第二引数として任意のbinary functionの結果をイテレーティブに実行して、最終結果のみを返す。

q)0 +/ 1 2 3 4 5 6 7 8 9 10
55
q)katei:{0N!"x:",string x; 0N!"y:",string y;x+y} / ←のような関数を噛ませることで途中何をしているかを観察できる
q)0 katei/ 1 2 3 4 5 6 7 8 9 10
q)katei:{0N!"x:",string x; 0N!"y:",string y;x+y}
q)0 katei/ 1 2 3 4 5 6 7 8 9 10
"x:0"
"y:1"
"x:1"
"y:2"
"x:3"
"y:3"
"x:6"
"y:4"
"x:10"
"y:5"
"x:15"
"y:6"
"x:21"
"y:7"
"x:28"
"y:8"
"x:36"
"y:9"
"x:45"
"y:10"
55

また、上記の書き方では最初の第一引数となる値(0)を指定しているが、以下のように省略することも可能。

q)(+/) 1 2 3 4 5 6 7 8 9 10
55
q)(katei/) 1 2 3 4 5 6 7 8 9 10
"x:1"
"y:2"
"x:3"
"y:3"
"x:6"
"y:4"
"x:10"
"y:5"
"x:15"
"y:6"
"x:21"
"y:7"
"x:28"
"y:8"
"x:36"
"y:9"
"x:45"
"y:10"
55
Scan \

任意のbinary functionの結果を蓄積して、過程をすべて返す。
Over(/)の過程を教えてくれる版と理解すればよい。

q)0+\1 2 3 4 5 6 7 8 9 10
1 3 6 10 15 21 28 36 45 55
q)(+\)1 2 3 4 5 6 7 8 9 10
1 3 6 10 15 21 28 36 45 55
Each Prior (':)

与えられたlistのn+1番目を第一引数、n番目を第二引数として任意のbinary functionを実行する。

q)katei:{0N!"x:",string x; 0N!"y:",string y;x-y}
q)100 katei': 100 99 101 102 101
"x:100"
"y:100"
"x:99"
"y:100"
"x:101"
"y:99"
"x:102"
"y:101"
"x:101"
"y:102"
0 -1 2 1 -1

Application(@, .)

関数への引数の適用、リストへのインデックスの適用、dictionalyへのkeyの適用は、ビルトインの高階関数が暗黙的に呼び出されることで処理されている。
その高階関数こそが、Applicationである。Applicationにはbasic applicationとvector applicationの2種類がある。

Basic Application(@)とは

以下に挙げる処理は、実はbasic application関数@によって実行されている。

  • 引数を1つだけ取る関数への引数適用
  • indexによるリストからの値取出し
  • dictionalyに対するkeyの適用

今まで当たり前のように使ってきた書き方には@の暗黙的な呼出が介在しており、以下はそれぞれ等価である。

q)hoge:1 2 3
q)hoge[0]
1
q)hoge@0
1
q)@[hoge;0]
1
q)moge:`a`b`c!(1 2;3 4 5; enlist 6)
q)moge
a| 1 2
b| 3 4 5
c| ,6
q)moge[`b]
3 4 5
q)moge@`b
3 4 5
q)f:{x*x}
q)f@3
9
Vector Application(.)

以下に挙げる処理は、実はvector application関数.によって実行されている。

  • 引数を複数取る関数への引数適用
  • indexing at depthによるリストからの値取出し
q)hoge
10 20 30
40 50
q)moge[`b;1] / indexing at depth
4
q)hoge[1][0] / ←はbasic applicationで取り出した配列40 50に対してbasic applicationを繰返しているだけであり全く別物なので注意
40
  • dictionaly内のネストしたアイテムの参照
q)moge
a| 1 2
b| 3 4 5
c| ,6
q)moge[`b;1]
4

今まで当たり前のように使ってきた書き方には.の暗黙的な呼出が介在しており、以下はそれぞれ等価である。

q)moge[`b;1]
4
q)moge
a| 1 2
b| 3 4 5
c| ,6
q)moge[`b;1]
4
q)moge . (`b;1)
4
q)f:{x*y*z}
q)f[2;3;4]
24
q)f . 2 3 4
24
q).[f; 2 3 4]
24
Basic Application(@)の応用①:General Apply with Unary function

以下のApplicationをGeneral Apply with Unary functionと呼ぶ。
ここで、Lは任意のデータ構造L、lはデータ構造Lから値を取出すためのindexないしkey、そしてfはkeyに対して適用する関数である。
@[L;I;f]
これを用いると、以下のような挙動になる。

q)L:10 20 30 40 50
q)@[L; 1; neg]
10 -20 30 40 50
q)d:`a`b`c!10 20 30
q)ks:`a`c
q)@[d; ks; neg]
a| -10
b| 20
c| -30

お分かり頂けただろうか?一つ目のリストの例を取ると、ここでは以下の処理が一息に行われている。
① リストLの2(index 1)番目のアイテム=20を取出す
② 取り出した20に対してビルトイン関数negを適用する=-20
③ 最初に与えたリストLのうち、①で取り出したインデックスの箇所を②で加工した値で置き換えたコピーを返す

上記の例におけるアウトプットはあくまでも元のデータのコピーであり、元のデータへの破壊的な変更は行われない。
ただし、第一引数のデータ構造の名前をシンボルで渡す、つまり所謂参照渡しを行うことで、元データそのものを変更することも可能である。
この原則は、この後に紹介する一連のApplicationにおいても同様である。

q)d:`a`b`c!10 20 30
q)d
a| 10
b| 20
c| 30
q)@[`d; ks; neg]
`d
q)d
a| -10
b| 20
c| -30
Basic Application(@)の応用②:General Apply with Binary function

以下のApplicationをGeneral Apply with Binary functionと呼ぶ。先ほど紹介した例が引数を1つだけ取るunary関数であったのに対して、こちらはbinary、2つの引数を取る関数に適用する場合というわけだ。
ここで、Lは任意のデータ構造L、lはデータ構造Lから値を取出すためのindexないしkey、gはkeyに対して適用するbinary function、vはbinary functionに適用する第二引数のAtomないしリストである。
@[L; I; g; v]
基本的な挙動はunary functionの場合と同じで、使い方は以下の通りだ。

q)L:10 20 30 40
q)@[L; 0 1; +; 100 200]
110 220 30 40
Vector Application(.)の応用①:Vector Apply with Unary function

General Applicationでは、扱うデータの対象はデータ構造の第一階層に位置するデータの集合であって、その内側には立ち入らなかった。

q)m:(10 20 30; 100 200 300; 1000 2000 3000)
q)@[m; 0 2; +; 1 2]
11   21   31
100  200  300
1002 2002 3002

そこで、@の代わりに.を使用してやることで、ネストした値にアクセスすることが出来る。
定義としては以下となる。
.[L; I; f]
挙動は以下の要領だ。

q)m:(10 20 30; 100 200 300)
q).[m; 0 1; neg]
10  -20 30
100 200 300
q)d:`a`b`c!(10 20 30; 40 50; enlist 60)
q).[d; (`a; 1); neg]
a| 10 -20 30
b| 40 50
c| ,60
Vector Application(.)の応用②:Vector Apply with binary function

もはや詳述の必要はないだろうが、全く同じ理屈でbinary functionを用いたVector Applyが可能である。
.[L; I; g; v]

q)d:`a`b`c!(10 20 30; 40 50; enlist 60)
q).[d; (`a; 1); +; 1000]
a| 10 1020 30
b| 40 50
c| ,60
q).[d; (`a; ::); +; 1000 2000 3000]
a| 1010 2020 3030
b| 40 50
c| ,60

ここまでの内容を一通り理解した皆さんは、大抵のqの実装であればリファレンスを紐解きながら読み解けるだけの知識が身に付いているはずだ。
本連載はまだまだ続くが、ここからは、各自の職場のq言語プログラムを読解き、自分でも書いてみる中で、少しずつ習熟度を高めて頂ければと思う。
え?職場にkdbがない?分かりました。あなたは自宅にkdbを組むしかないでしょう。
そんな皆さんのために、基本的な文法の解説が終わった暁には、よりアーキテクチャ寄りの、如何にしてKDBエコシステムを組み上げるかにについての記事を書くつもりだ。お楽しみに。

*1:リファレンスにも載ってない

*2:何故そんなややこしい仕組みになっているのかはよく分からない…