轨道
/
jq
jq
/
练习
/
遥控车
遥控车

遥控车

学习练习

简介

函数

你可以在 jq 中定义自己的自定义函数,把需要的逻辑封装起来。 _函数_的行为和内置函数一样:接收一个输入,产生零个、一个或多个输出。

定义函数

你可以用下面的语法来定义 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 代码的顶部,也就是“main”表达式的前面。

嵌套函数

_函数_可以嵌套:

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
Note

之所以需要分号,是因为逗号在 jq 中已经有用途了:它是一个用来连接流的运算符。

用逗号代替分号,会试图对_只有一个实参_的 add_mul 函数发起两次调用,但这种函数并不存在,所以第一次调用就会失败:

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
    • 结果为 8
    • 于是最终结果是 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 会进行尾调用优化,但仅限零元数的函数。

模块

jq 模块是一个只包含函数的文件。 模块通过 include 或 import 命令引入到一个 jq 程序中。

说明

在这个练习里,你会玩一辆遥控车,你终于攒够钱把它买下来了。

汽车一开始的电池是满的(100%)。 每次你用遥控器驾驶这辆车,它都会行驶 20 米,并消耗 1% 的电量。 这辆车的昵称只有在创建时才会确定。

这辆遥控车有一个很酷的 LED 显示屏,可以显示两条信息:

  • 它行驶的总距离,显示为:"<METERS> meters"。
  • 电池的剩余电量,显示为:"Battery at <PERCENTAGE>%"。

如果电量降到 0%,你就不能再驾驶这辆车了,显示屏会显示 "Battery empty"。

1. 创建一辆全新的遥控车

实现 new_remote_control_car/0 函数,返回一个全新的遥控车对象:

new_remote_control_car
# => {
#      "battery_percentage": 100,
#      "distance_driven_in_meters": 0,
#      "nickname": null
#    }

2. 创建一辆带昵称的全新遥控车

实现 new_remote_control_car/1 函数,返回一个带有指定昵称的全新遥控车对象:

new_remote_control_car("Blue")
# => {
#      "battery_percentage": 100,
#      "distance_driven_in_meters": 0,
#      "nickname": "Blue"
#    }

3. 显示距离

实现 display_distance/0 函数,它接收一个汽车对象作为输入,并输出 LED 显示屏上显示的距离字符串:

new_remote_control_car | display_distance
# => "0 meters"

4. 显示电量百分比

实现 display_battery/0 函数,它接收一个汽车对象作为输入,并输出 LED 显示屏上显示的电量百分比字符串:

new_remote_control_car | display_battery
# => "Battery at 100%"

如果电量降到 0%,显示屏会显示 "Battery empty"。

5. 驾驶会改变电量和行驶距离

实现 drive/0 函数,它会:

  • 接收一个汽车对象作为输入
  • 把行驶的米数增加 20
  • 消耗 1% 的电量
  • 输出修改后的汽车对象
new_remote_control_car("Red") | drive
# => {
#      "battery_percentage": 99,
#      "distance_driven_in_meters": 20,
#      "nickname": "Red"
#    }

6. 处理电量耗尽时的驾驶

更新 drive/0 函数,使其在电量耗尽(为 0%)时既不增加行驶距离,也不减少电量:

{
  battery_percentage: 0,
  distance_driven_in_meters: 2000,
  nickname: "Red"
} | drive
# => {
#      "battery_percentage": 0,
#      "distance_driven_in_meters": 2000,
#      "nickname": "Red"
#    }
通过 GitHub 编辑 链接将在新窗口或新标签页中打开
jq Exercism

准备好开始 遥控车 了吗?

注册 Exercism,借助 12 个概念75 个练习 和真人导师指导,学习并掌握 jq,全部免费。