jqでは、必要なロジックをまとめるために、自分だけのカスタム関数を定義できます。_関数_は組み込み関数とまったく同じように動作し、入力を受け取って、0個、1個、あるいはそれ以上の出力を出します。
jqの_関数_は、次の構文で定義できます。
# no arguments
def funcname: expression;
# or with arguments
def funcname(args): expression;
defキーワードで始まり、jqの他の構文と同じように、読みやすさのために自由に空白を使えます。_関数_は、使う前に定義しなければなりません。次のコードはエラーになります。
def A: B(10);
def B(n): n + 1;
A
# => error: B/1 is not defined
つまり、_関数_はjqコードの先頭、「メイン」の式より前に置く必要があります。
_関数_は入れ子にできます。
def A:
def B(n): n + 1;
B(10)
;
A
# => 11
ここでは、Bの_関数_はAの本体の中でしか見えません。
_関数_は、変数と入れ子の関数のために新しいスコープを導入します。
_関数_の引数は、カンマではなく_セミコロン_で区切ります。 たとえば、数値を受け取り、それに数値を足して、さらに数値を掛ける_関数_は次のようになります。
def add_mul(adder; multiplier): (. + adder) * multiplier;
10 | add_mul(5; 4) # => 60
セミコロンが必要なのは、カンマにはすでにjqでの役割があるからです。それは、ストリームをつなげる演算子です。
セミコロンの代わりにカンマを使うと、_引数が1つ_のadd_mul関数を2回呼び出そうとします。そんな関数は存在しないので、最初の呼び出しで失敗します。
10 | add_mul(5, 4)
# error: add_mul/1 is not defined
関数の_引数_は値ではなく、フィルターです。 この意味で、これらは他の言語でコールバックと呼ばれるものと同じように動作します。
add_mul関数を例にしてみましょう。
10 | add_mul(. + 5; . - 2) # => 200
ここで何が起きているのでしょうか?
adder引数には_式_. + 5が入ります
. + adderを行うと、それは. + . + 5になります. == 10なので、これは25と評価されますmultiplier引数は_式_. - 2です
25 * 8 == 200になります_引数_を変数として「実体化」したいこともあるでしょう。
def my_func(arg):
arg as $arg
| other stuff ...
;
これには省略記法があります。
def my_func($arg):
other stuff ...
;
これは単なる「糖衣構文」にすぎないことに注意してください。$の付いていないargという名前も、_関数_のスコープ内に残っています。
_関数_にはアリティがあります。つまり、受け取る_引数_の数です。
_関数_は、同じ名前を違う_アリティ_で使うことができます。組み込みのrange関数がその例で、range/1、range/2、range/3がすべて共存しています。
これは、引数を介して状態を引き継ぐ再帰関数を定義するのに便利です。たとえば、mapは次のように実装することも_できます_。
def my_map($accumulator; func):
if length == 0
then $accumulator
else first as $elem | .[1:] | my_map($accumulator + [$elem | func]; func)
end
;
def my_map(func):
my_map([]; func)
;
[1, 2, 3, 4] | my_map(. * 10) # => [10, 20, 30, 40]
jqは末尾呼び出し最適化を行いますが、それはアリティが0の関数だけです。
jqのモジュールは、関数だけを含むファイルです。
モジュールは、includeまたはimportコマンドでjqのプログラムに取り込みます。
この演習では、ラジコンカーを動かして遊びます。このラジコンカーは、ようやくお金を貯めて買うことができたものです。
車のバッテリーは、最初は満充電(100%)です。 リモコンで車を動かすたびに、車は20メートル進み、バッテリーを1%消費します。 車のニックネームは、作成されるまで分かりません。
ラジコンカーには、2つの情報を表示する素敵なLEDディスプレイが付いています。
"<METERS> meters"のようになります。"Battery at <PERCENTAGE>%"のようになります。バッテリーが0%になると、車を動かすことはできなくなり、バッテリー表示は"Battery empty"になります。
new_remote_control_car/0関数を実装して、新品のラジコンカーのオブジェクトを返すようにしましょう。
new_remote_control_car
# => {
# "battery_percentage": 100,
# "distance_driven_in_meters": 0,
# "nickname": null
# }
new_remote_control_car/1関数を実装して、指定したニックネームを持つ新品のラジコンカーのオブジェクトを返すようにしましょう。
new_remote_control_car("Blue")
# => {
# "battery_percentage": 100,
# "distance_driven_in_meters": 0,
# "nickname": "Blue"
# }
display_distance/0関数を実装しましょう。この関数は車のオブジェクトを入力として受け取り、LEDディスプレイに表示される距離の文字列を出力します。
new_remote_control_car | display_distance
# => "0 meters"
display_battery/0関数を実装しましょう。この関数は車のオブジェクトを入力として受け取り、LEDディスプレイに表示されるバッテリー残量の文字列を出力します。
new_remote_control_car | display_battery
# => "Battery at 100%"
バッテリーが0%になると、バッテリー表示は"Battery empty"になります。
drive/0関数を実装しましょう。この関数は次の処理を行います。
new_remote_control_car("Red") | drive
# => {
# "battery_percentage": 99,
# "distance_driven_in_meters": 20,
# "nickname": "Red"
# }
drive/0関数を更新して、バッテリーが切れているとき(0%のとき)は走行距離を増やさず、バッテリーの残量も減らさないようにしましょう。
{
battery_percentage: 0,
distance_driven_in_meters: 2000,
nickname: "Red"
} | drive
# => {
# "battery_percentage": 0,
# "distance_driven_in_meters": 2000,
# "nickname": "Red"
# }