Reputation: 76
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
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
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