返回介绍

15.5 在结构化的思维框架下,函数拆分的可能求解

发布于 2024-12-15 23:01:51 字数 6414 浏览 0 评论 0 收藏 0

如前所述,函数的拆分与数据的拆分有一定的关系。总的来看,若函数 A 与函数 B 之间没有时序依赖,则函数 A 与函数 B 能否拆分取决于它们所处理的数据是否能拆分或复制(映像)。

根据函数本身的结构化性质,当某个函数拆分成函数 A 与函数 B 时,必然是三种逻辑结构所映射的关系。进一步地,它们对数据拆分的需要也各有不同。

其一,顺序结构意味着函数 A 与函数 B 可以使用数据的映射(的部分或全部)。例如下面的代码:

1
2
3
4
5
6
7
8
9
  // JavaScript Syntax
  
  // 示例 1
  function foo() {
    var a = 100, b = '...', c = 'hello';
    a += 1;
    b = c + b;
    return [a, b];
  }

foo() 函数持有了 a、b、c 三个数据的全集,并且我们假设——事实上我们是特意这样构造的——函数的两个子步骤(代码 6、7 行)之间没有时序依赖。那么我们可以将 a、b、c 映射为两个数据,并在各自的子函数中使用它们:

1
2
3
4
5
6
7
8
9
10
11
12
13
  // 示例 2, 将 foo_1() 与 foo_2() 分布在不同的子系统中运算,结果 foo() 的值将与上例一致
  function foo_1() {
    var data = { a: 100 };
    return data.a + 1;
  }
  function foo_2() {
    var data = { b: '...', c: 'hello' };
    return data.c + data.b
  }
  
  function foo() {
    return [foo_1(), foo_2()]
  }

在示例 1 中使用“ var ”声明的数据总量被拆分成示例 2 中的两个对象,由于示例 1 中的两个步骤之间是顺序关系,因此它们可以分别使用示例 2 中的两个 data ,即“数据总量的映射”的部分 7

其二,分支结构(以及多重分支)类似于顺序结构,函数 A 与函数 B 可以使用数据的映射(的部分或全部),但是函数 A 与函数 B 相对于条件判断逻辑都存在“(逻辑的)时序依赖”。例如:

1
2
3
4
5
6
7
8
9
10
  // 示例 3
  function foo(x) {
    var a = 100, b = '...', c = 'hello';
    if (x) {
      return a += 1;
    }
    else {
      return b = c + b;
    }
  }

在这个 foo() 示例中,函数的两个分支之间是没有时序依赖的,但是它们都必须在x这个逻辑之后执行。由于x相对于两个分支不存在——或可以不存在——数据依赖关系,因此两个分支也可以持有各自的 data。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
  // 示例 4
  function foo_1() {
    var data = { a : 100 };
    return data.a + 1;
  }
  function foo_2() {
    var data = { b: '...', c: 'hello' };
    return data.c + data.b
  }
  
  function foo(x) {
    return x ? foo_1() : foo_2()
  }

请注意一个有趣的事实:示例 4 所使用的 foo_1()foo_2() ,与示例 2 中是完全一致的。这意味着这两个逻辑以及相关的数据,与其外在的其他逻辑无关。这体现了它们的可分布性,即可拆分与可处理。

在示例 4 中,对于 foo() 函数来讲, foo_1()foo_2() 相对于 x 这个逻辑都存在“逻辑上的”时序依赖,但它们之间以及它们之于 x 的数据,都不存在依赖。

其三,循环结构意味着函数 A 与函数 B 使用数据全集,或其整体的单一映像。例如:

1
2
3
4
5
6
7
8
9
  // 示例 5
  function foo() {
    var a = 100, b = '...', c = 'hello';
    for (var i=0; i<100; i++) {
      a += 1;
      b = c + b;
    }
    return [a, b];
  }

首先,一种错误的理解在于将 5、6 两行代码视作不存在依赖的两个子过程,进而做这样的处理:

1
2
3
4
5
6
7
8
9
10
11
12
  // 示例 6 - 不正确的逻辑
  function foo_1() {
    // 对于 a+=1 循环 100 次,返回 a
  }
  
  function foo_2() {
    // 对于 b = c + b 循环 100 次,返回 b
  }
  
  function foo() {
    return [foo_1(), foo_2()];
  }

尽管在这样的逻辑中, foo_1()foo_2() 是可以持有数据的部分或部分映像的。但这与我们在这里讨论“循环逻辑”的初衷是相背离的。

我们事实上是在讨论将“一个具有循环逻辑性质的函数”拆分为多个子函数的情况。我们的目标是找到与“循环逻辑”这一性质相关的数据处理方案,而非将循环逻辑映射为多个却无视该逻辑之于数据的关系。在上述方案中,循环之于数据的性质是没有丝毫变化的。

将“循环逻辑本身”拆分开来,其基本含义是循环项次的展开。也就是说,我们能够将 100 次循环变成两个 50 次,或者 100 个 1 次。我们讨论的是这 50 次或 1 次中的数据之于“循环项次的展开”的逻辑间的关系。然而关于这一问题的答案是简单的:每一个循环项次,都必然面临数据的全集,或其全集的映像。因为 5、6 两行代码在时间上——可以理解为在一个时间区段中——是关联的,而“循环项次的展开”只是将时间区段趋向无限小的分隔,而并没有将上述这一关联关系解构。

以函数式语言的处理为例,我们可以将上述逻辑变成一个基于函数参数界面的递归,例如:

1
2
3
4
5
6
7
8
9
10
11
  // 示例 7 - 使用递归的方案
  var a = 100, b = '...', c = 'hello';
  function foo_x(i) {
    a += 1;
    b = c + b;
    return (--i <= 0) ? [a, b] : foo_x(i);
  }
  
  function foo() {
    return foo_x(100);
  }

我们应该注意到,仅以“循环逻辑的展开”而言,函数 foo_x() 的任意一个实例都只依赖调用界面上的i值。而这个i值是一个循环过程中的中间值,或是一个传入的确值,都是与这个函数无关的。因此,任意递归函数的单一实例,对于“循环逻辑”都是透明的。

然而再观察上述的示例 7,我们发现函数 foo_x() 的任意一个实例,无论它仅是一个单次递归,或是分布到其他计算环境中的一个迭代区段,它都必将面临整个数据全集:

1
  var a = 100, b = '...', c = 'hello';

一种较好的、较可行的方案是将这个数据全集也放在函数的参数界面上 8 。例如:

1
2
3
4
5
6
7
8
9
10
  // 示例 8 - 使用递归的方案,并将数据关联在函数参数界面上
  function foo_x(i, data) {
    data.a += 1;
    data.b = data.c + data.b;
    return (--i <= 0) ? [data.a, data.b] : foo_x(i, data);
  }
  
  function foo() {
    return foo_x(100, {a: 100, b: '...', c: 'hello'});
  }

这样带来的结果是: foo_x() 的执行可以被分布,但其“所有分布(的各个服务之间)”存在着逻辑之于数据全集的关联。在现实中,这一分布带来了逻辑向计算系统迁移的可能性,即一个大的循环过程可以分布在多个计算系统中完成,因而仍然是非常重要的大型系统下的分布解决方案。

但是整个循环逻辑与其占用的时间区段的总量并没有变化。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文