http://project.soft114.com/lubankit/index_chinese.html

4. 鲁班部件组合入门

Poor boy, pickin' up sticks
Build ya a house out of mortar and bricks
- Po’ Boy, Bob Dylan

到目前为止, 我们看到的鲁班代码都是顺序执行的过程代码. 在第一章里提过, 鲁班的一个重要特色是部件组合. 部件组合就是用小的鲁班部件连接组合成更大的鲁班部件. 连接组合与顺序调用不同, 连接组合是将要用的部件列举出来并定义它们之间数据依赖关系. 组合的鲁班部件执行的时候, 里面的子部件的执行顺序是由当时的数据更新的流向决定的.
鲁班部件组合与EXCEL很相似. 在EXCEL里每个格子里要不是个常数, 要不是由一个公式. 公式可以从其他格子里的值计算出当前格子的值. 所以每个公式格子都定义了自己和其他格子的关系. 如果你改变一个格子里的值, 所有直接或间接依赖于这个格子的其他格子都会被从新计算赋值. 计算顺序由依赖关系决定. 而计算的执行则由数据更新引发.
鲁班的组合部件的外部界面与鲁班过程部件没有区别. 所以它们可以混合使用. 你只需要知道部件的界面, 不需要知道它是用组合还是用过程实现的.


6.1 简单组合举例
鲁班部件组合代码格式很简单, 就是一个组合单元列表. 每个组合单元有一个名字和内容定义. 一个组合单元和其他组合单元的数据依赖关系是有它的内容定义决定的. 一个组合单元类似于一个EXCEL的格子. 下面是一个简单的组合例子.

namespace demo;

struct simplecomp ( output oneoone; )
as composition
{
A1: 100;
A2: A1+1;
output.oneoone: A2;
}

以上鲁班组合里有三个组合单元. 单元A1里是一个简单常数100, A2里是一个表达式”A1+1”. 最后一个单元的名字有点特别, 但它的意思很明确, 就是将单元A2的值放到输出属性”oneoone”里.
下面一行鲁班代码调用上面定义的鲁班部件”demo::simplecomp”.

result = demo::simplecomp(=).oneoone; // result = 101

以上代码是标准的鲁班部件类型调用, 而且在调用后取出属性”oneoone”的值赋值给变量”result”. 变量”result”的值是整数101.
为了显示组合与过程的区别, 我们可以把”demo::simplecomp”部件的代码改成以下.

namespace demo;

struct simplecomp ( output oneoone; )
as composition
{
output.oneoone: A2;
A2: A1+1;
A1: 100;
}

新的代码里组合单元的顺序是完全颠倒原来的顺序. 如果我们再调用这个部件, 结果却完全不变. 从这儿我们可以看出, 组合部件里单元的顺序完全不重要. 重要只的是单元之间的数据依赖关系.

6.2 组合单元详述
鲁班组合部件里的单元有两类, 一是定型单元, 一是不定型单元. 这一节里我们详细解说这两类单元的定义和用途.

6.2.1 不定型单元
到现在我们见过的鲁班部件组合里的单元都是不定型单元. 鲁班组合不定型单元里可以是常数, 也可以是表达式, 就象我们上面的例子一样. 不定型单元和EXCEL里的格子可以说很相似. 除了有一点, 鲁班的不定型单元里还可以放任意的鲁班脚本程序代码. 这一点是EXCEL做不到的. 下面是一个例子.

namespace demo;

struct compadhoc
(
input in1, in2;
output out;
)
as composition
{
A1: input.in1 + 1;
A2: input.in2 + 1;
A3: { std::println(obj=“run A3”); A3=A1*A2; }
A4: { std::println(obj=“run A4”); A4=A2*A2; }
output.out: A3+A4;
}

以上组合代码里的单元都是不定型单元. 单元A1和A2里都是简单表达式, 在运行时鲁班会运算表达式然后把结果放在单元里.
单元A3和A4也是不定型单元, 里面放的是鲁班脚本程序. A3和A4里的程序相似, 都是先在屏幕上印出自己运行的信息, 然后给自己运算赋值. 需要注意的是脚本程序单元和表达式单元不同之处. 表达式单元的运算结果直接赋值给单元自己. 脚本程序单元没有明确单一的运算结果, 给自己单元赋值必须由明确的赋值语句执行. 在A3单元里给自己赋值的语句是”A3=A1*A2”, 这句的意思是把单元A1的值乘以单元A2的值然后赋值给单元A3自己. 在单元A4里给自己赋值的语句是”A4=A2*A2”, 是将单元A2的平方赋值给A4自己. 值得注意的是在不定型单元的定义里, 对其他单元的引用等于建立了当前单元和被引用单元之间的数据依赖关系. 鲁班在运行这个部件时就会按这样的依赖顺序和实际的数据更新来决定那个单元会被执行.
上面代码的最后一个单元是将表达式”A3+A4”连接到输出属性”out”.

为了简单起见, 鲁班部件组合的输出属性单元, 象以上例子的”output.out”单元, 只能是表达式, 不能是脚本程序.

6.2.2 定型单元
定型单元里放的是已经定义好的鲁班部件. 用户只需要定义单元里的部件的输入和输出属性的连接. 下面是一个例子.

namespace demo;

struct TwoAdder
(
input op1, op2;
output result;
)
as process
{
output.result = input.op1+input.op2;
}

struct FourAdder
(
input op1,op2,op3,op4;
output result;
)
as composition
{
adder1: demo::TwoAdder(op1=input.op1, op2=input.op2);
adder2: demo::TwoAdder(op1=input.op3, op2=input.op4);
adder3: demo::TwoAdder(op1=adder1.result, op2=adder2.result);
output.result: adder3.result;
}

上面的例子是用两输入的加法器部件”demo::TwoAdder”来组合成四输入的加法器”demo::FourAdder”. 我们来看看其中的构造.
程序开始定义了一个简单的加法器部件”demo::TwoAdder”. “demo::TwoAdder”的功能是把两个输入属性”op1”, “op2”的值加起来放到输出属性”result”里. “demo::TwoAdder”是一个过程部件.
关键的部分是部件”demo::FourAdder”的构造. “demo::FourAdder”是一个组合部件. 里面有三个定型单元”adder1”, “adder2”, “adder3”. 三个单元放的都是上面已经定义好的”demo::TwoAdder”部件. 其中”adder1”和”adder2”的输入连接到”demo::FourAdder”的四个输入上. “adder3”的输入连接到”adder1”和”adder2”的输出. 最后将”adder3”的输出连接到”demo::FourAdder”的最终输出属性”result”上.
这个例子用三个两输入加法器组合成一个四输入加法器. 用鲁班的部件组合构造可以很简明的定义组合所用部件和它们之间的数据依赖关系.
另外值得注意的是定型单元和不定型单元的区别. 当使用不定型单元时, 其他单元引用其中包含的数据对象值本身. 而使用定型单元时, 其他单元引用其中包含的鲁班部件的输出属性.

6.3 组合部件内部执行顺序
鲁班组合部件与过程部件的一大区别是组合部件定义的是内部组合单元之间的数据依赖关系, 而不是执行顺序. 组合单元的真正执行顺序是由运行时的输入数据更新决定的. 这一节就讲述具体的细节.

还是拿上面用过的例子demo::compadhoc (稍加修改) 来讲.

namespace demo;

struct compadhoc
(
input in1, in2;
output out;
)
as composition
{
A1:{ std::println(obj=“run A1”); A1= input.in1 + 1; }
A2: { std::println(obj=“run A2”); A2= input.in2 + 1; }
A3: { std::println(obj=“run A3”); A3=A1*A1; }
A4: { std::println(obj=“run A4”); A4=A2*A2; }
output.out: A3+A4;
}

把上面代码存入文件 compadhoc.lbn 然后我们调用这个鲁班部件 demo::compadhoc . 调用的鲁班脚本代码是:

x = demo::compadhoc(in1=1, in2=2); // 第一行
std::println(obj=x.out); // 第二行
std::println(obj="change input in1"); // 第三行
x.in1=0; // 第四行
std::println(obj=x.out); // 第五行
std::println(obj="change input in2"); // 第六行
y =x(in2=1); // 第七行
std::println(obj=y.out); // 第八行


把以上代码存入一个叫 start.lbn 的文件. 然后启动执行:

Mycomputer > luban compadhoc.lbn start.lbn
run A1
run A2
run A3
run A4
13
change input in1
run A1
run A3
10
change input in2
run A2
run A4
5

以下逐行解释以上程序的运行输出.
* start.lbn的第一行是调用部件demo::compadhoc, 调用时把输入in1设置成1, in2设置成2, 最后把调用是产生的新部件对象赋值给变量x.. 调用时鲁班运行部件demo::compadhoc, 运行时打印出:
run A1
run A2
run A3
run A4

在demo::compadhoc部件内部, 数据流依赖关系是这样定义的:

input.in1 --> A1 --> A3 –----
| -->output.out
input.in2 --> A2 --> A4 ----

从上面的数据流看, A1一定在A3之前, A2一定在A4之前. 上面的程序输出”run A1 run A2run A3 run A4”是满足数据流的要求的.
另外有一点需要注意的是, 对于没有数据依赖关系的单元,它们的执行顺序关系是没有定义的. 比如上面例子里的A1和A2, A3和A4之间都没有依赖关系. 所以A1和A2哪个先执行, A3和A4哪个先执行都是没有定义的. 换句话说, 如果运行结果是”run A2 run A1 run A4 run A3”, 这也是符合鲁班语言定义的.
这个鲁班部件的功能是计算 (in1+1)x(in1+1) + (in2+1)x(in2+1). 所以之后打印的结果是 13 (in1=1, in2=2)

* 第四行是改变鲁班部件x的输入in1的值成0. 这里请读者注意, 鲁班组合部件和过程部件的最大区别就在这里. 记得以前讲述过, 如果部件的输入改变会引发部件的运行. 鲁班组合部件也是这样. 但是和鲁班过程部件不一样的是, 鲁班过程部件运行时必须运行整个过程, 而鲁班组合部件只运行那些和被改变的输入有数据依赖关系的单元. 换句话说, 鲁班组合部件只执行从被改变的输入起的数据流下游的组合单元.
在这个例子里, 输入in1的数据流下游的组合单元有A1和A3, 所以执行时打印出”run A1 run A3”. 执行后输出结果变成10, 所以在第五行代码打印输出时印出10.

* 第七行是动态调用鲁班部件对象x并把输入in2的值设置成1. 和上面讲述的相似, 动态调用时鲁班组合部件只执行从被改变的输入起的数据流下游的组合单元.
在这里, 输入in2的数据流下游的组合单元有A2和A4, 所以执行时打印出”run A2 run A4”. 执行后输出结果变成5, 所以在最后一行代码打印输出时印出5.

从以上例子可以得出以下结论:
1. 鲁班组合部件的内部单元执行顺序由数据流依赖关系决定. 彼此之间没有数据依赖关系的单元, 则没有明确定义的顺序关系.
2. 改变组合部件的输入属性或者动态调用组合部件对象, 只引发被改变的输入起的数据流下游的组合单元执行.

鲁班组合部件的根据输入属性改变来决定部分执行组合单元的特色, 可能对于很多应用环境可能很方便.


6.4 数据流定义规则
在编写鲁班组合部件时, 用户可以将表达式, 鲁班脚本程序和已定义的鲁班部件放到组合单元里. 鲁班语言会自己找出单元之间的数据流依赖关系来决定运行的单元和它们之间的顺序. 下面讲述在定义组合单元时必须遵守的规则.

6.4.1能改变自己不能改变外界
对于鲁班组合部件里的单元来说, 外面的其他单元都是只能读不能写的. 一个单元可以改变的是自己的内容. 如果一个单元试图去改变其他单元的内容, 鲁班会报错. 直接给单元赋值,调用单元的成语函数和改变单元属性都是改变单元内容的操作.不能改变外界只能改变自己的规则是为了保证鲁班组合部件里的数 据流是简明单向的.在一个单元里改变其他单元的内容会使数据流变得复杂和难以理解.下面是一个例子.

namespace demo;

struct WrongComp()
as composition
{
A1: 1;
A2: { A1=0; A2=A1+1; }
}

把以上程序存入一个文件wrongcomp.lbn里,然后用以下命令启动执行.

Mycomputer > luban wrongcomp.lbn –s “demo::WrongComp(=);”

Error when executing Luban code from stdin:
Failed to resolve external symbols for struct evaluation: Can not set cell content other than self: A1
demo::WrongComp has error and can not be used
Can not resolve external symbol demo::WrongComp

上面鲁班程序运行时显示出错误信息说,不能给除了自己以外的其他单元赋值

6.4.2 数据流不能循环
鲁班组合部件不允许在同步单元之间存在循环数据依赖关系.同步单元和异步单元的区别以后还会讲述.在这里大家只要知道,到目前为止讲述的所有单元都是同步单元就好了.下面是一个循环依赖关系的例子.

namespace demo;

struct CyclicComp()
as composition
{
A1: A2+1;
A2: A1-1;
}

上面程序里的单元A1和A2相互引用, 成为循环依赖关系. 这个鲁班组合部件是不合法的. 把以上程序存入一个文件cycliccomp.lbn里,然后用以下命令启动执行.

Mycomputer > luban cycliccomp.lbn –s “demo::CyclicComp(=);”
ERROR: Struct evaluation exception: Failed to initialize composition struct for evaluation: Invalid composition, cyclic path involving: A2 A1

鲁班报错说有循环依赖关系, 而且鲁班还列举了循环圈里的单元. 这个例子里单元A1和A2互相引用, 所以上面信息里说A2和A1组成循环.

6.5 过程与组合的选择
现在大家知道定义一个鲁班部件可以有两种方式, 一是传统的过程, 二是用组合. 组合方式很多人其实在用EXCEL Spreadsheet时就是在用组合方式编程序. 鲁班的组合部件是又向前进了一大步. 相比EXCEL Spreadsheet, 组合的鲁班部件有明确的界面可以重用和共享.
一般来说, 如果一个部件内部有复杂的控制结构, 比如循环, 比较, 跳转等, 这样的部件应该用传统的过程代码来编写. 一般来说, 这样的情况在相对较小的部件里比较常见.
如果一个部件内部可以看成一些相对独立的单元, 而在单元之间有明确的数据流依赖关系. 这样的部件就很适合用组合来编写. 一般来说, 在高层的部件里这样的组合比较常见.
还有可能是纯粹是用户个人选择. 比如说, 一个用惯了EXCEL Spreadsheet的人会发现组合方式编程很自然.

6.6 部件界面继承和异步单元
鲁班组合里的还有两个很有用的特色会在后面相关章节讲述. 其中和部件界面继有关的构造会在下一章讲述. 和多线程有关的异步单元会在讲述异步部件的第七章讲述.

本站文章仅代表作者观点,本站仅传递信息,并不表示赞同或反对.转载本站点内容时请注明来自www.linuxeden.com-Linux伊甸园。如不注明,www.linuxeden.com将根据《互联网著作权行政保护办法》追究其相应法律责任。