Skip to main content

· 8 min read

细心的用户会发现我们的低代码平台并没有提供 breakcontinuereturn 等常见的短路结构,且可能因此对产品的表达能力抱有疑问。本文会解释不提供这三个语言结构的动机、原因,并解释其对表达能力不构成影响。

小知识

表达能力上来说,breakcontinue 并没有从本质上增强语言的表达能力。

让我们考虑下面几个具体、典型的使用场景:

  1. 存在对列表的大量查询,希望使用 ifbreak 来提高运行速度。此种代码类似

    for (int i = 0; i < list.length; i++) {
    if (test(list[i])) {
    // 处理代码
    break;
    }
    }

    使用低代码平台的可视化逻辑编写,示意如下:

    使用 for 循环和 break

    我们可以使用 ListFind 等函数来替代三段式 for 循环,这样的代码读起来更直观,更不容易引入 bugs。示意如下: 使用 `ListFind`

    即先找到要处理的元素 res,后续再对 res(的成员)进行处理。

  2. 对列表前部元素做处理,在不满足时提前退出:

    for (int i = 0; i < list.length; i++) {
    // 对元素处理...
    // 满足条件时结束
    if (test(list[i])) {
    break;
    }
    }

    此类处理流程也可由 takeWhiledropWhile 等高阶函数解决,这些函数的第一个参数是一个条件测试,不满足条件则终止对列表的迭代。以 Java 9 的 takeWhile 举例说明:

    Stream.of("cat", "dog", "elephant", "fox", "rabbit", "duck")
    .takeWhile(n -> n.length() % 2 != 0) // 连续获取列表头部的项,直到字符串长度为偶数时停止
    .forEach(System.out::println); // 对获取到的项做打印操作

    上述代码只会打印 catdog,而不会打印从 elephant 开始的项,因为 elephant 的长度是 8 ,是偶数。

    低代码平台目前将 takeWhiledropWhile 等函数定为 P1 优先级,将在后续版本陆续提供。

  3. 跳过列表中的某些元素:

    for (int i = 0; i < list.length; i++) {
    if (test(list[i])) {
    continue;
    }
    // 对元素处理...
    }

    此类代码最简单的等价处理方法是不使用卫语句,代价是代码多一层缩进

    for (int i = 0; i < list.length; i++) {
    if (!test(list[i])) {
    // 对元素处理...
    }
    }

上面我们介绍了使用 breakcontinue 的 3 种典型场景和其替代方案。我们没有讨论复杂情形,例如在循环体的中间部分,在代码嵌套很深的地方使用 continuebreak。但我们认为这种代码其实是难以维护的,应避免写出,尤其是在低代码产品中。

return 与之非常类似,并没有从本质上提高语言的表达能力。

完整细节

“小知识”章节介绍了一些典型场景和其替代方案。本章节为熟悉编程的专业技术人员提供更多的细节说明和支持材料。

针对大量查询的场景,我们应当认识到 List 这种顺序结构本身就不支持高速查询

  • 假设列表长度为 N,则不使用 break 时的查询时间为 O(N)
  • 可假设待查询的元素在数组中的平均位置应在 1/2 处,则使用 break 的时间复杂度为 O(N/2)

O(N)O(N / 2)的复杂度在一个量级,并没有本质改善。

换一种思路,我们可以使用低代码平台提供的 Map 等高效查询结构,这样时间复杂度会降到 O(log N)O(1),得到本质改善。

数据量级 | 复杂度 \ 时间消耗O(1)O(log N)O(N)O(N/2)
10016.64410050
1 00019.9661 000500
10 000113.28810 0005 000
100 000116.61100 00050 000
1 000 000119.931 000 000500 000
10 000 000123.25310 000 0005 000 000
100 000 000126.575100 000 00050 000 000

另外,从设计理念上来说, breakcontinuereturn控制流结构是过程式、命令式的产物,而我们希望给用户带来声明式、函数式的产品体验,它会更面向数据流

我们用一个小例子来描述控制流和数据流的区别。

  • 如下的代码片段是典型的控制流结构,它声明了一个变量 x,此变量在不同条件下有不同的值。代码所做的事情便是在不同条件下用赋值语句去改变 x 的值。

    let x;
    if (flag) {
    x = 404;
    } else {
    x = 200;
    }
  • 侧重数据流的代码则会这样描述:

    let x = flag ? 404 : 200; // typescript
    let x = if flag then 404 else 200 -- Haskell

而函数式、声明式的一大特点正是面向数据流而非控制流编程。例如

  • Java 等语言在用 foreach 循环代替三段式 for 循环,而这也带来了无法使用 break 的“问题”:用户可以在 foreach 循环中使用 return 来代替 continue,但无法找到 break 的替代。业界兴起的 map、reduce 编程范式亦不支持 breakcontinue 结构。
  • 像 Haskell 这种纯函数式语言就没有提供 return 这种让函数提前返回的结构——函数体的最后代码即为要返回的值。Haskell 非常极端,它甚至没有提供循环这种控制流(使用递归代替),也就无从谈起 breakcontinue。这种极端行为的一个良性后果其实是让函数体简短、减少 bugs,易于维护。
  • Scala 语言提供了循环,但其 break 需要导入(import util.control.Breaks._)才能使用;从此设计也可看出它不与循环结构强绑定,也不被推荐使用。

(题外话:不提供 returnbreakcontinue 后,很多不可达代码也一并不存在了。)

以上便是低代码产品尚未提供 breakcontinuereturn 的原因。可总结为

  1. 削减过程式、命令式、控制流的味道,增加声明式、函数式、数据流的味道。
  2. 有同等替代用法,这些替代用法通常速度更优,或更结构化、不容易引入 bugs。