使用函数式进行面向对象编程
1. 函数式编程中的一流公民函数
在 Haskell 中,函数是一等公民,这意味着你可以像对待数据一样对待函数。闭包(closure)允许你将状态封装在函数中,而无需显式地使用可变变量。
基本构造函数示例:杯子(Cup)
cup flOz = \\message -> message flOz
- 这里定义了一个
cup
函数,它接受一个参数flOz
(液体盎司),并返回一个新函数。这个新函数等待一个“消息”(message
),并将flOz
传递给它。 - 这是一种封装状态的方式,
flOz
被“记住”在闭包中。
获取状态:getOz
getOz aCup = aCup (\\flOz -> flOz)
getOz
是一个访问器函数,它从aCup
中提取flOz
的值。- 示例:
coffeeCup = cup 12 -- 创建一个装有12盎司咖啡的杯子 GHCi> getOz coffeeCup 12
更新状态:drink
初始版本:
drink aCup ozDrank = cup (flOz - ozDrank)
where flOz = getOz aCup
drink
表示从杯子中喝掉ozDrank
盎司液体,返回一个新的杯子对象。- 问题:如果喝的量超过杯子里的量,结果可能是负数。
改进版本:
drink aCup ozDrank = if ozDiff >= 0
then cup ozDiff
else cup 0
where flOz = getOz aCup
ozDiff = flOz - ozDrank
- 改进后,如果喝的量超过剩余量,杯子剩余量不会变成负数,而是变成 0。
模拟多次喝水:foldl
afterManySips = foldl drink coffeeCup [1,1,1,1,1]
- 使用
foldl
依次应用drink
函数,模拟喝 5 次,每次喝 1 盎司。 - 如果
coffeeCup
初始有 12 盎司,喝完 5 次后还剩 7 盎司。
2. 更复杂的对象:机器人(Robot)
构造函数
robot (name, attack, hp) = \\message -> message (name, attack, hp)
robot
是一个构造函数,接受三个参数:名字(name
)、攻击力(attack
)、生命值(hp
),并返回一个函数。- 示例:
killerRobot = robot ("Kill3r", 25, 200) -- 创建一个名叫“Kill3r”的机器人
Getter 函数
getName aRobot = aRobot (\\(n, a, h) -> n)
getAttack aRobot = aRobot (\\(n, a, h) -> a)
getHP aRobot = aRobot (\\(n, a, h) -> h)
- 这些函数分别提取机器人的名字、攻击力和生命值。
Setter 函数
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))
- 这些函数返回一个新机器人对象,更新了对应的属性,而不修改原始对象(符合无状态编程)。
打印机器人信息
printRobot aRobot = aRobot (\\(n, a, h) -> n ++ " attack:" ++ show a ++ " hp:" ++ show h)
- 示例输出:
"Kill3r attack:25 hp:200"
3. 对象之间的交互:战斗(Fight)
造成伤害:damage
damage aRobot attackDamage = aRobot (\\(n, a, h) -> robot (n, a, h - attackDamage))
damage
函数减少目标机器人的生命值,返回一个新机器人对象。
定义战斗逻辑:fight
fight aRobot defender = damage defender attack
where attack = if getHP aRobot > 10
then getAttack aRobot
else 0
- 如果攻击者的生命值大于 10,它会用自己的攻击力伤害防御者;否则,攻击力为 0(模拟“死亡”状态)。
三回合战斗示例
gentleGiant = robot ("GentleGiant", 10, 100) -- 假设初始值
killerRobot = robot ("Kill3r", 25, 200)
gentleGiantRound1 = fight killerRobot gentleGiant -- Kill3r 攻击 GentleGiant
killerRobotRound1 = fight gentleGiant killerRobot -- GentleGiant 反击 Kill3r
gentleGiantRound2 = fight killerRobotRound1 gentleGiantRound1
killerRobotRound2 = fight gentleGiantRound1 killerRobotRound1
gentleGiantRound3 = fight killerRobotRound2 gentleGiantRound2
killerRobotRound3 = fight gentleGiantRound2 killerRobotRound2
- 每轮战斗生成一个新对象,状态不会改变原始对象。
4. 无状态编程的重要性
示例:快慢机器人
fastRobot = robot ("speedy", 15, 40)
slowRobot = robot ("slowpoke", 20, 30)
- 假设“快”机器人应该先攻击,但代码中顺序可以任意排列。
战斗顺序
slowRobotRound1 = fight fastRobot slowRobot
fastRobotRound1 = fight slowRobotRound1 fastRobot
slowRobotRound2 = fight fastRobotRound1 slowRobotRound1
fastRobotRound2 = fight slowRobotRound1 fastRobotRound1
slowRobotRound3 = fight fastRobotRound2 slowRobotRound2
fastRobotRound3 = fight slowRobotRound3 fastRobotRound2
- 在 Haskell 中,结果不依赖执行顺序,因为它是纯函数式的,没有副作用。
与有状态编程对比
在面向对象编程(OOP)中:
fastRobot.fight(slowRobot)
slowRobot.fight(fastRobot)
-
如果是异步或并发执行,顺序不可控,可能导致结果不一致。
-
而 Haskell 的无状态特性保证了计算的可预测性,即使你重新排列代码: 结果仍然相同。 ```haskell fastRobotRound3 = fight slowRobotRound3 fastRobotRound2 fastRobotRound2 = fight slowRobotRound2 fastRobotRound1 fastRobotRound1 = fight slowRobotRound1 fastRobot slowRobotRound2 = fight fastRobotRound1 slowRobotRound1 slowRobotRound3 = fight fastRobotRound2 slowRobotRound2 slowRobotRound1 = fight fastRobot slowRobot
```
5. 为什么无状态编程重要?
- 清晰性:隐藏状态可能让代码更简洁,但容易引入隐藏的错误。
- 可控性:无状态编程让你完全控制计算顺序,避免并发或异步带来的不确定性。
- 可预测性:Haskell 的纯函数式特性确保结果只依赖输入,不受执行顺序影响。
总结
这段代码展示了如何在 Haskell 中使用闭包和函数式编程模拟对象和状态管理。通过 cup
和 robot
示例,你可以看到无状态编程如何通过生成新对象来“更新”状态,而不修改原有数据。这种方法在并发编程中尤为强大,因为它消除了副作用和顺序依赖的问题。