我正在阅读和听到,人们(也在此站点上)经常赞美函数式编程范例,强调使所有内容保持不变是多么好的。值得注意的是,人们甚至在传统的命令式OO语言(例如C#,Java或C ++)中也提出了这种方法,不仅是在像Haskell这样的纯函数式语言中,这种语言迫使程序员这样做。
我觉得很难理解,因为我发现可变性和副作用...很方便。但是,考虑到人们当前如何谴责副作用并认为是在可能的情况下消除它们的一种好习惯,我相信如果我想成为一名合格的程序员,我必须让我的乔妮开始对这种范例有一些更好的了解...因此,我的问。
当我发现功能范式存在问题时,一个地方就是自然地从多个地方引用一个对象。让我用两个例子来描述它。
第一个例子是我打算在业余时间制作的C#游戏。这是一种基于回合制的网络游戏,其中两个玩家都有4个怪物组成的团队,并且可以将一个怪物从其团队中移到战场上,在那里它将面对对方玩家发送的怪物。玩家还可以从战场上召回怪物,并用其团队中的另一个怪物代替它们(类似于口袋妖怪)。
在这种情况下,可以从至少2个地方自然引用单个怪物:玩家的团队和战场,其中引用了两个“活动”怪物。
现在,让我们考虑一个怪物被击中并失去20点生命值时的情况。在命令式范式的括号内,我修改了此怪物的health
字段以反映此更改-这就是我现在正在做的。但是,这使Monster
类可变且相关的函数(方法)不纯净,我认为到目前为止,这仍被认为是一种不好的做法。
即使我允许自己以低于理想状态的方式编写游戏代码,以便希望在将来的某个时候完成游戏,但我还是想知道并理解它应该如何写得正确。因此:如果这是设计缺陷,如何解决?
就我所知,在函数式中,我将对此Monster
对象进行复制,使其与旧对象相同,除了该字段。并且该方法suffer_hit
将返回此新对象,而不是就地修改旧对象。然后,我同样会复制Battlefield
对象,并保持其所有字段相同(除了该怪物)。
这至少有两个困难:
- 层次结构可以比Just-
Battlefield
>的简化示例更深入Monster
。我必须对所有字段(除了一个字段)进行这种复制,并在此层次结构中一直返回一个新对象。这将是样板代码,我发现这很烦人,特别是因为功能编程应该减少样板代码。 - 但是,一个更为严重的问题是,这将导致数据不同步。野外活动的怪物的生命值会降低。但是,从其控制者那里引用的同一个怪物
Team
不会。如果我改用命令式风格,那么对数据的每次修改都可以在所有其他代码位置立即看到,在这种情况下,我发现它真的很方便- 但是我得到东西的方式正是人们所说的势在必行!- 现在,可以通过
Team
每次攻击之后的旅程来解决此问题。这是额外的工作。但是,如果以后突然可以从更多地方引用怪物,该怎么办?例如,如果我具有一种能力,例如,让一个怪物专注于另一个不一定在场上的怪物(我实际上正在考虑这种能力)怎么办?我肯定会记得在每次攻击后立即进行一次聚焦怪物的旅程吗?这似乎是一个定时炸弹,随着代码变得越来越复杂,它会爆炸,所以我认为这是没有解决方案的。
- 现在,可以通过
当我遇到相同的问题时,从第二个示例中得出一个更好的解决方案的想法。在学术界,我们被告知要在Haskell中编写我们自己设计的语言的口译员。(这也是我被迫开始了解FP是什么的方式)。当我实现闭包时出现了问题。现在可以再次从多个位置引用同一作用域:通过保存该作用域的变量以及任何嵌套作用域的父作用域!显然,如果通过指向该范围的任何引用对该范围进行了更改,则此更改也必须通过所有其他引用可见。
我提出的解决方案是为每个范围分配一个ID,并在State
monad中保存所有范围的中央字典。现在,变量将仅保留它们绑定到的作用域的ID,而不是作用域本身,并且嵌套作用域还将保留其父作用域的ID。
我猜想在我的怪物战斗游戏中可以尝试相同的方法。相反,它们拥有保存在中央怪物词典中的怪物ID。
但是,我再次看到这种方法存在问题,使我无法毫不犹豫地接受它作为解决问题的方法:
它再次是样板代码的来源。它使单线必然变成三线:以前对单个字段进行单行就地修改现在需要(a)从中央字典中检索对象(b)进行更改(c)保存新对象到中央字典。同样,持有对象和中央字典的id而不是拥有引用会增加复杂性。因为FP的广告是为了减少复杂性和样板代码,所以这暗示我做错了。
我还打算写一个似乎更严重的第二个问题:这种方法引入了内存泄漏。无法访问的对象通常将被垃圾回收。但是,即使没有可访问的对象都引用此特定ID,也无法对中央词典中保存的对象进行垃圾回收。尽管从理论上讲,仔细的编程可以避免内存泄漏(一旦不再需要,我们可以小心地从中央字典中手动删除每个对象),但是这很容易出错,并且FP广告宣传增加了程序的正确性,因此这可能再次不是正确的方法。
但是,我及时发现它似乎是一个已解决的问题。Java提供WeakHashMap
了可用于解决此问题的方法。C#提供了类似的功能- ConditionalWeakTable
尽管根据文档,它是供编译器使用的。在Haskell中,我们有System.Mem.Weak。
存储此类词典是解决此问题的正确功能解决方案,还是我看不到更简单的解决方案?我想象这样的词典的数量很容易增长,而且非常糟糕。因此,如果这些字典区域也应该是不可变的,这可能意味着很多参数传递,或者在支持该功能的语言中,表示单子计算,因为字典将以单子形式保存(但是我再次将其读为纯函数式)语言,尽可能少的代码应该是monadic,而这个字典解决方案会将几乎所有代码都放在State
monad内;这再次使我怀疑这是否是正确的解决方案。)
经过一番考虑之后,我想我会再问一个问题:构建这样的词典能获得什么?许多专家认为,命令式编程的错误之处在于某些对象的更改会传播到其他代码段。为了解决这个问题,对象应该是不可变的-正是出于这个原因,如果我正确理解的话,对它们所做的更改应该在其他地方不可见。但是现在,我担心对过期数据进行操作的其他代码段,因此发明了中央字典,以便……将某些代码段中的更改再次传播到其他代码段中!因此,我们不是回到带有所有假定缺点的命令式样式,而是增加了复杂性吗?
Team
)可以通过(战斗编号,怪物实体ID)元组检索战斗的结果,从而获得怪物的状态。