文章
Get Programming with Haskell-10
函数式面向对象编程
# 总结篇:用机器人实现函数式面向对象编程
## 1.带有一个属性的对象:一杯咖啡
我们先从建模一个**简单的咖啡杯(cup)**开始。你将把本节的所有代码保存在一个名为 `cup.hs` 的文件中。在这个例子中,杯子只有一个最基本的属性:**当前杯中液体的盎司数(ounces)**。你需要一种方式来存储这个数值,以便之后可以访问。这个存储方式就相当于你的基本“对象”。幸运的是,在第前面章节中,你已经学习了一个非常有用的工具,可以在函数中捕获值:**闭包(closure)**!你将定义一个 `cup` 函数,它接收杯子中的液体盎司数,并返回一个**保存该值的闭包**:
```haskell
cup f10z = \_ -> f10z
```
由于你拥有**一等函数(first-class functions)**,因此可以像处理数据一样操作存储在闭包中的值。现在,你可以像传递对象一样传递这些存储的信息。但显然,仅能存储"盎司"这一数值还不够——你需要能够对这个杯子内部的数值**施加某种操作(apply a message)**。此时,你将通过一等函数向这个"对象"传递一条**消息(message)**,而这条消息能对其内部属性执行特定行为。
需要注意的是,这里采用的模式与常见的**方法调用(calling methods)**有所不同。传统的方法调用遵循**"对象→动作"**的模式,

而你的做法将反转这一模式:改为向对象发送消息。

这种相对少见的模式在**Common Lisp**对象系统(CLOS)和R语言的S3对象系统中均有应用。
**函数式对象(function-based objects)**的核心思想:通过闭包存储状态,再通过高阶函数实现消息传递,从而替代传统面向对象中的方法调用范式。
### 1.创建构造器
创建对象实例最常见的方式是使用一种称为**构造函数(constructor)**的特殊方法。要为你的对象实现构造函数,唯一需要做的就是**提供一种向对象发送消息的途径**。只需在闭包中添加一个**命名参数(named argument)**,即可实现消息传递机制。
```haskell cup flOz = \message -> message flOz ``` cup对象的构造器 现在你已经拥有了一个能够创建对象实例的基础构造函数。值得注意的是,你仅通过**lambda函数**、**闭包**和**一等函数**就实现了这一功能!在GHCi交互环境中,你可以这样创建杯子对象的实例: ```haskell GHCi> aCup = cup 6 ``` 你也可以在`cup.hs`文件中定义一个12盎司的咖啡杯对象。 ```haskell coffeeCup = cup 12 ``` ### 2.为你的对象添加访问器(对象的成员函数) 你已将数值存储在对象中,但还需要赋予这个对象实用的功能。接下来,你将创建简单的消息机制来获取和修改对象内部的值。首先,你需要能够获取杯中当前的咖啡量。为此,你将创建一个名为**`getOz`**的消息——该消息接收一个杯子对象作为参数,并返回其包含的液体盎司数(flOz)。 ```haskell getOz aCup = aCup (\flOz -> flOz) -- 添加"成员函数" ``` getOz message ```haskell ghci> getOz coffeeCup 12 ``` 接下来,我们要实现一个稍复杂的功能:对杯子最实用的操作当然是——喝掉里面的饮料!这个操作本质上会改变对象的状态。但在Haskell中该如何实现呢?其实很简单:你将在后台创建一个新对象。当传递设置液体盎司值的消息时,该操作需要返回一个新的对象实例,并确保其内部属性已正确更新。 ```haskell drink aCup ozDrank = cup (f10z - ozDrank) -- drink message 更新cup的状态 where f10z = getOz aCup -- 获取对象原先的状态 ``` 现在可以再ghci中嘬一口咖啡:  当前实现存在一个小缺陷:饮用量的计算可能超出杯子实际容量。具体表现为——**`drink`**消息允许杯子出现负值。我们可以重写这个方法,确保杯中的咖啡量最小值始终为0。
```haskell
```haskell cup flOz = \message -> message flOz ``` cup对象的构造器 现在你已经拥有了一个能够创建对象实例的基础构造函数。值得注意的是,你仅通过**lambda函数**、**闭包**和**一等函数**就实现了这一功能!在GHCi交互环境中,你可以这样创建杯子对象的实例: ```haskell GHCi> aCup = cup 6 ``` 你也可以在`cup.hs`文件中定义一个12盎司的咖啡杯对象。 ```haskell coffeeCup = cup 12 ``` ### 2.为你的对象添加访问器(对象的成员函数) 你已将数值存储在对象中,但还需要赋予这个对象实用的功能。接下来,你将创建简单的消息机制来获取和修改对象内部的值。首先,你需要能够获取杯中当前的咖啡量。为此,你将创建一个名为**`getOz`**的消息——该消息接收一个杯子对象作为参数,并返回其包含的液体盎司数(flOz)。 ```haskell getOz aCup = aCup (\flOz -> flOz) -- 添加"成员函数" ``` getOz message ```haskell ghci> getOz coffeeCup 12 ``` 接下来,我们要实现一个稍复杂的功能:对杯子最实用的操作当然是——喝掉里面的饮料!这个操作本质上会改变对象的状态。但在Haskell中该如何实现呢?其实很简单:你将在后台创建一个新对象。当传递设置液体盎司值的消息时,该操作需要返回一个新的对象实例,并确保其内部属性已正确更新。 ```haskell drink aCup ozDrank = cup (f10z - ozDrank) -- drink message 更新cup的状态 where f10z = getOz aCup -- 获取对象原先的状态 ``` 现在可以再ghci中嘬一口咖啡:  当前实现存在一个小缺陷:饮用量的计算可能超出杯子实际容量。具体表现为——**`drink`**消息允许杯子出现负值。我们可以重写这个方法,确保杯中的咖啡量最小值始终为0。
```haskell
drink aCup ozDrank = if ozDiff >= 0 -- 杯中的饮品容量最小为0,不能无限喝 then cup ozDiff else cup 0 where flOz = getOz aCup ozDiff = flOz - ozDrank

可以添加一个辅助函数检查cup是否为空
```haskell
isEmpty aCup = getOz aCup == 0
由于需要持续追踪对象状态,频繁执行饮用操作可能导致代码变得冗长。幸运的是,**foldl函数可以完美解决这个问题——我们在第9课讨论过,这个高阶函数接收三个参数:处理函数、初始值和列表,最终将其归约为单个值。以下是通过foldl**实现五次连续饮用的代码示例(部分实现):
afterManySips = foldl drink coffeeCup [1,1,1,1,1]
#
2.让我们构建一个更复杂的对象:战斗机器人!#
至此,我们已经完成了对象的基础建模:通过构造函数封装对象信息,再借助访问器与之交互。掌握了这些对象表示的基本方法后,现在让我们来构建更有趣的内容——组装战斗机器人! 每个机器人将包含以下基本属性:
- 名称
- 攻击力
- 生命值 为了更优雅地处理这三个属性,相比直接向闭包传入三个易造成混乱的独立参数,我们将采用元组来封装机器人属性。例如:("Bob",10,100) 表示一个名为Bob的机器人,其攻击力为10,生命值为100。 此时的消息传递机制也将升级:不再是向单一值发送消息,而是面向这组属性集合进行通信。注意,我们将通过对元组参数的模式匹配,使这些值更易于读取和理解。
-- robot的构造函数,有3个属性
robot (name, attack, hp) = \message -> message (name, attack, hp)
所有对象本质上都是可接收消息的属性集合。在下一单元中,我们将探讨Haskell的类型系统——它能提供更强大的数据抽象方法。但即便如此,元组作为最简数据结构的核心理念仍将延续。 创建机器人实例的示例如下:
killerRobot = robot ("Kill3r",25,200)
要使该对象具备实用性,我们需要添加若干访问器来便捷操作这些值。首先创建一个辅助函数,通过命名方式访问元组的各个部分(其原理与处理二元组的fst和snd函数相同)。
-- 添加获取对象属性的辅助函数
name (n, _, _) = n -- 通过模式匹配获取我们需要的值
attack (_, a, _) = a
hp (_, _, hp) = hp
接下来,添加我们的get,set访问器函数和”toString”函数打印我们的robot状态
-- 添加get访问器函数
getName aRobot = aRobot name
getAttack aRobot = aRobot attack
getHP aRobot = aRobot hp
-- 添加set访问器函数
setName aRobot newName = aRobot (\(n,a,h) -> robot (newName, a, h))
setAttack aRobot newAttack = aRobot (\(n,a,h) -> robot (n, newAttack, h))
setHP aRobot newHP = aRobot (\(n,a,h) -> robot (n, a, newHP))
-- 添加打印状态的函数(类比 Java中的toString)
printRobot aRobot = aRobot (\(n, a, h) -> n ++ " attack: " ++ (show a) ++ " hp:" ++ (show h))