Search code examples
haskellservant

Is there an idiomatic way to do deal with this situation when two structures share some content?


I'm making a toy forum to gain familiarity with Haskell and Servant.

My API looks something like this:

type UserAPI = "messages" :> ReqBody '[JSON] Msg :> Header "X-Real-IP" String :> Post '[JSON] APIMessage
               :<|> "messages" :> ReqBody '[JSON] Int :> Get '[JSON] [Msg']

My types look something like this:

data Msg = Msg
  { thread :: Int
  , dname :: String
  , contents :: String
  } deriving (Eq, Show, Generic)
data Msg' = Msg'
  { thread' :: Int
  , stamp' :: UTCTime
  , dname' :: String
  , contents' :: String
  , ip' :: String
  } deriving (Eq, Show, Generic)

and they derive ToJSON / FromJSON / FromRow instances, which is very convenient.

Msg represents the data the API expects when receiving messages and Msg' the data it sends when queried for messages, which has two additional fields that are added by the server, but this doesn't feel right and there has to be a cleaner way to achieve this.

Any insight on an idiomatic way to do deal with this sort of problem appreciated.


Solution

  • I will consider here that you question is more a conceptual one ("What can I do when I have two data types that share some structure ?") than a simple "How do I model inheritance in Haskell ?" that is already replied here.

    To answer your question, you will need to consider more than just the structure of your data. For example, if I provide you A and B and if I state that

    data A = A Int String
    data B = B Int 
    

    I doubt that you will automatically make the assumption that a A is a B with an extra String. You will probably try to figure the exact relation between these two data structure. And this is the good thing to do.

    If each instance of A can actually be seen as an instance of B then it can be relevant to provide way to represent it in your code. Then you could use a plain Haskell way with a

    data A = A { super :: B, theString :: String }
    data B = B { id :: Int }
    

    Obviously, this will not be easy to work with these datatype without creating some other functions. For example a fromB function could be relevant

    fromB :: B -> String -> A 
    toB   :: A -> B
    

    And you can also use typeclass to access id

    class HasId a where
        getId :: a -> Int
    
    instance HasId A where
        getId = id . super 
    

    This is where some help form Lens can be useful. And the answer to this question How do I model inheritance in Haskell? is a good start. Lens package provides Object Oriented syntactic sugar to handle inheritance relationship.

    However you can also find that a A is not exactly a B but they both share the same ancestor. And you could prefer to create something like

    data A = A { share :: C, theString :: String }
    data B = B { share :: C }
    data C = C Int
    

    This is the case when you do not want to use a A as a B, but it exists some function that can be used by both. The implementation will be near the previous cases, so I do not explain it.

    Finally you could find that there does not really exists relation that can be useful (and, therefore, no function that will really exists that is shared between A and B). Then you would prefer to keep your code.

    In your specific case, I think that there is not a direct "is a" relation between Msg and Msg' since one is for the receiving and the other is for the sending. But they could share a common ancestor since both are messages. So they will probably have some constructors in common and accessors (in term of OO programming).

    Try to never forget that structure is always bind to some functions. And what category theory teaches us is that you cannot only look at the structures only but you have to consider their functions also to see the relation between each other.