你如何在Haskell中管理对象图?

时间:2010-03-02 13:11:04

标签: oop haskell

我正在尝试重新学习系统分析。我有很多面向对象的思想,但我还是找不到Haskell中的等价物。

虚构的系统包括救护站,救护车和船员。 (它已经得到了对象y。)所有这些状态都可以包含在一个大的SystemState类型中。 SystemState [Stations] [Ambulances] [Crew]。然后我可以创建带有SystemState的函数,并返回一个新的SystemState。

module AmbSys
    ( version
    , SystemState
    , Station
    , Ambulance
    , Crew
    ) where

version = "0.0.1"

data SystemState = SystemState [Station] [Ambulance] [Crew] deriving (Show)

data Station = Station { stName :: String
                       , stAmbulances :: [Ambulance]
                       } deriving (Show)

data Ambulance = Ambulance { amCallSign :: String
                           , amStation :: Station
                           , amCrew :: [Crew]
                           } deriving (Show)

data Crew = Crew { crName :: String
                 , crAmbulance :: Ambulance
                 , crOnDuty :: Bool
                 } deriving (Show)

这是我创建一些数据的ghci会话。

*AmbSys> :load AmbSys                 
[1 of 1] Compiling AmbSys           ( AmbSys.hs, interpreted )
Ok, modules loaded: AmbSys.
*AmbSys> let s = Station "London" []                
*AmbSys> let a = Ambulance "ABC" s []               
*AmbSys> let s' = Station "London" [a]
*AmbSys> let c = Crew "John Smith" a False        
*AmbSys> let a' = Ambulance "ABC" s [c]   
*AmbSys> let s'' = Station "London" [a']             
*AmbSys> let system_state = SystemState [s''] [a'] [c]
*AmbSys> system_state                                 
SystemState [Station {stName = "London", stAmbulances = [Ambulance {amCallSign = "ABC",
 amStation = Station {stName = "London", stAmbulances = []}, amCrew = [Crew 
 {crName = "John Smith", crAmbulance = Ambulance {amCallSign = "ABC", 
 amStation = Station {stName = "London", stAmbulances = []}, amCrew = []}, 
 crOnDuty = False}]}]}] [Ambulance {amCallSign = "ABC", amStation = Station {
 stName = "London", stAmbulances = []}, amCrew = [Crew {crName = "John Smith",
 crAmbulance = Ambulance {amCallSign = "ABC", amStation = Station {stName = "London",
 stAmbulances = []}, amCrew = []}, crOnDuty = False}]}] [Crew {crName = "John Smith",
 crAmbulance = Ambulance {amCallSign = "ABC", amStation = Station {stName = "London",
 stAmbulances = []}, amCrew = []}, crOnDuty = False}]

您已经可以在这里看到一些问题:

  1. 我无法创建一致的SystemState - 某些值是'旧'值,例如s或s',而不是s''。
  2. 许多对“相同”数据的引用都有单独的副本。
  3. 我现在可以创建一个函数,它接受一个SystemState和一个Crew成员的名字,该名称返回一个新的SystemState,其中该工作人员是“休班”。

    我的问题是我必须找到并更改救护车中的机组成员和SystemState中的(相同副本)机组成员。

    这对小型系统来说是可能的,但实际系统有更多的联系。它看起来像一个n平方的问题。

    我非常清楚我正在以面向对象的方式思考系统。

    如何在Haskell中正确创建这样的系统?

    编辑:感谢大家的回答,以及reddit上的回复http://www.reddit.com/r/haskell/comments/b87sc/how_do_you_manage_an_object_graph_in_haskell/

    我现在的理解似乎是我可以在Haskell中做我想要的事情。在缺点方面,似乎对象/记录/结构图不是Haskell中的“第一类”对象(因为它们在C / Java /等中),因为缺少引用。这只是一个权衡 - 在Haskell中,某些任务在语法上更简单,有些在C中更简单(也更不安全)。

7 个答案:

答案 0 :(得分:8)

小提示:如果你使用递归letwhere(在.hs文件中,我认为它不适用于ghci)你至少可以更容易地设置初始图形如下:

ambSys = SystemState [s] [a] [c] where
    s = Station "London" [a]
    a = Ambulance "ABC" s [c]
    c = Crew "John Smith" a False

这会让你进入我认为你想要达到的状态,但是不要尝试使用派生的Show实例:-)像这样更新状态是另一种豆类;我会考虑一下,看看我想出了什么。

编辑:我已经考虑了一些,这就是我可能会做的事情:

我会使用键来破坏对象图中的循环。这样的东西可行(我在构建真实图形时使用了类似的方法):

import qualified Data.Map as M

version = "0.0.1"

newtype StationKey = StationKey String deriving (Eq,Ord,Show)
newtype AmbulanceKey = AmbulanceKey String deriving (Eq,Ord,Show)
newtype CrewKey = CrewKey String deriving (Eq,Ord,Show)

data SystemState = SystemState (M.Map StationKey Station) (M.Map AmbulanceKey Ambulance) (M.Map CrewKey Crew) deriving (Show)

data Station = Station { stName :: StationKey
                       , stAmbulances :: [AmbulanceKey]
                       } deriving (Show)

data Ambulance = Ambulance { amCallSign :: AmbulanceKey
                           , amStation :: StationKey
                           , amCrew :: [CrewKey]
                           } deriving (Show)

data Crew = Crew { crName :: CrewKey
                 , crAmbulance :: AmbulanceKey
                 , crOnDuty :: Bool
                 } deriving (Show)

ambSys = SystemState (M.fromList [(londonKey, london)]) (M.fromList [(abcKey, abc)]) (M.fromList [(johnSmithKey, johnSmith)]) where
    londonKey = StationKey "London"
    london = Station londonKey [abcKey]
    abcKey = AmbulanceKey "ABC"
    abc = Ambulance abcKey londonKey [johnSmithKey]
    johnSmithKey = CrewKey "John Smith"
    johnSmith = Crew johnSmithKey abcKey False

然后你可以开始定义自己的状态修改组合器。正如你所看到的,状态的构建现在更加冗长,但是show再次很好地工作了!

此外,我可能会设置一个类型类,以使StationStationKey等类型之间的链接更加明确,如果这样做太麻烦了。我没有在我的图形代码中这样做,因为我只有两个键类型,它们也是不同的,所以新类型不是必需的。

答案 1 :(得分:5)

在你开始讨论继承和子类型多态之前,它没有得到面向对象的y。在构想OO之前,程序包含称为“救护车”和“车站”的数据结构; OO没有垄断数据抽象和封装。 FP设计也将是“域驱动”,命令式编程也是如此。

您遇到的问题是如何管理状态,这是Haskell中的一个长期问题(实际上,在任何编程系统中,请参阅SICP的3.1.3节(Abelson和Sussman的结构和计算机程序的解释){{3 (不要被大的,学术性的词语和域名推迟,它是非常易读的 - 他们的例子是银行账户)。

你的问题是你引用并保持过时的状态。我建议你编写采用当前状态,修改它并返回新状态的函数。类似的东西:

addStation state station = 
     let (SystemState stations ambs crews) = state
     in SystemState (station:stations) ambs crews)

如果你使用ghci解释器,那么了解it变量会很方便,它包含最后一次计算的结果。

你最终会在State Monad结束,但听起来就像以后......

答案 2 :(得分:3)

其他人给出的一个选项是能够使用单独的Key类型,并在可能的船员,车站或救护车的地图中按键查找您可能持有循环引用的值。

使用引用当然有更直接的编码,其行为更像您习惯的:

data Station = Station { stName :: String 
                       , stAmbulances :: [IORef Ambulance] 
                       } deriving (Show) 

data Ambulance = Ambulance { amCallSign :: String 
                           , amStation :: IORef Station 
                           , amCrew :: [IORef Crew] 
                           } deriving (Show) 

data Crew = Crew { crName :: String 
                 , crAmbulance :: IORef Ambulance 
                 , crOnDuty :: Bool 
                 } deriving (Show) 

这会产生严重的副作用编程风格。本质上,您只需使用IO monad开始在Haskell中编写C / C ++。

有两种类似Haskell的方法可以解决这个问题。

一个是打结,并保留循环引用,但随后更新成为问题。

另一种是杀死循环引用:

data Station = Station { stName :: String 
                       , stAmbulances :: [Ambulance] 
                       } deriving (Show) 

data Ambulance = Ambulance { amCallSign :: String 
                           , amCrew :: [Crew] 
                           } deriving (Show) 

data Crew = Crew { crName :: String 
                 , crOnDuty :: Bool 
                 } deriving (Show) 

您可以从车站访问工作人员:

stCrew :: Station -> [Crew]
stCrew = stAmbulances >>= amCrew

根据您需要的访问类型,这可能需要相当慢的路径来访问Crew的成员。

但是,更好的编码可能是几乎完全从你的思想中消除对象,并拥抱你用来查找键作为结构本身一部分的地图。我为这段代码的粗略性而道歉,我正在即兴地写它。

import Control.Monad ((>=>))
import Data.Map (Map)
import qualified Data.Map as Map

type Name = String
newtype CrewTraits = CrewTraits { onDuty :: Bool }
type Crew = (Name, CrewTraits) 

type CallSign = String
type AmbulanceTraits = Map Name AssignmentTraits
type Amulance = (CallSign, AmbulanceTraits)

type StationName = String
type StationTraits = Map CallSign AmbulanceTraits
type Station = (StationName,StationTraits)

type Fleet = Map StationName StationTraits

crew :: Name -> Bool -> Crew
crew name isOnDuty = (name, CrewTraits isOnDuty)

ambulance :: CallSign -> [Crew] -> Ambulance
ambulance sign crew = (sign, Map.fromList crew)

station :: StationName -> [Ambulance] -> Station
station name ambulances = (name, Map.fromList ambulances)

fleet :: [Station] -> Fleet
fleet = Map.fromList

现在,您只需使用Data.Map中的内置功能即可更改工作站:

updateStationTraits :: (StationName -> StationTraits -> Maybe StationTraits) ->
                       StationName -> Fleet -> Fleet
updateStationTraits = Map.updateWithKey

你可以通过组合Name和StationTraits使其看起来更自然:

updateStation :: (Station -> Maybe StationTraits) -> 
                 StationName -> Fleet -> Fleet
updateStation = Map.updateWithKey . curry

addAmbulanceToFleet :: Ambulance -> StationName -> Fleet -> Fleet
addAmbulanceToFleet (k,v) = Map.adjust (Map.insert k v)

通过所有这些,您现在可以将此结构中的路径概念与先前的密钥概念统一起来:

type CrewPath = (StationName,CallSign,Name)
type AmbulancePath = (StationName, CallSign)
type StationPath = StationName

lookupCrewTraits :: CrewKey -> Fleet -> Maybe CrewTraits
lookupCrewTraits (s,c,n) = lookup s >=> lookup c >=> lookup n

lookupCrew :: CrewKey -> Fleet -> Maybe Crew
lookupCrew scn@(_,_,n) = (,) n `fmap` lookupCrewTraits scn

答案 3 :(得分:2)

Haskell是您所描述系统类型建模的绝佳选择。

但是,与任何编程语言一样,您对系统进行建模的方式如下: 严重依赖于您将要对其执行的操作。 像Haskell这样的函数式编程语言可以帮助您专注于此。 对数据建模很好,但函数在哪里?

您的救护车,车站和船员类型简单明了。 我不确定你为什么要把它们组合成一个大的 SystemState。在某些情况下,这种结构确实很有用。 不过,为你复杂的东西并不奇怪, 因为它有点像 ad hoc mash-up。是否需要它 完全取决于你将要编写的函数类型。

但这里的主要问题是如何有效地使用GHCi。

你究竟想在GHCi做什么?我花了很多时间 GHCi提示。我可以将这个时间分为三类:探索功能 更好地理解它们,测试和调试功能以确保它们正常工作, 并使用我已经理解的功能执行一次性计算 并且已经知道正在工作。我不认为我非常喜欢GHCi 只需键入数据结构并让GHCi将它们吐回我身边。

但是,对于这三种用途中的每一种,我确实需要数据结构。 通常我需要的那些足够简单,我可以 一次输入整个东西。它们实际上并不一定非常简单 - 不要忘记你可以输入多个相互递归 单个 let 语句中的定义,用';'分隔它们,和 GHCi支持使用“:{{”和“:}”命令的多行语句。

如果我需要的数据结构足够复杂,我想要 像往常一样逐步建立它,有 几种简单的方法。

获取您反复修改以构建的可变变量 你的结构,类似于你的方式 命令行提示命令式语言, 看看Data.IORef模块。如果你是Haskell的新手,我愿意 建议在编程中避免像瘟疫这样的Data.IORef - 它会一直诱惑你,它几乎总是错误的 去做。但是在GHCi的提示下,没关系。

说实话,我几乎从不这样做。懒惰,我只是使用 向上箭头和其他命令行编辑键来获取整个事物 逐步进入一个GHCi命令。

当然,如果您输入的数据结构实际上是 有意义的,而不是一个扔掉的例子,你想要打字 它在你最喜欢的Haskell编辑器中的文件而不是 在提示。然后你将使用编辑器的GHCi集成, 或GHCi的“:r”命令,以保持您的最新版本 结构在GHCi中可用。

答案 4 :(得分:1)

有几种方法可以解决这个问题。一种简单的方法是将您的数据视为SQL数据库。也就是说,您的车站,救护车和船员都是与其相关的卫星数据的表格。另一个选择是将其定义为带有图形库的图形数据库。

答案 5 :(得分:1)

我也试过这样做,我得出的结论是,Haskell(对我来说)可能不适合这项工作。

你的问题2得到了它:

  

对“相同”数据的大量引用   有单独的副本。

Haskell作为一种语言,是专门设计的,难以“共享实例”或制作“单独的副本”。因为所有变量都包含不可变值,所以没有对象的引用来比较标识。

那说,有一些技巧。

一种技巧是使用mutable objects作为结构。但是,这会强制所有代码成为monad。

您还可以查看本文Type-Safe Observable Sharing,其中介绍了如何在创建图表时使用支持低级引用的一些较新语言功能。他们的例子是数字电路,但我认为它是概括的。

答案 6 :(得分:1)

如果你确实需要数据是递归的,请使用像fgl这样的图形库。