返回文章列表

文章

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)**有所不同。传统的方法调用遵循**"对象→动作"**的模式, ![](https://prod-files-secure.s3.us-west-2.amazonaws.com/4b1b167f-4b52-4031-8233-f4f2689af574/1cc284f1-5be1-49a9-8e0d-86cb02a573db/image.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=ASIAZI2LB4664ZBW4GBG%2F20260827%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20260827T181111Z&X-Amz-Expires=3600&X-Amz-Security-Token=IQoJb3JpZ2luX2VjEHEaCXVzLXdlc3QtMiJFMEMCHyo186JR1R4lo%2Fd3xfrFI%2BRhRH3vJotvhcypOWgu8W4CIGrU7Fl7hIW0Ut0Lgpp3nev2qCXvZ4CcxIxjCoIfdjJeKv8DCDoQABoMNjM3NDIzMTgzODA1Igx0P00ZJQZDCM9S3bkq3AM8tBvi2EhKkzGZHqQPO2ErF%2F2noa8mj2HNaBEaMPPacvFKt%2FWksv9cVKeHJgU7vhirDHDLvEk%2BFaZZCEnBWV6MljVHFhdwn4VPxnkcEghRGSXMLEhKOecitKZct0ZIZ9g2omWXmXJ3mfb%2Bvju6%2B33MDVo3ZL1ZuxckxvYadds4k4B90ZbPNynqqohKcIXTVehBZTJFlflymcVmH80OX404y6SuVGjt5XAz2dR442W1MYUBO2celNwB6KNY%2FXfJxvYyLqNHgekXWgHtAnniap61VGDBzE3HiSYAvqc0s8rlN6mxgDnk7%2FeXZSZ9hqsKEsmz1MAwvnGkvLOYdLLZQC4OVCTWarMwVfYwyQnSQdeNnb2g0yQsrjPeZW%2FeRlb6vGJncDW1wGIzR2%2FhW3cb6wB6wcljmbf40zG4I2YnUZOhwVFgqZYCw%2FLpUyzguQbIF9kljHpT1Rab3XVcPG%2FB1HOzwrZngGgSlMxSinsOOwA7%2BKJXRpfaTWtTWDlHFzthCXpefGMqtN78TLhl7M21wp9Ct5ndufyGKA%2FW2KFKQP8uZLg6S1iTkDkLuX2yLoclJRm6sd5px03LXcB5gDDdMdLlPFnqBT8dghsXfr65Riubp54sQjDuU7eYbhCRNTDj0MHUBjqnAYba6aLxGfZwsOANjDHu73cnOssB6FoiWojyHdGn5gSYHiklk4K6puJDS%2FgKz%2BjEBkZCOaa6Qipiy0IMiOnpXpiqKIg5N%2FTNN7vc1n%2BZekvG4ApSPXNeWoE6Q2sESI9CnagtVNVBGiv0mujJqc0hAmbov%2FPWNbmLGv1J2S04Rr9k4t97c7dEvYyQ4AxTrKtotjoNmbB%2FtxZj8FW9EmmUld3ennL0Z8xk&X-Amz-Signature=a78a57db8e2119a2689ba9c78c617b340f0efa6578a76ac1ae2f7db1ae09cf25&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject) 而你的做法将反转这一模式:改为向对象发送消息。 ![](https://prod-files-secure.s3.us-west-2.amazonaws.com/4b1b167f-4b52-4031-8233-f4f2689af574/37a8353b-0575-42c0-9e40-5f8e13653281/image.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=ASIAZI2LB4664ZBW4GBG%2F20260827%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20260827T181111Z&X-Amz-Expires=3600&X-Amz-Security-Token=IQoJb3JpZ2luX2VjEHEaCXVzLXdlc3QtMiJFMEMCHyo186JR1R4lo%2Fd3xfrFI%2BRhRH3vJotvhcypOWgu8W4CIGrU7Fl7hIW0Ut0Lgpp3nev2qCXvZ4CcxIxjCoIfdjJeKv8DCDoQABoMNjM3NDIzMTgzODA1Igx0P00ZJQZDCM9S3bkq3AM8tBvi2EhKkzGZHqQPO2ErF%2F2noa8mj2HNaBEaMPPacvFKt%2FWksv9cVKeHJgU7vhirDHDLvEk%2BFaZZCEnBWV6MljVHFhdwn4VPxnkcEghRGSXMLEhKOecitKZct0ZIZ9g2omWXmXJ3mfb%2Bvju6%2B33MDVo3ZL1ZuxckxvYadds4k4B90ZbPNynqqohKcIXTVehBZTJFlflymcVmH80OX404y6SuVGjt5XAz2dR442W1MYUBO2celNwB6KNY%2FXfJxvYyLqNHgekXWgHtAnniap61VGDBzE3HiSYAvqc0s8rlN6mxgDnk7%2FeXZSZ9hqsKEsmz1MAwvnGkvLOYdLLZQC4OVCTWarMwVfYwyQnSQdeNnb2g0yQsrjPeZW%2FeRlb6vGJncDW1wGIzR2%2FhW3cb6wB6wcljmbf40zG4I2YnUZOhwVFgqZYCw%2FLpUyzguQbIF9kljHpT1Rab3XVcPG%2FB1HOzwrZngGgSlMxSinsOOwA7%2BKJXRpfaTWtTWDlHFzthCXpefGMqtN78TLhl7M21wp9Ct5ndufyGKA%2FW2KFKQP8uZLg6S1iTkDkLuX2yLoclJRm6sd5px03LXcB5gDDdMdLlPFnqBT8dghsXfr65Riubp54sQjDuU7eYbhCRNTDj0MHUBjqnAYba6aLxGfZwsOANjDHu73cnOssB6FoiWojyHdGn5gSYHiklk4K6puJDS%2FgKz%2BjEBkZCOaa6Qipiy0IMiOnpXpiqKIg5N%2FTNN7vc1n%2BZekvG4ApSPXNeWoE6Q2sESI9CnagtVNVBGiv0mujJqc0hAmbov%2FPWNbmLGv1J2S04Rr9k4t97c7dEvYyQ4AxTrKtotjoNmbB%2FtxZj8FW9EmmUld3ennL0Z8xk&X-Amz-Signature=5c151c5782bb4bf9efc5c97dbd322181ddda71b1b9ff1f610f990549dc0cfdc5&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject) 这种相对少见的模式在**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中嘬一口咖啡: ![](https://prod-files-secure.s3.us-west-2.amazonaws.com/4b1b167f-4b52-4031-8233-f4f2689af574/4238d846-f0db-47f1-b428-7a76d2adfe34/image.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=ASIAZI2LB4664ZBW4GBG%2F20260827%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20260827T181111Z&X-Amz-Expires=3600&X-Amz-Security-Token=IQoJb3JpZ2luX2VjEHEaCXVzLXdlc3QtMiJFMEMCHyo186JR1R4lo%2Fd3xfrFI%2BRhRH3vJotvhcypOWgu8W4CIGrU7Fl7hIW0Ut0Lgpp3nev2qCXvZ4CcxIxjCoIfdjJeKv8DCDoQABoMNjM3NDIzMTgzODA1Igx0P00ZJQZDCM9S3bkq3AM8tBvi2EhKkzGZHqQPO2ErF%2F2noa8mj2HNaBEaMPPacvFKt%2FWksv9cVKeHJgU7vhirDHDLvEk%2BFaZZCEnBWV6MljVHFhdwn4VPxnkcEghRGSXMLEhKOecitKZct0ZIZ9g2omWXmXJ3mfb%2Bvju6%2B33MDVo3ZL1ZuxckxvYadds4k4B90ZbPNynqqohKcIXTVehBZTJFlflymcVmH80OX404y6SuVGjt5XAz2dR442W1MYUBO2celNwB6KNY%2FXfJxvYyLqNHgekXWgHtAnniap61VGDBzE3HiSYAvqc0s8rlN6mxgDnk7%2FeXZSZ9hqsKEsmz1MAwvnGkvLOYdLLZQC4OVCTWarMwVfYwyQnSQdeNnb2g0yQsrjPeZW%2FeRlb6vGJncDW1wGIzR2%2FhW3cb6wB6wcljmbf40zG4I2YnUZOhwVFgqZYCw%2FLpUyzguQbIF9kljHpT1Rab3XVcPG%2FB1HOzwrZngGgSlMxSinsOOwA7%2BKJXRpfaTWtTWDlHFzthCXpefGMqtN78TLhl7M21wp9Ct5ndufyGKA%2FW2KFKQP8uZLg6S1iTkDkLuX2yLoclJRm6sd5px03LXcB5gDDdMdLlPFnqBT8dghsXfr65Riubp54sQjDuU7eYbhCRNTDj0MHUBjqnAYba6aLxGfZwsOANjDHu73cnOssB6FoiWojyHdGn5gSYHiklk4K6puJDS%2FgKz%2BjEBkZCOaa6Qipiy0IMiOnpXpiqKIg5N%2FTNN7vc1n%2BZekvG4ApSPXNeWoE6Q2sESI9CnagtVNVBGiv0mujJqc0hAmbov%2FPWNbmLGv1J2S04Rr9k4t97c7dEvYyQ4AxTrKtotjoNmbB%2FtxZj8FW9EmmUld3ennL0Z8xk&X-Amz-Signature=a9d2e5015161b1d08166558eb40d9303f4fd721304adcc3e47beabd15e7e0aac&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject) 当前实现存在一个小缺陷:饮用量的计算可能超出杯子实际容量。具体表现为——**`drink`**消息允许杯子出现负值。我们可以重写这个方法,确保杯中的咖啡量最小值始终为0。
```haskell

drink aCup ozDrank = if ozDiff >= 0 -- 杯中的饮品容量最小为0,不能无限喝 then cup ozDiff else cup 0 where flOz = getOz aCup ozDiff = flOz - ozDrank

![](https://prod-files-secure.s3.us-west-2.amazonaws.com/4b1b167f-4b52-4031-8233-f4f2689af574/3cc2a304-6cce-41e3-9c14-d2b07d508c2f/image.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=ASIAZI2LB4664ZBW4GBG%2F20260827%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20260827T181111Z&X-Amz-Expires=3600&X-Amz-Security-Token=IQoJb3JpZ2luX2VjEHEaCXVzLXdlc3QtMiJFMEMCHyo186JR1R4lo%2Fd3xfrFI%2BRhRH3vJotvhcypOWgu8W4CIGrU7Fl7hIW0Ut0Lgpp3nev2qCXvZ4CcxIxjCoIfdjJeKv8DCDoQABoMNjM3NDIzMTgzODA1Igx0P00ZJQZDCM9S3bkq3AM8tBvi2EhKkzGZHqQPO2ErF%2F2noa8mj2HNaBEaMPPacvFKt%2FWksv9cVKeHJgU7vhirDHDLvEk%2BFaZZCEnBWV6MljVHFhdwn4VPxnkcEghRGSXMLEhKOecitKZct0ZIZ9g2omWXmXJ3mfb%2Bvju6%2B33MDVo3ZL1ZuxckxvYadds4k4B90ZbPNynqqohKcIXTVehBZTJFlflymcVmH80OX404y6SuVGjt5XAz2dR442W1MYUBO2celNwB6KNY%2FXfJxvYyLqNHgekXWgHtAnniap61VGDBzE3HiSYAvqc0s8rlN6mxgDnk7%2FeXZSZ9hqsKEsmz1MAwvnGkvLOYdLLZQC4OVCTWarMwVfYwyQnSQdeNnb2g0yQsrjPeZW%2FeRlb6vGJncDW1wGIzR2%2FhW3cb6wB6wcljmbf40zG4I2YnUZOhwVFgqZYCw%2FLpUyzguQbIF9kljHpT1Rab3XVcPG%2FB1HOzwrZngGgSlMxSinsOOwA7%2BKJXRpfaTWtTWDlHFzthCXpefGMqtN78TLhl7M21wp9Ct5ndufyGKA%2FW2KFKQP8uZLg6S1iTkDkLuX2yLoclJRm6sd5px03LXcB5gDDdMdLlPFnqBT8dghsXfr65Riubp54sQjDuU7eYbhCRNTDj0MHUBjqnAYba6aLxGfZwsOANjDHu73cnOssB6FoiWojyHdGn5gSYHiklk4K6puJDS%2FgKz%2BjEBkZCOaa6Qipiy0IMiOnpXpiqKIg5N%2FTNN7vc1n%2BZekvG4ApSPXNeWoE6Q2sESI9CnagtVNVBGiv0mujJqc0hAmbov%2FPWNbmLGv1J2S04Rr9k4t97c7dEvYyQ4AxTrKtotjoNmbB%2FtxZj8FW9EmmUld3ennL0Z8xk&X-Amz-Signature=6ffefcadfea6bece99232d135c8bcce278623b79b37e2e66e719a6efe7493864&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject)
可以添加一个辅助函数检查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)

要使该对象具备实用性,我们需要添加若干访问器来便捷操作这些值。首先创建一个辅助函数,通过命名方式访问元组的各个部分(其原理与处理二元组的fstsnd函数相同)。

-- 添加获取对象属性的辅助函数 
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))