Simon
Simon

Reputation: 76

How to handle multiple but similar record types

So let's say I have the following nearly identical type declarations. The types are related to each other in that MyType2 is a "processed" version of MyType1.

data MyType1 = MyType1 {
   field1 :: Maybe Text
   manyOtherFields :: Maybe Whatever
}

data MyType2 = MyType2 {
  field1 :: Text,
  manyOtherFields :: Whatever
}

Initially field1 is a Maybe because the data is coming from user input. Once processed however it becomes a Just value.

This pattern of processing Maybes into Justs is likely to be repeated 10's or even 100's of times throughout my program with potentially many similar combinations describing variations of essentially the same entity at different processing stages.

How do I avoid duplicating my type definitions for all the similar combinations ?

Further explanation:

In my actual program, the problem is accepting a file as input from a web form. When the form is received by my code the file input field is a Maybe FilePath, so I have a data type like:

data Media = Media {
  filePath :: Maybe FilePath
  altText :: Text,
}

Once the input has been processed I'd need a new data type:

data Media2 = Media2 {
  filePath :: FilePath
  altText :: Text,
  height :: Int,
  width :: Int
}

This seems ugly and impractical since similar patterns will be repeated over and over in my program. It's quite possible I'll need a Media3 (and 4) not to mention all the other entities and their variations.

Upvotes: 2

Views: 114

Answers (2)

Daniel Wagner
Daniel Wagner

Reputation: 152707

You give three data types in your gist:

data Media = Media {
    mediaId :: Int
  , mediaName :: Text
  , mediaFilePath :: FilePath
  , mediaMimeType :: Text
  , mediaHash :: Text
  , mediaWidth :: Int
  , mediaHeight :: Int
  , mediaCreated :: UTCTime
  , mediaUpdated :: UTCTime
}
data Media2 = Media2 {
    mediaId :: Int
  , mediaName :: Text
  , mediaFilePath :: Maybe FilePath
  , mediaMimeType :: Text
  , mediaHash :: Text
  , mediaWidth :: Int
  , mediaHeight :: Int
  , mediaCreated :: UTCTime
  , mediaUpdated :: UTCTime
}
data Media3= Media3 {
    media3Id :: Int
  , media3Name :: Text
  , media3FilePath :: FilePath
  , media3NewFilePath :: Maybe FilePath
  , media3MimeType :: Text
  , media3Hash :: Text
  , media3Width :: Int
  , media3Height :: Int
  , media3Created :: UTCTime
  , media3Updated :: UTCTime
}

...and complain that these violate the DRY principle (and I agree!). One simple solution is to split out the shared parts, thus:

data Metadata = Metadata
  { id :: Int
  , name :: Text
  , mimeType :: Text
  , hash :: Text
  , width :: Int
  , height :: Int
  , created :: UTCTime
  , updated :: UTCTime
  }

Then you have a few options for parameterizing the remaining bits. One choice is to have type modifiers; for example:

data Located   a = Located   { location :: FilePath, locatedValue :: a }
data Motion    a = Motion    { oldLocation, newLocation :: FilePath, motionValue :: a }
data UILocated a = UILocated { uiField :: Maybe FilePath, uilocatedValue :: a }

so that the old Media type, for example, would now be a Located Metadata. Another choice would be to have a sum type for locations:

data Location
    = OnDisk FilePath
    | Nowhere
    | Moving FilePath FilePath

Then you could use (Metadata, Location) as your type for all three, or put the location in a field of Metadata. This loses some static checking, but may be convenient in some situations.

Yet a third option is to add a polymorphic field to the metadata type:

data Metadata a = Metadata
  { id :: Int
  , name :: Text
  , mimeType :: Text
  , hash :: Text
  , width :: Int
  , height :: Int
  , created :: UTCTime
  , updated :: UTCTime
  , extra :: a
  }

so that your old Media type, for example, would now be Metadata FilePath, and Media3 would be Metadata (FilePath, Maybe FilePath).

Upvotes: 4

chi
chi

Reputation: 116139

I forgot the name of this technique... however, here it is:

import Data.Maybe
import Data.Functor.Identity

data MyType f = MyType
   { field1 :: f Text
   , manyOtherFields :: f Whatever
   }
type MyType1 = MyType Maybe
type MyType2 = MyType Identity

The price to pay is to have an Identity constructor wrapping the data when it's fully processed.

For instance:

x :: MyType1  -- partially processed
x = MyType{field1 = Nothing, manyOtherFields = Just whatever}

y :: MyType2  -- fully processed
y = MyType{field1 = Identity someText, manyOtherFields = Identity whatever}

Upvotes: 4

Related Questions