Composing Programs 2.4 Mutable Data - 05

伯克利CS 61A的教学用书笔记和一些课程笔记

这一节有点抽象,但是也很有意思。介绍了声明式编程,同时也涉及数据抽象,还有上一篇文章的内容。

2.4.12   Dispatch Dictionaries

暂时译为传播约束。

可变数据使我们能够模拟变化的系统,也使我们能够构建新的抽象类型。 这次,我们结合了nonlocal,list和dictionary来构建基于约束的系统(constraint-based system),支持多个方向的计算。 将程序表示为约束是声明式编程(declarative programming的一种,其中程序员声明要解决的问题的结构,但是抽象出精确地解决问题的方法的详细信息。

一般计算机程序被构建为单向计算的,它对预先指定的参数执行操作以产生期望的输出,就像函数一样,输入数据,进行加工,输出想要的数据。而另一方面,我们会经常希望用数量之间的关系来进行建模。比如,我们考虑理想气体定律,该定律通过玻尔兹曼常数k(Boltzmann's constant (k))将理想气体的压力p,体积v,数量n和温度t(pressure (p), volume (v), quantity (n), and temperature (t))关联起来:

p * v = n * k * t

这样的方程不是一个方向(is not one-directional)计算的,给定任意四个参数,我们能计算第五个参数,如果将这个方程翻译成传统方式构建的计算机语言,我们只能选择一个来计算,比如计算压力的函数不能计算温度,即使这两个量来自同一个方程。

在本节中,我们将概述线性关系的一般模型设计。我们将会定义存在于数量之间的基本约束,比如一个加法器adder(a, b, c),它强制执行数学关系a + b = c。

我们还将定义一种组合的方法,这样,类似上述的基本约束(primitive constraints)就可以组合起来,表达更复杂的关系。这样,我们的程序变得类似于一种编程语言。

我们通过构建一个由连接器(connectors)连接约束的网络来组合约束。连接器是"保存"值的对象,可以参与一个或者多个约束。

比如,我们知道华氏温度和摄氏温度(Fahrenheit and Celsius temperatures)的关系:

9 * c = 5 * (f - 32)

这个方程式c和f的一个复合约束,这种约束可以看作是一个由基本的加法器、乘法器和常数约束组成的网络(Such a constraint can be thought of as a network consisting of primitive adder, multiplier, and constant constraints.)。

在上图中,我们看到左边的框有三个端口,标记为a,b和c。a端口连接了一个连接器Celsius,它保存着摄氏温度。b端口连接着一个连接器w,它保存着数字9。c端口,产生由乘法器约束得到的乘积,并连接到另一个乘法器的c端口。

这个网络是这样进行计算的:当给连接器一个值(由用户或者连接到它的约束框)时,它激活所有和它连接的约束(除了之前被激活的之外),并告诉它们有值产生。然后,每个被激活的约束框轮询它的连接器,看看是否有足够的信息来确定连接器的值,如果是,则该框将设置该连接器,然后该连接器唤醒其他相关的约束,以此类推。

比如,在摄氏温度与华氏温度进行转换时,常数框立即将w,x和y设置为9,5和32。连接器将激活乘法器和加法器,但是这时它们没有足够的信息确定框的值,无法继续进行,直到用户(或者网络的其他部分)将比如Celsius设置为一个值(比如25),则将激活左边的乘法器(乘法框),因为这个乘法器能够确定值,并从c输出,整个网络继续传播,最后得到Fahrenheit为77。

Using the Constraint System

让我们来用一下约束系统。为了实现上面的系统,我们首先定义两个连接器Celsius和Fahrenheit,我们用构造器(constructor)connector来产生它们。假设构造器connector已经被实现。

>>> celsius = connector('Celsius')
>>> fahrenheit = connector('Fahrenheit')

函数converter将不同的连接器和约束集成在一起构成网络。假设加法器和乘法器以及常数约束已经实现。

>>> def converter(c, f):
        """Connect c to f with constraints to convert from Celsius to Fahrenheit."""
        u, v, w, x, y = [connector() for _ in range(5)]
        multiplier(c, w, u)
        multiplier(v, x, u)
        adder(v, y, f)
        constant(w, 9)
        constant(x, 5)
        constant(y, 32)
>>> converter(celsius, fahrenheit)

我们用消息传递(message passing)来协调约束和连接器。关于message passing可以看这篇文章Composing Programs 2.4 Mutable Data - 04

约束是本身不保存局部状态的字典。它们对消息的响应是非纯函数,这些函数更改它们所约束的连接器。连接器是保存当前值并响应操纵该值的消息(message)的字典。约束不会直接更改连接器的值,而是会通过发送消息来进行更改,以便连接器可以响应更改而通知其他约束。 这样,连接器代表数字,但也封装了连接器的行为。(Connectors are dictionaries that hold a current value and respond to messages that manipulate that value. Constraints will not change the value of connectors directly, but instead will do so by sending messages, so that the connector can notify other constraints in response to the change. In this way, a connector represents a number, but also encapsulates connector behavior.)

比如,我们可以发送一条消息给连接器,以设置它的值。这里,我们('user')设置Celsius的值为25。

>>> celsius['set_val']('user', 25)
Celsius = 25
Fahrenheit = 77.0

然后Celsius的值在网络中传播(propagates through the network),最后Fahrenheit的值也改变了。之所以这两个连接器的名称会被这样对应打印出来,是因为我们用用构造器构造它们的时候就是这样命名的。

现在我们试着将Fahrenheit的值设置为212。

>>> fahrenheit['set_val']('user', 212)
Contradiction detected: 77.0 vs 212

这个连接器报出了一条信息,表示产生了矛盾:它的值是77.0,但是有人想要设置为212。如果我们真的想要修改网络中的值,我们可以告诉Celsius忘记(forget)它的旧值。

>>> celsius['forget']('user')
Celsius is forgotten
Fahrenheit is forgotten

连接器Celsius发现'user'想要撤销先前的值,所以Celsius对旧值进行'forget',并在整个网络中传播,同时更改了其他部分,最后Fahrenheit也'forget'了旧值77.0。

现在Fahrenheit没有绑定值,所以我们可以赋新值,并且在网络中传播,也得到对应的Celsius的新值。

>>> fahrenheit['set_val']('user', 212)
Fahrenheit = 212
Celsius = 100.0

现在我们又通过Fahrenheit的值得到了Celsius的值,实现了多方向的计算。这种非方向性计算是基于约束的系统的显著特征(This non-directionality of computation is the distinguishing feature of constraint-based systems.)。

Implementing the Constraint System

正如我们看到的,连接器(connectors)是将消息名称映射到函数和数据值的字典。下面我们来实现响应以下消息(message)的连接器:

  • connector['set_val'](source, value) 表示 source 正在请求连接器 connector 将当前值设置为 value.
  • connector['has_val']() 返回连接器是否已经有一个值.
  • connector['val'] 是连接器当前的值.
  • connector['forget'](source) 告诉连接器 source 请求清除(forget)它的值.
  • connector['connect'](source) 告诉连接器产生source的一个新的约束.

约束(Constraints)也是字典,它通过两个消息(message)从连接器接收信息(information):

  • constraint['new_val']() 表示某个连接到约束的连接器有新值.
  • constraint['forget']() 表示某个连接到约束的连接器忘记它的值.

当约束接收到这些消息,它们适当地传播到其他连接器。

adder函数对三个连接器构造一个加法器约束,前两个必须加起来等于第三个:a + b = c。为了支持多向约束传播,加法器还必须指定从c减去a得到b,同样从c减去b得到a。

>>> from operator import add, sub
>>> def adder(a, b, c):
        """The constraint that a + b = c."""
        return make_ternary_constraint(a, b, c, add, sub, sub)

这里,我们想要实现一个通用地三元约束,它使用来自adder的三个连接器,再加上三个函数来创建一个接收 'new_val' 和 'forget' 的约束。对消息(message)的响应是局部函数,它们被放在一个命名为约束(constraint)的字典中。

>>> def make_ternary_constraint(a, b, c, ab, ca, cb):
        """The constraint that ab(a,b)=c and ca(c,a)=b and cb(c,b) = a."""
        def new_value():
            av, bv, cv = [connector['has_val']() for connector in (a, b, c)]
            if av and bv:
                c['set_val'](constraint, ab(a['val'], b['val']))
            elif av and cv:
                b['set_val'](constraint, ca(c['val'], a['val']))
            elif bv and cv:
                a['set_val'](constraint, cb(c['val'], b['val']))
        def forget_value():
            for connector in (a, b, c):
                connector['forget'](constraint)
        constraint = {'new_val': new_value, 'forget': forget_value}
        for connector in (a, b, c):
            connector['connect'](constraint)
        return constraint

constraint是一个调度字典(dispatch dictionary),它的每个键的值都是一个函数名,指向一个局部函数。它响应约束收到的两个消息(message),但也在调用其连接器的时候作为source参数传递。

给约束传递'new_val'就会调用new_value函数,这个函数会先检查abc三个连接器是否已经有值,如果有 if av and bv 就会判断为真,那么就会将c的值设置为函数ab的返回值,对于加法器adder,函数ab就是add函数。

约束将自己作为source参数传递给连接器。如果a和b都没有值,那么约束将检查a和c,以此类推。

如果这个约束的某一个连接器需要 'forget',则约束会将它的所有连接器都设置为 'forget' 。

multiplier 和 adder 非常相似。

>>> from operator import mul, truediv
>>> def multiplier(a, b, c):
        """The constraint that a * b = c."""
        return make_ternary_constraint(a, b, c, mul, truediv, truediv)

constant也是一个约束,但它不发送任何消息(message),因为它只涉及构造时的单个连接器。

>>> def constant(connector, value):
        """The constraint that connector = value."""
        constraint = {}
        connector['set_val'](constraint, value)
        return constraint

这三个约束条件足以实现我们的温度转换网络。

Representing connectors

一个连接器(connector)代表一个包含值的字典,但是也有具有局部状态的响应函数,连接器必须跟踪为其提供当前值的informant,以及它参与的约束list。

>>> def connector(name=None):
        """A connector between constraints."""
        informant = None
        constraints = []
        def set_value(source, value):
            nonlocal informant
            val = connector['val']
            if val is None:
                informant, connector['val'] = source, value
                if name is not None:
                    print(name, '=', value)
                inform_all_except(source, 'new_val', constraints)
            else:
                if val != value:
                    print('Contradiction detected:', val, 'vs', value)
        def forget_value(source):
            nonlocal informant
            if informant == source:
                informant, connector['val'] = None, None
                if name is not None:
                    print(name, 'is forgotten')
                inform_all_except(source, 'forget', constraints)
        connector = {'val': None,
                     'set_val': set_value,
                     'forget': forget_value,
                     'has_val': lambda: connector['val'] is not None,
                     'connect': lambda source: constraints.append(source)}
        return connector

connector也是个调度字典(dispatch dictionary),它的键就是上面定义的五个消息(message),用于与约束通信。四个消息是函数,一个是connector的值。

当需要给连接器设置值时,set_value就会被调用,如果连接器当前值是None,就会将informant记录为传入连接器的source,并将连接器的值设置为value。然后,连接器将会将这个新值传播到所有其他和它连接在一起的约束(constraints)。这是使用以下迭代函数实现的。

>>> def inform_all_except(source, message, constraints):
        """Inform all constraints of the message, except source."""
        for c in constraints:
            if c != source:
                c[message]()

如果要求连接器忘记(forget)其值,它就会调用局部函数forget_value,函数首先确保请求这source和最初设置值的是同一个约束,如果是连接器将informant和值都设置为None。

我们所设计的约束程序引入了很多思想,这些思想将在面向对象编程中再次出现,约束和连接器都是通过消息进行操作的抽象。

我们将在本章的后面部分使用一个类似字典的体系结构,包含字符串键值和函数值来实现一个面向对象的系统。

 

下一篇正式进入面向对象编程。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值