Safe modelling of relational data in Haskell

前端 未结 5 1796
醉梦人生
醉梦人生 2021-01-30 13:25

I find it very common to want to model relational data in my functional programs. For example, when developing a web-site I may want to have the following data structure to stor

5条回答
  •  醉酒成梦
    2021-01-30 13:49

    The ixset library will help you with this. It's the library that backs the relational part of acid-state, which also handles versioned serialization of your data and/or concurrency guarantees, in case you need it.

    The thing about ixset is that it manages "keys" for your data entries automatically.

    For your example, one would create one-to-many relationships for your data types like this:

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

    You can then find the message of a particular user. If you have built up an IxSet like this:

    user1 = User "John Doe" undefined
    user2 = User "John Smith" undefined
    
    messageSet =
      foldr insert empty
      [ Message user1 undefined "bla"
      , Message user2 undefined "blu"
      ]
    

    ... you can then find messages by user1 with:

    user1Messages = toList $ messageSet @= user1
    

    If you need to find the user of a message, just use the user function like normal. This models a one-to-many relationship.

    Now, for many-to-many relations, with a situation like this:

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

    ... you create an index with ixFun, which can be used with lists of indexes. Like so:

    instance Indexable Message where
      empty = ixSet [ ixFun users ]
    
    instance Indexable User where
      empty = ixSet [ ixFun messages ]
    

    To find all the messages by an user, you still use the same function:

    user1Messages = toList $ messageSet @= user1
    

    Additionally, provided that you have an index of users:

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

    ... you can find all the users for a message:

    messageFooUsers = toList $ userSet @= messageFoo
    

    If you don't want to have to update the users of a message or the messages of a user when adding a new user/message, you should instead create an intermediary data type that models the relation between users and messages, just like in SQL (and remove the users and messages fields):

    data UserMessage = UserMessage { umUser :: User, umMessage :: Message } 
    
    instance Indexable UserMessage where
      empty = ixSet [ ixGen (Proxy :: Proxy User), ixGen (Proxy :: Proxy Message) ]
    

    Creating a set of these relations would then let you query for users by messages and messages for users without having to update anything.

    The library has a very simple interface considering what it does!

    EDIT: Regarding your "costly data that needs to be compared": ixset only compares the fields that you specify in your index (so to find all the messages by a user in the first example, it compares "the whole user").

    You regulate which parts of the indexed field it compares by altering the Ord instance. So, if comparing users is costly for you, you can add an userId field and modify the instance Ord User to only compare this field, for example.

    This can also be used to solve the chicken-and-egg problem: what if you have an id, but neither a User, nor a Message?

    You could then simply create an explicit index for the id, find the user by that id (with userSet @= (12423 :: Id)) and then do the search.

提交回复
热议问题