Подтвердить что ты не робот

Совет, определяющий структуру данных в Haskell

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

В SQL я могу сделать:

create table cages (id integer primary key, volume double);
create table experiments (id integer primary key, notes text)
create table rats (
    weight double,
    cage_id integer references cages (id),
    experiment_id integer references experiments (id)
);

(я понимаю, что это позволяет мне назначать двух крыс из разных эксперименты в одну клетку. Это предназначено. Я фактически не запускаю исследовательский центр для животных).

Две операции, которые должны быть возможны: (1) с учетом крысы, найдите объем ее клетки и (2), учитывая крысу, получите примечания к эксперименту, к которому он принадлежит.

В SQL это будут

select cages.volume from rats
  inner join cages on cages.id = rats.cage_id
  where rats.id = ...; -- (1)
select experiments.notes from rats
  inner join experiments on experiments.id = rats.experiment_id
  where rats.id = ...; -- (2)

Как я могу моделировать эту структуру данных в Haskell?


Один из способов сделать это -

type Weight = Double
type Volume = Double

data Rat = Rat Cage Experiment Weight
data Cage = Cage Volume
data Experiment = Experiment String

data ResearchFacility = ResearchFacility [Rat]

ratCageVolume :: Rat -> Volume
ratCageVolume (Rat (Cage volume) _ _) = volume

ratExperimentNotes :: Rat -> String
ratExperimentNotes (Rat _ (Experiment notes) _) = notes

Но разве эта структура не представила бы кучу копий Cage и Experiment s? Или я просто не беспокоюсь об этом и надеюсь, что оптимизатор позаботится об этом?

4b9b3361

Ответ 1

Вот короткий файл, который я использовал для тестирования:

type Weight = Double
type Volume = Double

data Rat = Rat Cage Experiment Weight deriving (Eq, Ord, Show, Read)
data Cage = Cage Volume               deriving (Eq, Ord, Show, Read)
data Experiment = Experiment String   deriving (Eq, Ord, Show, Read)

volume     = 30
name       = "foo"
weight     = 15
cage       = Cage volume
experiment = Experiment name
rat        = Rat cage experiment weight

Затем я начал ghci и импортировал System.Vacuum.Cairo, доступный из восхитительного пакета vacuum-cairo.

*Main System.Vacuum.Cairo> view (rat, Rat (Cage 30) (Experiment "foo") 15)

not-shared

*Main System.Vacuum.Cairo> view (rat, Rat (Cage 30) experiment 15)

shared-experiment

(Я не совсем уверен, почему в этом есть стрелки в два раза, но вы можете игнорировать/свернуть их.)

*Main System.Vacuum.Cairo> view (rat, Rat cage experiment weight)

shared-args

*Main System.Vacuum.Cairo> view (rat, rat)

shared-all

*Main System.Vacuum.Cairo> view (rat, Rat cage experiment (weight+1))

shared-modified

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

Ответ 2

Более естественное представление Haskell вашей модели было бы для клеток содержать реальные объекты крысы вместо их идентификаторов:

data Rat = Rat RatId Weight
data Cage = Cage [Rat] Volume
data Experiment = Experiment [Rat] String

Затем вы создадите объекты ResearchFacility с помощью интеллектуального конструктора, чтобы убедиться, что они следуют правилам. Он может выглядеть примерно так:

research_facility :: [Rat] -> Map Rat Cage -> Map Rat Experiment -> ResearchFacility
research_facility rats cage_assign experiment_assign = ...

где cage_assign и experiment_assign - это карты, которые содержат ту же информацию, что и внешние ключи cage_id и experiment_id в sql.

Ответ 3

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

data Rat = Rat { getCage       :: Cage
               , getExperiment :: Experiment
               , getWeight     :: Weight }

data Cage = Cage { getVolume :: Volume }

-- Now this function is so trivial to define that you might actually not bother:
ratCageVolume :: Rat -> Volume
ratCageVolume = getVolume . getCage

А что касается представления данных, я могу пойти куда-то по этим строкам:

type Weight = Double
type Volume = Double

-- Rats and Cages have identity that goes beyond their properties;
-- two distinct rats of the same weight can be in the same cage, and
-- two cages can have same volume.
-- 
-- So should we give each Rat and Cage an additional field to
-- represent its key?  We could do that, or we could abstract that out
-- into this:

data Identity i a = Identity { getId  :: i
                             , getVal :: a }
            deriving Show

instance Eq i => Eq (Identity i a) where
    a == b = getId a == getId b

instance Ord i => Ord (Identity i a) where
    a `compare` b = getId a `compare` getId b


-- And to simplify a common case:
type Id a = Identity Int a


-- Rats' only real intrinsic property is their weight.  Cage and Experiment?
-- Situational, I say.
data Rat = Rat { getWeight :: Weight  }

data Cage = Cage { getVolume :: Volume }

data Experiment = Experiment { getNotes :: String }
                  deriving (Eq, Show)

-- The data that you're manipulating is really this:
type RatData = (Id Rat, Id Cage, Id Experiment)

type ResearchFacility = [RatData]

Ответ 4

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

getCageById       :: IdType -> IO (Maybe Cage)
getRatById        :: IdType -> IO (Maybe Rat)
getExperimentById :: IdType -> IO (Maybe Experiment)

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

data Rat = Rat Cage Experiment Weight

... тогда функция getRatById должна запускать три запроса базы данных, чтобы вернуть результат. Это казалось приятным удобным способом делать вещи вначале, но в конечном итоге это была огромная проблема с производительностью, особенно если мы хотели, чтобы запрос возвращал кучу результатов. Структура данных ПРЕДЛАГАЕТ нам сделать соединение, даже если нам нужна только строка из таблицы крыс. Дополнительные запросы к базе данных - проблема, а не возможность дополнительных объектов в ОЗУ.

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

type IdType = Int
type Weight = Double
type Volume = Double

data Rat = Rat
    { ratId        :: IdType
    , cageId       :: IdType
    , experimentId :: IdType
    , weight       :: Weight
    }
data Cage = Cage IdType Volume
data Experiment = Experiment IdType String

(Возможно, вы даже захотите использовать newtypes для различения разных идентификаторов.) Это больше работает, чтобы получить всю структуру, но это позволяет эффективно получать части структуры. Конечно, если вам никогда не нужно получать отдельные части структуры, тогда мой совет может оказаться неприемлемым. Но мой опыт заключается в том, что частичные запросы очень распространены, и я не хочу делать их искусственно медленными. Если вам нужна удобная функция, которая выполняет соединение для вас, вы можете, конечно, написать ее. Но не используйте модель данных, которая блокирует вас в этом шаблоне использования.