Как вы управляете графом объектов в Haskell? - PullRequest
15 голосов
/ 02 марта 2010

Я пытаюсь заново изучить системный анализ. У меня есть много объектно-ориентированного мышления, для которого я пока не могу найти эквивалентов в Haskell.

Вымышленная система состоит из станций скорой помощи, машин скорой помощи и экипажа. (Он уже получает object-y.) Все это состояние может быть заключено в большой тип SystemState. SystemState [Станции] [Скорая помощь] [Экипаж]. Затем я могу создать функции, которые принимают 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. множество ссылок на «одинаковые» данные имеют отдельные копии.

Теперь я могу создать функцию, которая принимает имя SystemState и имя члена экипажа, которое возвращает новое состояние SystemState, где этот член экипажа находится в нерабочем состоянии.

Моя проблема в том, что я должен найти и заменить члена экипажа в машине скорой помощи и (идентичной копии) члена экипажа в SystemState.

Это возможно для небольших систем, но реальные системы имеют гораздо больше связей. Это похоже на проблему n-квадрата.

Мне очень хорошо известно, что я думаю о системе объектно-ориентированным способом.

Как правильно создать такую ​​систему в Haskell?

Редактировать: Спасибо всем за ваши ответы, и те, кто на Reddit тоже http://www.reddit.com/r/haskell/comments/b87sc/how_do_you_manage_an_object_graph_in_haskell/

Теперь я понимаю, что в Хаскеле я могу делать то, что хочу. С другой стороны, кажется, что графы объектов / записей / структур не являются объектами первого класса в Haskell (как в C / Java / и т. Д.) Из-за необходимого отсутствия ссылок. Здесь есть только компромисс - некоторые задачи синтаксически проще в Haskell, некоторые проще (и более небезопасны) в C.

Ответы [ 7 ]

8 голосов
/ 02 марта 2010

Небольшой совет: если вы используете рекурсивный let или where (в файле .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 снова работает хорошо!

Также я бы, вероятно, настроил класс типов, чтобы сделать связь между типами Station и StationKey и так далее более явной, если это становится слишком громоздким. Я не делал этого в своем графическом коде, так как там у меня было только два ключевых типа, которые также были различны, поэтому новые типы не были необходимы.

5 голосов
/ 02 марта 2010

Это не станет объектно-ориентированным, пока вы не начнете говорить о наследовании и полиморфизме подтипа. Программы содержали структуры данных, называемые «скорая помощь» и «станция» задолго до того, как была задумана ОО; ОО не имеет монополии на абстрагирование и инкапсуляцию данных. Проектирование FP также будет «управляемым доменом», как и императивное программирование.

Проблема, с которой вы сталкиваетесь, заключается в том, как управлять состоянием, и это хроническая проблема в Haskell (фактически, в любой системе программирования, см. Раздел 3.1.3 SICP (Структура и интерпретация компьютерных программ Абельсоном и Суссманом http://mitpress.mit.edu/sicp/ (не отчаивайтесь большими академическими словами или доменным именем, оно очень читабельное - их пример - банковский счет).

Ваша проблема в том, что вы ссылаетесь и держитесь за старое, устаревшее состояние. Я бы предложил вам написать функции, которые принимают текущее состояние, изменяют его и возвращают новое состояние. Что-то вроде:

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

А если вы используете интерпретатор ghci, вам будет полезно узнать о переменной it, которая содержит результат последнего вычисления.

Вы в конечном итоге окажетесь в Государственной Монаде, но, похоже, это будет позже ...

3 голосов
/ 02 марта 2010

Один из вариантов, который здесь предоставили другие, - это возможность использовать отдельный тип ключа и искать значения, к которым вы, возможно, располагаете круговые ссылки ключом, на карте возможных членов экипажа, станций или скорой помощи.

Конечно, существует более прямое кодирование с использованием ссылок, которое больше похоже на то, к чему вы привыкли:

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) 

Это приводит к сильному побочному стилю программирования. По сути, вы только начинаете писать C / C ++ на Haskell, используя монаду ввода-вывода.

Есть два подхода, подобных 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

В зависимости от того, какие виды доступа вам требуются, для доступа к члену экипажа может потребоваться довольно медленный путь.

Но, еще лучшая кодировка могла бы заключаться в том, чтобы почти полностью исключить объекты из вашего мышления и охватить карту, которую вы будете использовать для поиска ключа, как части самой структуры. Я прошу прощения за грубую природу этого кода, я пишу его прямо сейчас.

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
2 голосов
/ 02 марта 2010

Haskell - отличный выбор для моделирования системы, которую вы описываете.

Однако, как и в любом другом языке программирования, способ моделирования вашей системы сильно зависит от того, какие операции вы хотите сделать на нем. И функциональный язык программирования, такой как Haskell, поможет вам сосредоточиться на этом. Моделирование данных - это хорошо, но где функции?

Ваши типы для скорой помощи, станции и экипажа просты и понятны. Я не уверен, почему вы хотите объединить их в один большой SystemState. Такая конструкция действительно полезна в определенных ситуациях. Не удивительно, что это немного усложнило для вас, хотя, потому что это что-то вроде ad hoc месива. Нужно это или нет полностью зависит от того, какие функции вы будете писать.

Но главная проблема здесь в том, как эффективно использовать GHCi.

Что именно вы пытаетесь сделать в GHCi? Я провожу много времени на GHCi подскажите. Я могу разделить это время на три категории: изучение функций чтобы лучше понять их, тестировать и отлаживать функции, чтобы убедиться, что они работают, и выполнение одноразовых расчетов с использованием функций, которые я уже понимаю и уже знаю работаем. Я не думаю, что я использовал GHCi очень много для просто набираю структуры данных и заставляю GHCi выплевывать их мне.

Тем не менее, для каждого из этих трех применений мне нужны структуры данных. Обычно те, которые мне нужны, достаточно просты, чтобы я мог введите всю вещь за один раз. Они на самом деле не должны быть очень простыми для это - не забывайте, что вы можете ввести несколько взаимно-рекурсивных определения в одном операторе let , разделяя их символом ';', и что GHCi поддерживает многострочные операторы с командами ": {" и ":} '.

Если нужная мне структура данных достаточно сложна, чтобы построить его постепенно, как вы делали, есть несколько простых способов сделать это.

Чтобы получить изменяемую переменную, которую вы неоднократно модифицируете для сборки до вашей структуры, так же, как вы это сделали бы в приглашение командной строки для обязательного языка, посмотрите на модуль Data.IORef. Если вы новичок в Haskell, я бы рекомендую избегать Data.IORef, как чумы в вашем программировании - это всегда соблазнит вас, и это почти всегда неправильно сделать. Но по приглашению GHCi все нормально.

По правде говоря, я почти никогда не делаю этого. Будучи ленивым, я просто использую стрелка вверх и другие клавиши редактирования командной строки, чтобы получить все это в одну команду GHCi постепенно.

И, конечно, если структура данных, которую вы вводите, на самом деле осмысленный, а не выкидной пример, вы захотите напечатать это в файл в вашем любимом редакторе Haskell, а не по подсказке. Тогда вы будете использовать интеграцию GHCi вашего редактора, или команду:: r GHCi, чтобы сохранить актуальную версию вашего структура доступна в GHCi.

1 голос
/ 02 марта 2010

Если вам действительно нужно, чтобы данные были такими рекурсивными, используйте подходящую библиотеку графов, например fgl.

1 голос
/ 02 марта 2010

Я тоже пытался делать подобные вещи, и пришел к выводу, что Хаскелл (для меня), вероятно, не был подходящим инструментом для работы.

Ваш вопрос 2 получает ответ:

множество ссылок на «одинаковые» данные есть отдельные копии.

Haskell, как язык, специально разработан для , чтобы затруднить "совместное использование экземпляров" или создание "отдельных копий". Поскольку все переменные содержат неизменяемые значения, отсутствуют ссылки на объекты для сравнения на предмет идентичности.

Тем не менее, есть некоторые методы.

Один из методов - использовать изменяемые объекты для ваших структур. Однако это заставит весь ваш код в монаду.

Вы также можете посмотреть этот документ Типобезопасный общий доступ к наблюдаемым , в котором показано, как использовать некоторые новые языковые функции, поддерживающие низкоуровневые ссылки, при создании графика. Их пример - цифровая схема, но я думаю, что она обобщает.

1 голос
/ 02 марта 2010

Есть несколько способов обойти это. Один простой способ - представить ваши данные как базу данных SQL. То есть ваши станции, машины скорой помощи и экипаж - это все таблицы со спутниковыми данными, связанными с ними. Другой вариант - определить его как базу данных графов с библиотекой графов.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...