程序原本(八十七):系统的基础部件——分布(在结构化的思维框架下,函数拆分的可能求解)

阅读数:27 2019 年 10 月 5 日 13:34

程序原本(八十七):系统的基础部件——分布(在结构化的思维框架下,函数拆分的可能求解)

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

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

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

复制代码
// 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 映射为两个数据,并在各自的子函数中使用它们:

复制代码
// 示例 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

7 如果使用一个自动分布程序来处理函数foo(),我们显然只需要进行完整的语法扫描,以确定foo_1foo_2各自使用的那一部分数据即可。

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

复制代码
// 示例 3
function foo(x) {
var a = 100, b = '...', c = 'hello';
if (x)
{ return a += 1;
}
else {
return b = c + b;
}
}

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

复制代码
// 示例 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 使用数据全集,或其整体的单一映像。例如:

复制代码
// 示例 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 两行代码视作不存在依赖的两个子过程,进而做这样的处理:

复制代码
// 示例 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 两行代码在时间上——可以理解为在一个时间区段中——是关联的,而“循环项次的展开”只是将时间区段趋向无限小的分隔,而并没有将上述这一关联关系解构。

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

复制代码
// 示例 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()的任意一个实例,无论它仅是一个单次递归,或是分布到其他计算环境中的一个迭代区段,它都必将面临整个数据全集:

复制代码
var a = 100, b = '...', c = 'hello';

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

8 对于在分布环境下的“函数的参数界面”,在 Erlang 中可以理解为消息,而在另外一些基于数据库、数据中心或数据结点的解决方案中,可以理解为持锁的数据项或结点。

复制代码
// 示例 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()的执行可以被分布,但其“所有分布(的各个服务之间)”存在着逻辑之于数据全集的关联。在现实中,这一分布带来了逻辑向计算系统迁移的可能性,即一个大的循环过程可以分布在多个计算系统中完成,因而仍然是非常重要的大型系统下的分布解决方案。

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

评论

发布