48

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

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- 所以我认为我试图update-in在函数式编程和 Haskell 和静态类型的上下文中理解。

4

3 回答 3

41

记录更新语法是编译器的标准配置:

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 年编辑补充说:如今,人们普遍认为镜头包是一种特别好的实施技术。虽然它是一个非常大的软件包,但在网络上的各个地方也有非常好的文档和介绍性材料。

于 2011-09-09T17:59:42.067 回答
11

正如 Lambdageek 所建议的,这是您可以使用语义编辑器组合器 (SEC) 的方法。

首先是几个有用的缩写:

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 允许有针对性的应用,如果球员有饮食但我们不想调整它们,这将很有帮助。也许也有一种非常好的 SYB 方法来处理这种区别。

编辑:这是基本 SEC 的另一种样式:

onManager :: Lifter Coach BBTeam
onManager f t = t { manager = f (manager t) }
于 2011-12-24T19:31:49.737 回答
5

稍后您可能还想看看一些通用编程库:当您的数据复杂性增加并且您发现自己编写更多样板代码(例如增加球员的牛排含量、教练的饮食和观察者的啤酒含量)时即使以不那么冗长的形式仍然是样板。 SYB可能是最著名的库(并附带 Haskell 平台)。事实上,关于 SYB 的原始论文使用非常相似的问题来演示该方法:

考虑以下描述公司组织结构的数据类型。公司分为部门。每个部门都有一名经理,并由一组子单位组成,其中一个单位可以是单个员工或部门。经理和普通员工都只是领取薪水的人。

[跳过]

现在假设我们想将公司中每个人的工资提高一个指定的百分比。也就是说,我们必须编写函数:

增加 :: 浮动 -> 公司 -> 公司

(其余在论文中 - 建议阅读)

当然,在您的示例中,您只需要访问/修改一个微小的数据结构,因此它不需要通用方法(下面仍然是您的任务的基于 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)
于 2011-09-10T01:04:49.970 回答