40

関数型プログラムでリレーショナル データをモデル化することはよくあることです。たとえば、Web サイトを開発する場合、ユーザーに関する情報を格納するために、次のデータ構造が必要になる場合があります。

data User = User 
  { name :: String
  , birthDate :: Date
  }

次に、ユーザーがサイトに投稿したメッセージに関するデータを保存します。

data Message = Message
  { user :: User
  , timestamp :: Date
  , content :: String
  }

このデータ構造に関連する複数の問題があります。

  • 名前や生年月日が似ているユーザーを区別する方法はありません。
  • ユーザーデータはシリアライゼーション/デシリアライゼーションで複製されます
  • ユーザーを比較するには、データを比較する必要がありますが、これはコストのかかる操作になる可能性があります。
  • のフィールドへの更新は脆弱です。データ構造内Userのすべての の出現を更新するのを忘れる可能性があります。User

これらの問題は、データをツリーとして表すことができれば対処できます。たとえば、次のようにリファクタリングできます。

data User = User
  { name :: String
  , birthDate :: Date
  , messages :: [(String, Date)] -- you get the idea
  }

ただし、データを DAG (多対多の関係を想像してください) として、または一般的なグラフ (OK、そうではないかもしれません) として整形することは可能です。この場合、データをMapsに保存することで、リレーショナル データベースをシミュレートする傾向があります。

newtype Id a = Id Integer
type Table a = Map (Id a) a

この種の方法は機能しますが、複数の理由で安全ではなく醜いです:

  • あなたはId無意味なルックアップから離れた単なるコンストラクター呼び出しです。
  • ルックアップでは が得られますがMaybe a、多くの場合、データベースは構造的に値があることを保証します。
  • 不器用です。
  • データの参照整合性を確保するのは困難です。
  • インデックス (パフォーマンスに非常に必要) を管理し、その整合性を確保することは、さらに困難で扱いにくいものです。

これらの問題を克服するための既存の作業はありますか?

Template Haskell は (通常はそうであるように) それらを解決できるように見えますが、車輪の再発明はしたくありません。

4

5 に答える 5

29

ixsetライブラリ (または、ixset-typedよりタイプセーフなバージョン) がこれに役立ちます。のリレーショナル部分をサポートするライブラリでありacid-state、必要に応じて、データのバージョン管理されたシリアル化や同時実行性の保証も処理します。

Happstack Book にはIxSet のチュートリアルがあります。


重要なのixsetは、データ エントリの「キー」を自動的に管理することです。

あなたの例では、次のようなデータ型の 1 対多の関係を作成します。

data User =
  User
  { name :: String
  , birthDate :: Date
  } deriving (Ord, Typeable)

data Message =
  Message
  { user :: User
  , timestamp :: Date
  , content :: String
  } deriving (Ord, Typeable)

instance Indexable Message where
  empty = ixSet [ ixGen (Proxy :: Proxy User) ]

その後、特定のユーザーのメッセージを見つけることができます。このようなものを構築した場合IxSet

user1 = User "John Doe" undefined
user2 = User "John Smith" undefined

messageSet =
  foldr insert empty
  [ Message user1 undefined "bla"
  , Message user2 undefined "blu"
  ]

...その後、次の方法でメッセージを見つけることができますuser1

user1Messages = toList $ messageSet @= user1

メッセージのユーザーを見つける必要がある場合は、user通常どおり関数を使用してください。これは、1 対多の関係をモデル化します。

さて、多対多の関係の場合、次のような状況になります:

data User =
  User
  { name :: String
  , birthDate :: Date
  , messages :: [Message]
  } deriving (Ord, Typeable)

data Message =
  Message
  { users :: [User]
  , timestamp :: Date
  , content :: String
  } deriving (Ord, Typeable)

... でインデックスを作成しixFunます。これは、インデックスのリストで使用できます。そのようです:

instance Indexable Message where
  empty = ixSet [ ixFun users ]

instance Indexable User where
  empty = ixSet [ ixFun messages ]

ユーザーによるすべてのメッセージを見つけるには、同じ関数を使用します。

user1Messages = toList $ messageSet @= user1

さらに、ユーザーのインデックスがある場合:

userSet =
  foldr insert empty
  [ User "John Doe" undefined [ messageFoo, messageBar ]
  , User "John Smith" undefined [ messageBar ]
  ]

...メッセージのすべてのユーザーを見つけることができます:

messageFooUsers = toList $ userSet @= messageFoo

新しいユーザー/メッセージを追加するときに、メッセージのユーザーまたはユーザーのメッセージを更新する必要がない場合は、代わりに、SQL のように、ユーザーとメッセージ間の関係をモデル化する中間データ型を作成する必要があります。 (usersおよびフィールドを削除しmessagesます):

data UserMessage = UserMessage { umUser :: User, umMessage :: Message } 

instance Indexable UserMessage where
  empty = ixSet [ ixGen (Proxy :: Proxy User), ixGen (Proxy :: Proxy Message) ]

これらの関係のセットを作成すると、何も更新することなく、メッセージとユーザーへのメッセージでユーザーをクエリできます。

ライブラリは、その機能を考えると非常にシンプルなインターフェースを備えています!

編集:「比較する必要があるコストのかかるデータ」について:ixsetインデックスで指定したフィールドのみを比較します (したがって、最初の例でユーザーによるすべてのメッセージを見つけるには、「ユーザー全体」を比較します)。

インスタンスを変更することにより、インデックス付きフィールドのどの部分を比較するかを調整しOrdます。そのため、ユーザーの比較にコストがかかる場合は、たとえば、userIdフィールドを追加して、このフィールドのみを比較するように変更できます。instance Ord User

これは、ニワトリが先か卵が先かという問題を解決するためにも使用できUserますMessage

次に、id の明示的なインデックスを作成し、その id で ( を使用してuserSet @= (12423 :: Id)) ユーザーを検索し、検索を実行するだけです。

于 2012-02-10T21:07:15.810 に答える
6

Opaleye を使用して回答を書くように求められました。実際、データベース スキーマがあれば、Opaleye のコードはかなり標準的なものなので、多くのことを言う必要はありません。user_tableとにかく、 with columns user_idnameand birthdate、およびmessage_tablewith columns user_idtime_stampandがあると仮定すると、ここにありcontentます。

この種の設計については、 Opaleye 基本チュートリアルで詳しく説明されています。

{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE Arrows #-}

import Opaleye
import Data.Profunctor.Product (p2, p3)
import Data.Profunctor.Product.TH (makeAdaptorAndInstance)
import Control.Arrow (returnA)

data UserId a = UserId { unUserId :: a }
$(makeAdaptorAndInstance "pUserId" ''UserId)

data User' a b c = User { userId    :: a
                        , name      :: b
                        , birthDate :: c }
$(makeAdaptorAndInstance "pUser" ''User')

type User = User' (UserId (Column PGInt4))
                  (Column PGText)
                  (Column PGDate)

data Message' a b c = Message { user      :: a
                              , timestamp :: b
                              , content   :: c }
$(makeAdaptorAndInstance "pMessage" ''Message')

type Message = Message' (UserId (Column PGInt4))
                        (Column PGDate)
                        (Column PGText)


userTable :: Table User User
userTable = Table "user_table" (pUser User
  { userId    = pUserId (UserId (required "user_id"))
  , name      = required "name"
  , birthDate = required "birthdate" })

messageTable :: Table Message Message
messageTable = Table "message_table" (pMessage Message
  { user      = pUserId (UserId (required "user_id"))
  , timestamp = required "timestamp"
  , content   = required "content" })

user_idユーザー テーブルをフィールドのメッセージ テーブルに結合するクエリの例:

usersJoinMessages :: Query (User, Message)
usersJoinMessages = proc () -> do
  aUser    <- queryTable userTable    -< ()
  aMessage <- queryTable messageTable -< ()

  restrict -< unUserId (userId aUser) .== unUserId (user aMessage)

  returnA -< (aUser, aMessage)
于 2015-02-03T20:14:07.740 に答える
5

リレーショナル データを表すもう 1 つの根本的に異なるアプローチは、データベース パッケージhaskelldbで使用されます。例で説明した型とはまったく異なりますが、SQL クエリへの型安全なインターフェイスを許可するように設計されています。データベース スキーマからデータ型を生成するためのツールがあり、その逆も同様です。説明したようなデータ型は、常に行全体を操作したい場合にうまく機能します。ただし、特定の列のみを選択してクエリを最適化したい状況では機能しません。これは、HaskellDB アプローチが役立つ場所です。

于 2012-02-16T17:59:38.763 に答える
3

完全な解決策はありませんが、ixsetパッケージを確認することをお勧めします。ルックアップを実行できる任意の数のインデックスを持つセット型を提供します。(永続化のために酸状態で使用することを意図しています。)

各テーブルの「主キー」を手動で維持する必要はありますが、いくつかの方法で大幅に簡単にすることができます。

  1. に型パラメーターを追加しIdて、たとえば、 aUserId Userだけではなくを含めるようにしますIdIdこれにより、別々のタイプの s を混同しないことが保証されます。

  2. 型を抽象Id化し、何らかのコンテキストで新しい型を生成するための安全なインターフェースを提供します (関連するものと現在の最高のStateものを追跡するモナドなど)。IxSetId

  3. たとえば、クエリで が期待されるUser場所にを指定したり、不変条件を適用したりできるラッパー関数を作成します(たとえば、 everyが有効な へのキーを保持している場合、値を処理せずに対応するものを検索できるようになります。 「危険」はこのヘルパー関数に含まれています)。Id UserMessageUserUserMaybe

追加の注意として、通常のデータ型は任意のグラフを表すことができるため、実際にはツリー構造は必要ありません。ただし、これにより、ユーザー名の更新などの単純な操作が不可能になります。

于 2012-02-10T20:46:04.887 に答える