假设我有以下数据模型,用于跟踪棒球运动员、球队和教练的统计数据:
data BBTeam = BBTeam { teamname :: String,
manager :: Coach,
players :: [BBPlayer] }
deriving (Show)
data Coach = Coach { coachname :: String,
favcussword :: String,
diet :: Diet }
deriving (Show)
data Diet = Diet { dietname :: String,
steaks :: Integer,
eggs :: Integer }
deriving (Show)
data BBPlayer = BBPlayer { playername :: String,
hits :: Integer,
era :: Double }
deriving (Show)
现在让我们假设经理,通常是牛排狂热者,想要吃更多的牛排——所以我们需要能够增加经理饮食中的牛排含量。以下是该函数的两种可能实现:
1)这使用了大量的模式匹配,我必须得到所有构造函数的所有参数顺序正确…两次。看起来它不是很好伸缩,也不是很好维护/可读性。
addManagerSteak :: BBTeam -> BBTeam
addManagerSteak (BBTeam tname (Coach cname cuss (Diet dname oldsteaks oldeggs)) players) = BBTeam tname newcoach players
where
newcoach = Coach cname cuss (Diet dname (oldsteaks + 1) oldeggs)
2)这使用了Haskell的记录语法提供的所有访问器,但它也很丑陋和重复,很难维护和阅读,我认为。
addManStk :: BBTeam -> BBTeam
addManStk team = newteam
where
newteam = BBTeam (teamname team) newmanager (players team)
newmanager = Coach (coachname oldcoach) (favcussword oldcoach) newdiet
oldcoach = manager team
newdiet = Diet (dietname olddiet) (oldsteaks + 1) (eggs olddiet)
olddiet = diet oldcoach
oldsteaks = steaks olddiet
我的问题是,在Haskell社区中,是其中一个比另一个更好,还是更受欢迎?是否有更好的方法来做到这一点(在保持上下文的同时修改数据结构内部的值)?我不担心效率,只担心代码的优雅性/通用性/可维护性。
我注意到Clojure: update-in
中有一些问题(或类似的问题?)——所以我想我正在尝试在函数式编程、Haskell和静态类型的背景下理解update-in
。
记录更新语法是编译器的标准语法:
addManStk team = team {
manager = (manager team) {
diet = (diet (manager team)) {
steaks = steaks (diet (manager team)) + 1
}
}
}
太可怕了!但是有一个更好的方法。Hackage上有几个实现函数引用和镜头的包,这绝对是你想要做的。例如,使用fclabels包,您可以在所有记录名称前面加上下划线,然后写入
$(mkLabels ['BBTeam, 'Coach, 'Diet, 'BBPlayer])
addManStk = modify (+1) (steaks . diet . manager)
2017年编辑补充:如今,人们普遍认为镜头封装是一种特别好的实现技术。虽然它是一个非常大的软件包,但也有非常好的文档和介绍性材料可以在网络上的各个地方找到。
您可以按照Lambdageek的建议使用语义编辑器组合器(SECs)。
首先是几个有用的缩写:
type Unop a = a -> a
type Lifter p q = Unop p -> Unop q
这里的Unop
是"语义编辑器",Lifter
是语义编辑器组合器。一些搬运工:
onManager :: Lifter Coach BBTeam
onManager f (BBTeam n m p) = BBTeam n (f m) p
onDiet :: Lifter Diet Coach
onDiet f (Coach n c d) = Coach n c (f d)
onStakes :: Lifter Integer Diet
onStakes f (Diet n s e) = Diet n (f s) e
现在只需编写sec来表达您想要的内容,即在(团队)经理饮食的利害关系中添加1:
addManagerSteak :: Unop BBTeam
addManagerSteak = (onManager . onDiet . onStakes) (+1)
与SYB方法相比,SEC版本需要额外的工作来定义SEC,我只提供了本示例中所需的SEC。SEC允许有针对性的应用程序,这将有助于如果玩家有饮食,但我们不想调整他们。也许有一种非常简单的方法来处理这种区别。
编辑:这是基本SECs的另一种样式:
onManager :: Lifter Coach BBTeam
onManager f t = t { manager = f (manager t) }
之后你可能还想看看一些泛型编程库:当你的数据复杂性增加时,你发现自己编写了更多的样板代码(比如为球员,教练的饮食增加牛排内容和观众的啤酒内容),即使不那么冗长的形式仍然是样板代码。SYB可能是最著名的库(并且是Haskell平台自带的)。实际上,关于SYB的原始论文使用了非常类似的问题来演示该方法:
考虑以下描述公司组织结构的数据类型。公司分为几个部门。每个部门都有一名经理,并由一组子单位组成,其中一个单位可以是单个雇员或一个部门。经理和普通员工都是领工资的人。
(跳跃)现在假设我们想给公司里每个人的工资增加一个指定的百分比。也就是说,我们必须编写如下函数:
increase:: Float ->公司→公司
(其余部分在论文中-建议阅读)
当然,在你的例子中,你只需要访问/修改一个微小的数据结构的一部分,所以它不需要通用的方法(仍然是基于syb的解决方案,你的任务是在下面),但是一旦你看到重复的代码/访问/修改模式,你可能想检查这个或其他泛型编程库。
{-# LANGUAGE DeriveDataTypeable #-}
import Data.Generics
data BBTeam = BBTeam { teamname :: String,
manager :: Coach,
players :: [BBPlayer]} deriving (Show, Data, Typeable)
data Coach = Coach { coachname :: String,
favcussword :: String,
diet :: Diet } deriving (Show, Data, Typeable)
data Diet = Diet { dietname :: String,
steaks :: Integer,
eggs :: Integer} deriving (Show, Data, Typeable)
data BBPlayer = BBPlayer { playername :: String,
hits :: Integer,
era :: Double } deriving (Show, Data, Typeable)
incS d@(Diet _ s _) = d { steaks = s+1 }
addManagerSteak :: BBTeam -> BBTeam
addManagerSteak = everywhere (mkT incS)
现代的解决方案是记录点语法,它在过去几年中作为编译器扩展慢慢添加。
目前,你需要GHC 9.2这样做:
{-# LANGUAGE OverloadedRecordDot, OverloadedRecordUpdate #-}
addManagerSteak team = team { manager.diet.steaks = team.manager.diet.steaks + 1 }
您也可以使用新的模式匹配语法(NamedFieldPuns
和RecordWildCards
)来匹配steaks
而不是team
,并使用而不是team.manager.diet.steaks
,但它会更长。
此外,succ
是一个比+ 1
更通用的函数,在Haskell中,所有变量都是常量,所以直接的+=
语法会误导。
因此,其他方法,特别是lens包(出于简单的需要,它一直是一个丑陋的hack,试图在错误的地方解决问题(作为一个库而不是修复语言语法),并且,如果出现任何错误,则会因此给出难以理解的错误消息)被认为是过时的。
您甚至可以使用{-# LANGUAGE NoTraditionalRecordSyntax #-}
禁用旧的内置语法。