Haskell有没有更新嵌套数据结构的习惯用法?



假设我有以下数据模型,用于跟踪棒球运动员、球队和教练的统计数据:

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 }

您也可以使用新的模式匹配语法(NamedFieldPunsRecordWildCards)来匹配steaks而不是team,并使用而不是team.manager.diet.steaks,但它会更长。

此外,succ是一个比+ 1更通用的函数,在Haskell中,所有变量都是常量,所以直接的+=语法会误导。

因此,其他方法,特别是lens包(出于简单的需要,它一直是一个丑陋的hack,试图在错误的地方解决问题(作为一个库而不是修复语言语法),并且,如果出现任何错误,则会因此给出难以理解的错误消息)被认为是过时的。
您甚至可以使用{-# LANGUAGE NoTraditionalRecordSyntax #-}禁用旧的内置语法。

最新更新