2010-03-02 57 views
15

我正試圖重新學習系統分析。我有很多面向對象的思想,但我無法在Haskell中找到等價物。一個虛構的系統由救護車站,救護車和乘員組組成。 (它已經獲得了object-y。)所有這個狀態都可以包含在一個大的SystemState類型中。 SystemState [車站] [救護車] [乘員組]。然後我可以創建帶有SystemState的函數,並返回一個新的SystemState。如何管理Haskell中的對象圖?

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. 對「相同」數據的大量引用具有單獨的副本。

我現在可以創建一個函數,它需要一個SystemState和一個船員成員的名字,該名字返回一個新的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.

回答

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實例:-)像這些更新狀態是另一個bean的罐頭;我會給出一些想法,看看我想出了什麼。

編輯:我已經想過這個問題多一些,這裏就是我想要做的可能:

我會用鑰匙打破對象圖中的週期。像這樣的東西會工作(建立真正的圖形時我用了一個類似的方法):

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 ing再次很好地工作!

另外,我可能會設置一個類型類別,使StationStationKey等類型之間的鏈接更加明確,如果這變得太麻煩。我在圖表代碼中沒有這樣做,因爲我只有兩種關鍵類型,它們也是不同的,所以新類型不是必需的。

+0

謝謝,這非常好地解決了偶然的問題之一。 – fadedbee 2010-03-02 13:55:10

+1

我將「Key」定義爲「newtype Key a = Key String derived(Eq,Ord,Show)」。它只是在三種不同的密鑰類型之間節省了少量的重複。 – 2010-03-02 22:14:58

1

這種情況有解決的幾種方法。一種簡單的方法是將您的數據視爲SQL數據庫。也就是說,你的車站,救護車和乘員都是帶有衛星數據的表格。另一種選擇是將其定義爲具有圖庫的圖形數據庫。

+0

這可能是正確的答案。圖表數據庫是我已經考慮過的答案,但我希望這不是唯一的答案。如果我更可愛的答案在幾天內不會出現,我會接受這個答案。 – fadedbee 2010-03-02 14:21:27

1

我試圖做這樣的事情也和我得出的結論是,哈斯克爾(我)很可能不適合這份工作的合適工具。

你的問題2得到它:

大量引用的「相同」的數據 有單獨副本。

Haskell中,作爲語言,是專門設計使它難以「共享實例」或使「單獨的副本」。因爲所有變量都保存不可變的值,所以沒有引用要比較身份的對象。

儘管如此,也有一些技巧。

一種技術是使用mutable objects您的結構。但是,這會強制所有代碼進入monad。

你也可以看看本文Type-Safe Observable Sharing它展示瞭如何使用一些較新的語言功能創建的圖形支持低級別的引用。他們的例子是數字電路,但我認爲它是泛化的。

5

它沒有得到面向對象-Y直到你開始談論繼承和子類型多態。早在OO被構想之前,程序就包含了叫做「救護車」和「車站」的數據結構; OO在數據抽象和封裝方面沒有壟斷。 FP設計也將是「領域驅動的」,以及必要的編程。

您遇到的問題是如何管理狀態,這是Haskell中的一個慢性問題(實際上,在任何編程系統中,請參見SICP第3.1.3節(Abelson和Sussman的計算機程序的結構和解釋http://mitpress.mit.edu/sicp/(不要被大的,學術的話語,也不要推遲域名,這是非常可讀的 - 他們的例子是銀行賬戶)

你的問題是,你引用並堅持舊的,過時的狀態。我建議你編寫採取當前狀態,修改它並返回新狀態的函數。例如:

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

如果你使用ghci解釋器,那麼知道它包含最後一次計算結果的it變量會很方便。

你最終會在國家單子結束了,但它聽起來就像是後來....

2

Haskell是建模您所描述的那種系統的最佳選擇。

然而,像任何編程語言,你的方式你的系統模型 很大程度上依賴於你將要在上面做什麼操作。 像Haskell這樣的函數式編程語言可以幫助您專注於此。 建模數據很好,但功能在哪裏?

您的救護車,車站和船員類型非常簡單明瞭。 我不確定你爲什麼然後想把它們集成到一個大的系統狀態 。這種結構在某些情況下確實有用。 這並不令人驚訝,因爲它有點爲特設 mash-up稍微複雜一點,雖然, 。是否需要 完全取決於您要編寫的函數種類。

但這裏的主要問題是如何有效地使用GHCi。

您在GHCi中究竟想要做什麼?我花了很多時間在GHCi提示符 處。我可以將這段時間分爲三類:探索函數 以更好地理解它們,測試和調試函數以確保它們正常工作,並使用我已經瞭解的 函數執行一次性計算,並且已知它們正在工作。我不認爲我剛剛使用GHCi來輸入數據結構並讓GHCi把它們吐回給我。

儘管如此,對於這三種用途中的每一種,我確實需要數據結構。 通常我需要的東西很簡單,我可以用 一次打完整個東西。對於 ,他們實際上不必非常簡單 - 不要忘記,您可以在單個let聲明中鍵入多個相互遞歸的 定義,方法是用GHCi支持的';'和 將它們分開,行語句用「:{」和「:}」命令

如果一個數據結構,我需要的是足夠複雜,我想 建立起來增量就像你在做什麼,有 幾種簡單的方法可以做到

要獲得一個可變變量,您需要反覆修改以在您的結構上構建 ,類似於你可以在 命令行提示符下執行命令語言, 查看Data.IORef模塊。如果你是Haskell的新手,我會推薦避免Data.IORef就像你的編程中的瘟疫 - 它總是會引誘你,它幾乎總是 做錯事。但在GHCi提示符下,沒關係。

說實話,我幾乎從來沒有這樣做。作爲懶惰,我只是使用向上箭頭和其他命令行編輯鍵來將整個事件 逐步添加到一個GHCi命令中。

和當然,如果你鍵入的數據結構實際上是 有意義的,而不是扔掉的例子,你要輸入 其放入自己喜歡的哈斯克爾編輯器,而不是 在提示符下的文件。然後,您將使用您的編輯器GHCi集成, 或GHCi的「:r」命令,以保持GHCi中提供的最新版本的 結構。

+0

使用ghci沒有特別的理由,這只是我在試驗時使用的。 我正在考慮的功能的一個例子是將一名乘員從一輛救護車移至另一輛救護車的功能。這會影響救護車和救護車領域的船員名單。 SystemState的目的是爲了提供這樣一個函數的參數。 – fadedbee 2010-03-02 19:05:57

1

如果您確實需要數據是遞歸的,請使用像fgl這樣的適當的圖庫。

3

其他人在這裏給出的一個選擇是能夠使用單獨的鍵類型,並通過可能的成員,站點或救護車地圖中的關鍵字查找可能的循環引用。

當然還有一個更直接的編碼使用的引用,其行爲更象你所習慣的:

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) 

這導致嚴重副作用effectful編程風格。實質上,您只需使用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 

,你可以讓看起來更自然的幾倍名稱和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 [email protected](_,_,n) = (,) n `fmap` lookupCrewTraits scn 
+0

這看起來很有趣。你寫的很多東西超出了我對Haskell的理解,但這是一件好事。 – fadedbee 2010-03-03 08:23:10