property-list-0.1: src/Data/PropertyList/PropertyListItem.hs
{-# LANGUAGE
MultiParamTypeClasses,
FlexibleInstances,
TemplateHaskell, CPP,
ViewPatterns
#-}
module Data.PropertyList.PropertyListItem
(PropertyListItem(..)) where
import Language.Haskell.TH
import Language.Haskell.TH.Syntax
import Data.PropertyList.Algebra
import Data.PropertyList.Types
import qualified Data.Map as M
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as BL
import Data.Time
import Data.Char
import Data.Int
import qualified Data.Text as Text
import Data.Word
import Data.OneOfN
import Control.Monad
import Data.Functor.Identity
import qualified Data.Traversable as Traversable
import Data.Generics
-- * some local utility functions
-- try to read a string to some other format using the Read instance
-- and failing gracefully
tryRead :: Read a => String -> Maybe a
tryRead s = case reads s of
[(d, "")] -> Just d
_ -> Nothing
-- try to convert a fractional type to an integral type without loss of precision.
-- in case of loss of precision, the conversion fails.
tryToIntegral :: (RealFrac a, Integral b) => a -> Maybe b
tryToIntegral d = case properFraction d of
(i, 0) -> Just i
_ -> Nothing
toStrictByteString :: BL.ByteString -> BS.ByteString
toStrictByteString = BS.concat . BL.toChunks
toLazyByteString :: BS.ByteString -> BL.ByteString
toLazyByteString = BL.fromChunks . return
-- |A class for items which can be converted to and from property lists. This
-- is more general than 'PListAlgebra' and 'PListCoalgebra', in that it allows
-- for transformations that are not primitive-recursive. This relaxation is
-- necessary and desirable in the 'PropertyListItem' situation because we are
-- more interested in composable injection/projection operations on than in
-- universal maps.
--
-- The algebraic interface also cannot work for arrays or dictionaries,
-- because it only allows primitive (co-)recursion - the conversions can only
-- operate on one \"layer\" of 'PropertyListS' at a time. This could be
-- handled by enlarging the types (from [t] to Either t [t], for example)
-- or by encoding in-band (by taking a singleton list to be an element
-- instead of a list, for example), but both of those \"solutions\" create
-- headaches of their own, and in any case the algebraic interface is probably
-- too bizarre for most users.
class PropertyListItem i where
-- |Construct a 'PropertyList' from the item.
toPropertyList :: i -> PropertyList
-- |Convert a property list to a property list item if its contents
-- _exactly_ fit the target type. Note that when using types
-- such as 'M.Map' 'String' 'Int' (as opposed to 'M.Map' 'String'
-- 'PropertyList') this will mean that a single element of the
-- dictionary of a non-'Int' type will cause the entire conversion to
-- fail.
fromPropertyList :: PropertyList -> Maybe i
-- |In order to support a general instance for lists without breaking
-- String, we use the same trick as the Prelude uses for Show.
-- Generally, the list methods should not be overridden, and maybe
-- they shouldn't even be exported.
listToPropertyList :: [i] -> PropertyList
listToPropertyList = plArray . map toPropertyList
listFromPropertyList :: PropertyList -> Maybe [i]
listFromPropertyList (fromPlArray -> Just x) = mapM fromPropertyList x
listFromPropertyList _ = Nothing
instance PropertyListItem a => PropertyListItem [a] where
toPropertyList = listToPropertyList
fromPropertyList = listFromPropertyList
instance PropertyListItem PropertyList where
toPropertyList = id
fromPropertyList = Just
instance PropertyListItem BS.ByteString where
toPropertyList = plData
fromPropertyList (fromPlData -> Just x) = Just x
-- fromPropertyList (fromPlString -> Just x) = Just (toStrictByteString x)
fromPropertyList _ = Nothing
instance PropertyListItem BL.ByteString where
toPropertyList = plData . toStrictByteString
fromPropertyList (fromPlData -> Just x) = Just (toLazyByteString x)
-- fromPropertyList (fromPlString -> Just x) = Just (toLazyByteString x)
fromPropertyList _ = Nothing
instance PropertyListItem UTCTime where
toPropertyList = plDate
fromPropertyList (fromPlDate -> Just x) = Just x
fromPropertyList _ = Nothing
instance PropertyListItem a => PropertyListItem (M.Map String a) where
{-# SPECIALIZE instance PropertyListItem (M.Map String PropertyList) #-}
toPropertyList = plDict . fmap toPropertyList
fromPropertyList (fromPlDict -> Just x) = Traversable.mapM fromPropertyList x
fromPropertyList _ = Nothing
instance PropertyListItem Double where
toPropertyList = plReal
fromPropertyList (fromPlInt -> Just i) = Just (fromInteger i)
fromPropertyList (fromPlReal -> Just d) = Just d
fromPropertyList (fromPlString -> Just s) = tryRead s
fromPropertyList _ = Nothing
instance PropertyListItem Float where
toPropertyList = toPropertyList . (realToFrac :: Float -> Double)
fromPropertyList = fmap (realToFrac :: Double -> Float) . fromPropertyList
-- this little bit of Template Haskell replicates the embedded instance
-- to many other integral types (by using SYB generics to replace every
-- occurrence of ''Integer in the declarations' template-haskell representation
-- with each other type's name)
$( do
decls <- [d|
instance PropertyListItem Integer where
toPropertyList = plInt . fromIntegral
fromPropertyList pl = case runIdentity (plistCoalgebra pl) of
PLInt i -> Just (fromIntegral i)
PLReal d -> tryToIntegral d
PLString s -> tryRead s
_ -> Nothing
|]
sequence
[ everywhereM (mkM (return . replace)) dec
| t <- [''Integer, ''Int,
''Int8, ''Int16, ''Int32, ''Int64,
''Word8, ''Word16, ''Word32, ''Word64]
, dec <- decls
, let replace name
| name == ''Integer = t
| otherwise = name
]
)
-- this instance doesnt make much sense by itself, but must be here to support strings
instance PropertyListItem Char where
toPropertyList c = plString [c]
fromPropertyList (fromPlString -> Just [c]) = Just c
fromPropertyList _ = Nothing
listToPropertyList = plString
listFromPropertyList (fromPlString -> Just x) = Just x
-- listFromPropertyList (fromPlData -> Just x) = Just (fromStrictByteString x)
listFromPropertyList (fromPlBool -> Just True) = Just "YES"
listFromPropertyList (fromPlBool -> Just False) = Just "NO"
listFromPropertyList (fromPlInt -> Just i) = Just (show i)
listFromPropertyList (fromPlReal -> Just d) = Just (show d)
listFromPropertyList other = Nothing
instance PropertyListItem Text.Text where
toPropertyList = toPropertyList . Text.unpack
fromPropertyList = fmap Text.pack . fromPropertyList
instance PropertyListItem Bool where
toPropertyList = plBool
fromPropertyList (fromPlBool -> Just d) = Just d
fromPropertyList (fromPlString -> Just b)
| map toLower b `elem` ["yes", "true"]
= Just True
| map toLower b `elem` ["no", "false"]
= Just False
fromPropertyList _ = Nothing
-- The following TH generates, for Either and for all OneOfN types
-- (N in [2..20]), an instance of the form:
--
-- instance (PropertyListItem a, PropertyListItem b, PropertyListItem c) => PropertyListItem (OneOf3 a b c) where
-- toPropertyList = oneOf3 toPropertyList toPropertyList toPropertyList
-- fromPropertyList pl = msum [ fmap OneOf3 (fromPropertyList pl)
-- , fmap TwoOf3 (fromPropertyList pl)
-- , fmap ThreeOf3 (fromPropertyList pl)
-- ]
$( let types = ''Either : [mkTcName ("OneOf" ++ show n) | n <- [2..20]]
-- mkTcName ensures we have the type constructor and not the data constructor
-- by assembling it with its 'flavour' explicitly set to match that of a known
-- type constructor.
mkTcName n = Name (mkOccName n) nameFlavour
where Name _ nameFlavour = ''OneOf2
mkInstance typeName = do
TyConI (DataD _ _ _ cons _) <- reify typeName
let conNames = [name | NormalC name _ <- cons]
let tyVarNames = zipWith (\con n -> mkName ("a" ++ show n)) conNames [1..]
tyVars = map varT tyVarNames
typeWithVars = foldl appT (conT typeName) tyVars
#if defined(__GLASGOW_HASKELL__) && __GLASGOW_HASKELL__ >= 612
context = cxt [classP ''PropertyListItem [tyVar] | tyVar <- tyVars]
#else
context = cxt [appT (conT ''PropertyListItem) tyVar | tyVar <- tyVars]
#endif
inst = appT (conT ''PropertyListItem) typeWithVars
pl = mkName "pl"
whre =
[ funD 'toPropertyList [clause [] (normalB toPLbody ) []]
, funD 'fromPropertyList [clause [varP pl] (normalB fromPLbody) []]
]
lcFirst (c:cs) = toLower c : cs
fold = varE . mkName . lcFirst . nameBase
toPLbody = appsE (fold typeName : map (const (varE 'toPropertyList)) conNames)
fromPLbody = appE (varE 'msum) $ listE
[ [| fmap $(conE con) (fromPropertyList $(varE pl)) |]
| con <- conNames
]
instanceD context inst whre
in
mapM mkInstance types
)