----------------------------------------------------------------------------- -- | -- Module : Data.Metrology.TH -- Copyright : (C) 2014 Richard Eisenberg -- License : BSD-style (see LICENSE) -- Maintainer : Richard Eisenberg (rae@cs.brynmawr.edu) -- Stability : experimental -- Portability : non-portable -- -- This module exports Template Haskell functions to make working with -- @units@ a little more convenient. ----------------------------------------------------------------------------- {-# LANGUAGE TemplateHaskell, CPP #-} {-# OPTIONS_HADDOCK prune #-} module Data.Metrology.TH ( evalType, declareDimension, declareCanonicalUnit, declareDerivedUnit, declareMonoUnit, declareConstant, -- for internal use only checkIsType ) where import Language.Haskell.TH import Language.Haskell.TH.Desugar ( dsType, sweeten ) import Language.Haskell.TH.Desugar.Expand ( expandUnsoundly ) import Language.Haskell.TH.Desugar.Lift () -- need Lift Rational import Data.Metrology.Dimensions import Data.Metrology.Units import Data.Metrology.LCSU import Data.Metrology.Poly -- | "Evaluates" a type as far as it can. This is useful, say, in instance -- declarations: -- -- > instance Show $(evalType [t| Length |]) where ... -- -- Without the 'evalType', the instance declaration fails because @Length@ -- mentions type families, which can't be used in instance declarations. -- -- This function is somewhat experimental, and will likely not work with -- more polymorphic types. (If it doesn't work, not all of the type families -- will be evaluated, and the instance declaration will fail. This function -- should never cause /incorrect/ behavior.) -- evalType :: Q Type -> Q Type evalType qty = do ty <- qty dty <- dsType ty ex_dty <- expandUnsoundly dty -- NB: No units type families branch on kind variables, so this is safe here. return $ sweeten ex_dty -- Checks to make sure the given name names a /type/, not a /data constructor/. -- Reports a compile-time error if the name is not a type. checkIsType :: Name -> Q () checkIsType n = do info <- reify n case info of ClassOpI {} -> generic_error DataConI {} -> datacon_error VarI {} -> generic_error _ -> return () where generic_error = reportError $ "The name " ++ show n ++ " does not describe a type.\n A type is expected here." datacon_error = reportError $ "The name " ++ show n ++ " describes a data constructor.\n Did you perhaps mean to say ''" ++ nameBase n ++ "? Note the two quotes." -- | Declare a new dimension of the given name: -- -- > $(declareDimension "Length") -- -- produces -- -- > data Length = Length -- > instance Dimension Length declareDimension :: String -> Q [Dec] declareDimension str = return [ mkEmptyDataD name #if __GLASGOW_HASKELL__ >= 711 , InstanceD Nothing [] (ConT ''Dimension `AppT` ConT name) [] #else , InstanceD [] (ConT ''Dimension `AppT` ConT name) [] #endif ] where name = mkName str -- | Conditionally generates a @Show@ instance maybeMkShowInstance :: Name -> Maybe String -> Q [Dec] maybeMkShowInstance name (Just abbrev) = [d| instance Show $(return $ ConT name) where { show _ = abbrev } |] maybeMkShowInstance _ Nothing = return [] -- | @declareCanonicalUnit unit_name dim (Just abbrev)@ creates a new -- canonical unit (that is, it is not defined in terms of other known units) -- named @unit_name@, measuring dimension @dim@. @abbrev@ will be the -- abbreviation in the unit's @Show@ instance. If no abbraviation is supplied, -- then no @Show@ instance will be generated. -- -- Example usage: -- -- > $(declareCanonicalUnit "Meter" [t| Length |] (Just "m")) declareCanonicalUnit :: String -> Q Type -> Maybe String -> Q [Dec] declareCanonicalUnit unit_name_str dim m_abbrev = do show_instance <- maybeMkShowInstance unit_name m_abbrev unit_instance <- [d| instance Unit $unit_type where type BaseUnit $unit_type = Canonical type DimOfUnit $unit_type = $dim |] return $ (mkEmptyDataD unit_name) : unit_instance ++ show_instance where unit_name = mkName unit_name_str unit_type = return $ ConT unit_name -- | @declareDerivedUnit unit_name base_unit_type ratio (Just abbrev)@ creates -- a new derived unit, expressed in terms of @base_unit_type@. @ratio@ says -- how many base units are in the derived unit. (Thus, if @unit_name@ is -- @"Minute"@ and @base_unit_type@ is @''Second@, then @ratio@ would be @60@.) -- @abbrev@, if supplied, becomes the string produced in the derived unit's -- @Show@ instance. If no abbreviation is supplied, no @Show@ instance is -- generated. -- -- Example usage: -- -- > $(declareDerivedUnit "Minute" [t| Second |] 60 (Just "min")) declareDerivedUnit :: String -> Q Type -> Rational -> Maybe String -> Q [Dec] declareDerivedUnit unit_name_str base_unit ratio m_abbrev = do show_instance <- maybeMkShowInstance unit_name m_abbrev unit_instance <- [d| instance Unit $unit_type where type BaseUnit $unit_type = $base_unit conversionRatio _ = ratio |] return $ (mkEmptyDataD unit_name) : unit_instance ++ show_instance where unit_name = mkName unit_name_str unit_type = return $ ConT unit_name -- | @declareMonoUnit unit_name (Just abbrev)@ creates a new derived unit, -- intended for use without unit polymorphism. The same type stands for both -- the unit and dimension, and the instance of 'DefaultUnitOfDim' is set up -- accordingly. Use this function (with the 'Data.Metrology' imports) if you -- don't want to bother with LCSUs and just want to get to work. The @abbrev@, -- if supplied, creates an appropriate @Show@ instance. -- -- > $(declareMonoUnit "Meter" (Just "m")) -- -- produces all of the following -- -- > data Meter = Meter -- > instance Dimension Meter -- > instance Unit Meter where -- > type BaseUnit Meter = Canonical -- > type DimOfUnit Meter = Meter -- > type instance DefaultUnitOfDim Meter = Meter -- > instance Show Meter where -- > show _ = "m" -- -- After a declaration like this, you probably want -- -- > type Length = MkQu_U Meter -- -- This last line is /not/ generated, as it is easy enough for you to write, -- and it involves a new name (@Length@). declareMonoUnit :: String -> Maybe String -> Q [Dec] declareMonoUnit unit_name_str m_abbrev = do show_instance <- maybeMkShowInstance unit_name m_abbrev dim_instance <- [d| instance Dimension $unit_type |] unit_instance <- [d| instance Unit $unit_type where type BaseUnit $unit_type = Canonical type DimOfUnit $unit_type = $unit_type |] default_instance <- [d| type instance DefaultUnitOfDim $unit_type = $unit_type |] return $ (mkEmptyDataD unit_name) : show_instance ++ dim_instance ++ unit_instance ++ default_instance where unit_name = mkName unit_name_str unit_type = return $ ConT unit_name -- | @declareConstant const_name value unit_type@ creates a new numerical -- constant, named @const_name@. Its numerical value is @value@ expressed -- in units given by @unit_type@. The constant is polymorphic in both its -- LCSU and numerical representation. For example, -- -- > declareConstant "gravity_g" 9.80665 [t| Meter :/ Second :^ Two |] -- -- yields -- -- > gravity_g :: ( Fractional n -- > , CompatibleUnit lcsu (Meter :/ Second :^ Two) ) -- > => MkQu_ULN (Meter :/ Second :^ Two) lcsu n -- > gravity_g = 9.80665 % (undefined :: Meter :/ Second :^ Two) declareConstant :: String -> Rational -> Q Type -> Q [Dec] declareConstant name value q_unit_type = do unit_type <- q_unit_type lcsu_name <- newName "lcsu" n_name <- newName "n" let lcsu = VarT lcsu_name n = VarT n_name const_name = mkName name const_type = ForallT [PlainTV lcsu_name, PlainTV n_name] [ mkClassP ''Fractional [n] , mkClassP ''CompatibleUnit [lcsu, unit_type] ] $ ConT ''MkQu_ULN `AppT` unit_type `AppT` lcsu `AppT` n ty_sig = SigD const_name const_type dec = ValD (VarP const_name) (NormalB $ VarE '(%) `AppE` LitE (RationalL value) `AppE` SigE (VarE 'undefined) unit_type) [] return [ty_sig, dec] where #if __GLASGOW_HASKELL__ < 709 mkClassP = ClassP #else mkClassP n tys = foldl AppT (ConT n) tys #endif -- Make a DataD like `data <name> = <name>`. mkEmptyDataD :: Name -> Dec mkEmptyDataD name #if __GLASGOW_HASKELL__ >= 801 = DataD [] name [] Nothing [con] [] #elif __GLASGOW_HASKELL__ >= 711 = DataD [] name [] Nothing [con] [] #else = DataD [] name [] [con] [] #endif where con = NormalC name []