17

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)
);

(これにより、異なる実験からの2匹のラットを同じケージに割り当てることができることを認識しています.これは意図されています.私は実際に動物研究施設を運営していません.)

可能でなければならない 2 つの操作: (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 でモデル化するにはどうすればよいでしょうか?


それを行う1つの方法は

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しかし、この構造ではs とsのコピーが大量に導入されるのではないでしょうExperimentか? それとも、それについて心配する必要はなく、オプティマイザーがそれを処理してくれることを願っていますか?

4

4 に答える 4

16

テストに使用した短いファイルは次のとおりです。

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

System.Vacuum.Cairo次に、楽しいvacuum-cairoパッケージから入手できるghci と import を開始しました。

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

非共有

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

共有実験

(なぜ二重矢印があるのか​​よくわかりませんが、無視/折りたたむことができます。)

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

共有引数

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

すべて共有

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

共有変更

上で説明したように、経験則では、コンストラクターを呼び出したときに新しいオブジェクトが作成されます。それ以外の場合、作成済みのオブジェクトに名前を付けるだけでは、新しいオブジェクトは作成されません。Haskell はイミュータブルな言語なので、これは安全に行うことができます。

于 2012-08-06T20:20:39.073 に答える
6

モデルのより自然なHaskell表現は、ケージにIDの代わりに実際のラットオブジェクトが含まれるようにすることです。

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およびはsqlのおよび外部キーとexperiment_assign同じ情報を含むマップです。cage_idexperiment_id

于 2012-08-06T18:59:02.263 に答える
4

最初の観察: レコードの使い方を学ぶ必要があります。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]
于 2012-08-06T23:59:38.087 に答える
3

私は日常の仕事でほとんどの場合 Haskell を使用しており、この問題に遭遇しました。私の経験では、データ構造のコピーがいくつ作成されるかという問題ではなく、関連するデータの依存関係が問題になります。同様のデータ構造を使用して、実際のデータが格納されているリレーショナル データベースとのインターフェイスを支援していました。つまり、このようなクエリがあったということです。

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

私たちは、あなたのデータ構造と同じように構築されたデータ構造から始め、その中に含まれるリンクされたデータ構造を使用しました。これは大きな間違いであることが判明しました。問題は、Rat に次の定義を使用すると...

data Rat = Rat Cage Experiment Weight

...次に、getRatById 関数は、結果を返すために 3 つのデータベース クエリを実行する必要があります。これは最初は便利な方法のように思えましたが、最終的には大きなパフォーマンスの問題になりました。特にクエリで大量の結果を返す必要がある場合はなおさらです。データ構造では、rat テーブルの行のみが必要な場合でも、結合を行う必要があります。余分なデータベース クエリが問題であり、RAM に余分なオブジェクトが存在する可能性はありません。

現在、私たちのポリシーは、データベース テーブルに対応することを意図したデータ構造を作成するときは、常にテーブルと同じように非正規化することです。したがって、あなたの例は次のようになります。

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

(異なる ID を区別するために newtype を使用することもできます。) 構造全体を取得するのは手間がかかりますが、構造の一部を効率的に取得できます。もちろん、構造の個々の部分を取得する必要がない場合、私のアドバイスは適切ではないかもしれません。しかし、私の経験では、部分クエリは非常に一般的であり、人為的に遅くしたくありません。結合を行う便利な関数が必要な場合は、確かに作成できます。ただし、この使用パターンに縛られるようなデータ モデルは使用しないでください。

于 2012-08-06T22:13:16.503 に答える