有没有一个Haskell习惯用法可以用来更新嵌套的数据结构?

eimct9ow  于 2023-08-06  发布在  其他
关注(0)|答案(4)|浏览(116)

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

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

xurqigkl

xurqigkl1#

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

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年编辑补充:目前,对于X1 E1 F1 X包是一种特别好的实现技术存在广泛的共识。虽然它是一个非常大的包,但在网络上的各个地方也有非常好的文档和介绍性材料。

9rnv2umw

9rnv2umw2#

下面是如何使用语义编辑器组合子(SEC),就像Lambdageek建议的那样。
首先是几个有用的缩写:

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的方式来处理这种区别。

  • Edit:* 这里有一个基本SEC的替代样式:
onManager :: Lifter Coach BBTeam
onManager f t = t { manager = f (manager t) }

tyky79it

tyky79it3#

稍后,您可能还想看看一些通用编程库:当你的数据复杂性增加,你发现自己写了更多的样板代码(比如增加球员的牛排含量,教练的饮食和观察员的啤酒含量),即使在不那么冗长的形式下,这仍然是样板代码。SYB可能是最知名的库(并随Haskell平台提供)。事实上,original paper on SYB使用非常类似的问题来演示该方法:
考虑以下描述公司组织结构的数据类型。公司分成几个部门。每个部门都有一个经理,并由子单元的集合组成,其中单元可以是单个员工或部门。无论是管理者还是普通员工,都只是领取工资的人。
[跳过]
现在假设我们想把公司里每个人的工资都增加一个特定的百分比。也就是说,我们必须编写函数:
增加::浮动->公司->公司
(the其余的都在报纸上-建议阅读)
当然,在你的例子中,你只需要访问/修改一个微小的数据结构的一部分,所以它不需要通用的方法(仍然是基于SYB的解决方案为您的任务是下面),但一旦你看到重复的代码/模式的访问/修改,你可能会想检查这个或other通用编程库。

{-# 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)

字符串

q9yhzks0

q9yhzks04#

现代的解决方案是record dot syntax,在过去的几年里慢慢增加了as compiler extensions
目前,您需要GHC 9.2这样做:

{-# LANGUAGE OverloadedRecordDot, OverloadedRecordUpdate #-}

addManagerSteak team = team { manager.diet.steaks = team.manager.diet.steaks + 1 }

字符串
您也可以使用新的模式匹配语法(NamedFieldPunsRecordWildCards)来匹配steaks而不是team,并使用 that 而不是team.manager.diet.steaks,但它会更长。
此外,succ是一个比+ 1更通用的函数,在Haskell中,所有变量都是常量,因此直接使用+=语法可能会产生误导。
所以其他的方法,特别是透镜包(它一直是一个丑陋的黑客,试图在错误的地方解决问题(作为一个库,而不是修复语言语法)出于简单的必要性,而且,如果出了什么问题,就会给出无法理解的错误消息)被认为是过时的。
您甚至可以使用{-# LANGUAGE NoTraditionalRecordSyntax #-}禁用旧的内置语法。

相关问题