はじめに
Regoは一般的な手続き型プログラミング言語と言語仕様が大きく異なります。
こみいったルールを書いた際いくつか文法でハマったところがあった1ので、注意点を備忘録としてまとめました。
A != B
と not A == B
は意味が異なる
「not
とかいう演算子使いどころなくね?」とか舐めてましたすみません
A != B
は「A != B
となるA, Bが存在する」ことを表します。一方、not A == B
は「A == B
となるA, Bが存在しない」ことを表します。違いが表れるのは A
や B
がもともと存在しなかった場合です。
{
"values": [
{"a": 1, "b": 1},
{"a": 1, "b": 2},
{"a": 1}
]
}
not_equals1[n] {
obj := input.values[n]
obj.a != obj.b
}
not_equals2[n] {
obj := input.values[n]
not obj.a == obj.b
}
{
"not_equals1": [
1
],
"not_equals2": [
1,
2
]
}
input.values[2]
はそもそも b
を持っていないので、 not obj.a == obj.b
の場合にしか該当しません。
参照した要素が存在しない場合ルールに該当しなくなる
conftestでエラーメッセージを表示しようとしたときにハマりました。見たい要素だけ切り取ってエラーメッセージに出そうとすると、その要素を持っていない場合は無視されてしまいます。
例えば、先ほどの値の比較の例を考えてみます。
{
"values": [
{"a": 1, "b": 1},
{"a": 1, "b": 2},
{"a": 1}
]
}
not_equals2[msg] {
obj := input.values[n]
not obj.a == obj.b
msg := sprintf("values[%d]: a must equal to b: a: %d, b: %d", [n, obj.a, obj.b])
}
{
"not_equals2": [
"values[1]: a must equal to b: a: 1, b: 2"
]
}
input.values[2]
の場合が消えてしまいました。これは、sprintf
内で obj.b
を参照したために、このフィールドが存在しない values[2]
の場合がルールに該当しなくなってしまったためです。
sprintf
を使う場合は、可能な限り大きな範囲のプロパティのみ参照しましょう。
not_equals2[msg] {
obj := input.values[n]
not obj.a == obj.b
msg := sprintf("values[%d]: a must equal to b: obj: %v", [n, obj])
}
{
"not_equals2": [
"values[1]: a must equal to b: obj: {\"a\": 1, \"b\": 2}",
"values[2]: a must equal to b: obj: {\"a\": 1}"
]
}
また、この仕組みを逆手にとって
obj.a
のような一見意味の無さそうな式文を書くことで a
が存在する場合のみを絞り込むことも可能です。
変数は特定の値を代入しているわけではない
一般的なプログラミング言語のように、「変数nに何か特定の整数値を代入して次の行に移る」という読み方をすると面食らいます。
Regoには行の評価に順序は無く、変数は条件を満たす値の集合を表します。
そのため、「変数nは以下を満たす整数の集合である」のように捉えたほうが分かりやすいと思います。
アルゴリズムを考えず、定義だけ宣言すればよいので慣れると便利です。
# 整数nが以下を満たすとき、nは合成数であるという
composite_numbers[n] {
# ある整数n, 整数n > m > 1が存在し
# NOTE: 無限ループにならないように、範囲をinput.numbersに制限(本質ではない)
n := input.numbers[i]
m := input.numbers[j]
n > m
m > 1
# mはnの約数である
n % m == 0
}
# 整数nが以下を満たすとき、nは素数であるという
prime_numbers[n] {
n := input.numbers[i]
n != 1
not composite_numbers[n]
}
{
"numbers": [
1,
2,
...,
100
]
}
{
"prime_numbers": [
2,
3,
5,
7,
11,
13,
17,
19,
23,
29,
31,
37,
41,
43,
47,
53,
59,
61,
67,
71,
73,
79,
83,
89,
97
]
}
ルールは分割して書ける
ルールを複数書けば、OR条件としてマージされた結果が得られます。
# グロタンディーク素数
prime_numbers[57]
{
"prime_numbers": [
2,
3,
...,
53,
57,
59,
...,
97
]
}
再帰が書けない
そろそろ宣言的プログラミングの感覚がつかめたところで、「よし、Prologみたいに書けばいいんだな」と再帰を使うと撃沈します。
fact[0] := 0
fact[n] := v if {
n := input.values[_]
v := n * fact[n-1]
}
{
"values": [1,2,3,4,5]
}
1 error occurred: policy.rego:25: rego_recursion_error: rule data.play.fact[__local0__] is recursive: data.play.fact[__local0__] -> data.play.fact[__local0__]
チューリング完全性は意図的にふさがれていました2。
おわりに
以上、Regoの個人的注意点の紹介でした。RegoはOPA以外でもいくつかのツールで使用可能なので、言語仕様を把握して使いこなせるようになりたいです。