convert-units-0: src/Data/Units/Base/TH.hs
{-# LANGUAGE TemplateHaskell #-}
--------------------------------------------------------------------------------
-- |
--
-- Module : Data.Units.Base.TH
-- Description : Template Haskell quasi quoter for unit declaration
-- Copyright : (c) Alice Rixte 2025
-- License : BSD 3
-- Maintainer : alice.rixte@u-bordeaux.fr
-- Stability : unstable
-- Portability : non-portable (GHC extensions)
--
--------------------------------------------------------------------------------
module Data.Units.Base.TH
( -- * Units
mkUnit
, mkUnitNoFactor
, mkBaseUnit
-- * Dimensions
, mkDim
-- * Prefixes
, mkPrefix
)
where
import GHC.TypeError
import Language.Haskell.TH
import Data.Type.Int
import Data.Units.Base.System
import Data.Units.Base.Convert
import Data.Units.Base.Prefix
------------------------------------ Units -------------------------------------
-- | List of derived class for a unit.
--
deriveList :: [Name]
deriveList =
[''Eq
, ''Ord
, ''Num
, ''Fractional
, ''Floating
, ''Real
, ''RealFrac
, ''RealFloat
]
-- | List of derived classes for a unit.
--
deriveListUnit :: [Name]
deriveListUnit = ''Show : deriveList
-- | Make a newtype of the form
--
-- @
-- newtype Minute a = Minute a
-- deriving ( Show, Eq, Ord, Num, Fractional, Floating, Real
-- , RealFrac, RealFloat)
-- @
--
mkUnitNewtype :: Quote m
=> [Name] -> Name -> m Dec
mkUnitNewtype l unitName =
let a = varT (mkName "a") in
newtypeD
(cxt [])
unitName
[PlainTV (mkName "a") BndrReq]
Nothing
(normalC unitName
[bangType (bang noSourceUnpackedness noSourceStrictness) a])
[ derivClause Nothing (conT <$> l)]
-- | Make instance of the form
--
-- @
-- instance IsUnit Hour where
-- type DimOf Hour = Time
-- @
--
mkIsUnitInstance :: Quote m => Name -> Name -> m [Dec]
mkIsUnitInstance unitName dimName = [d|
instance IsUnit $(conT unitName) where
type DimOf $(conT unitName) = $(conT dimName)
|]
-- | Make an instance of the form
--
-- @
-- instance ShowUnit Minute where
-- type ShowUnitType Minute = Text "min"
-- showUnit = "Minute"
-- prettyUnit = "min"
-- @
--
mkShowUnitInstance :: Quote m => Name -> String -> String -> m [Dec]
mkShowUnitInstance unitName unitStr prettyStr = [d|
instance ShowUnit $(conT unitName) where
type ShowUnitType $(conT unitName) = Text $(pure (LitT (StrTyLit prettyStr)))
showUnit = $(litE (StringL unitStr))
prettyUnit = $(litE (StringL prettyStr))
|]
-- | Make an instance of the form
--
-- @
-- instance Fractional a => ConversionFactor Minute a where
-- factor = 60
-- @
--
mkConvFactorInstance :: Quote m => Name -> Rational -> m [Dec]
mkConvFactorInstance unitName fctr = [d|
instance Fractional a => ConversionFactor $(conT unitName) a where
factor = $(litE (RationalL fctr))
|]
-- | Make an instance of the form
--
-- @
-- instance Fractional a => ConvertibleUnit Minute a
-- @
--
mkDefaultSigConvertibleInstance :: Quote m => Name -> m [Dec]
mkDefaultSigConvertibleInstance unitName = [d|
instance Fractional a => ConvertibleUnit $(conT unitName) a
|]
-- | Make an instance of the form
--
-- @
-- instance Fractional a => ConvertibleUnit Meter a where
-- toBaseUnit = coerce
-- {-# INLINE toBaseUnit #-}
-- fromBaseUnit = coerce
-- {-# INLINE fromBaseUnit #-}
-- @
--
mkNormalConvertibleInstance :: Quote m => Name -> m [Dec]
mkNormalConvertibleInstance unitName = [d|
instance Fractional a => ConvertibleUnit $(conT unitName) a where
toBaseUnit = coerce
{-# INLINE toBaseUnit #-}
fromBaseUnit = coerce
{-# INLINE fromBaseUnit #-}
|]
-- | Make a unit that can be converted via a factor
--
-- [Usage:]
--
-- @
-- \$(mkUnit "Minute" "min" ''Time 60)
-- @
--
mkUnit :: String -> String -> Name -> Rational -> Q [Dec]
mkUnit unitStr prettyStr dimName fctr = do
let unitName = mkName unitStr
newtypeDec <- mkUnitNewtype deriveListUnit unitName
isUnitDec <- mkIsUnitInstance unitName dimName
showUnitDec <- mkShowUnitInstance unitName unitStr prettyStr
convFactorDec <- mkConvFactorInstance unitName fctr
convUnitDec <-
if fctr == 1 then
mkNormalConvertibleInstance unitName
else
mkDefaultSigConvertibleInstance unitName
return $
[newtypeDec] ++ isUnitDec ++ showUnitDec ++ convFactorDec ++ convUnitDec
-- | Make a unit without declaring any conversion instances.
--
-- Conversion instances must be added by hand.
--
-- [Usage:]
--
-- @
-- \$(mkUnit \"Bel\" "B" ''NoUnit )
-- @
--
mkUnitNoFactor :: String -> String -> Name -> Q [Dec]
mkUnitNoFactor unitStr prettyStr dimName = do
let unitName = mkName unitStr
newtypeDec <- mkUnitNewtype deriveListUnit unitName
isUnitDec <- mkIsUnitInstance unitName dimName
showUnitDec <- mkShowUnitInstance unitName unitStr prettyStr
return $ [newtypeDec] ++ isUnitDec ++ showUnitDec
-- | Make a base unit.
--
-- In addition to calling 'mkUnit' with factor 1, this also makes an
-- instance of 'IsDim', which cannot be done in 'mkDim' since the base unit is
-- not yet declared.
--
-- [Usage:]
--
-- @ \$(mkBaseUnit "Second" "s" ''Time ) @
--
mkBaseUnit :: String -> String -> Name -> Q [Dec]
mkBaseUnit unitStr prettyStr dimName = do
let unitName = mkName unitStr
unitDec <- mkUnit unitStr prettyStr dimName 1
isDimDec <- mkIsDimInstance dimName unitName
return $ unitDec ++ isDimDec
---------------------------------- Dimensions ----------------------------------
-- | List of derived classes for a dimension.
--
deriveListDim :: [Name]
deriveListDim = deriveListUnit
mkDimNewtype :: Quote m
=> [Name] -> Name -> m Dec
mkDimNewtype = mkUnitNewtype
-- | Make an instance of the form
--
-- @
-- instance IsDim Time where
-- type DimToUnit Time = Second
-- @
--
mkIsDimInstance :: Quote m => Name -> Name -> m [Dec]
mkIsDimInstance dimName unitName = [d|
instance IsDim $(conT dimName) where
type DimToUnit $(conT dimName) = $(conT unitName)
|]
-- | Make a type instance of the form
--
-- @
-- type instance DimId Time = 400
-- @
--
mkDimIdTypeInstance :: Quote m => Name -> Integer -> m [Dec]
mkDimIdTypeInstance dimName n = [d|
type instance DimId $(conT dimName) = Pos $(litT (numTyLit n))
|]
-- | Make a type instance of the form
--
-- @
-- instance ShowDim Minute where
-- type ShowDimType Minute = Text "min"
-- showDim = "Minute"
-- prettyDim = "min"
-- @
--
mkShowDimInstance :: Quote m => Name -> String -> String -> m [Dec]
mkShowDimInstance dimName dimStr prettyStr = [d|
instance ShowDim $(conT dimName) where
type ShowDimType $(conT dimName) = Text $(pure (LitT (StrTyLit prettyStr)))
showDim = $(litE (StringL dimStr))
prettyDim = $(litE (StringL prettyStr))
|]
-- | Make a dimension.
--
-- This will not declare an instance for 'IsDim', which is instead declared
-- using 'mkBaseUnit'.
-- [Usage:]
--
-- @ \$(mkDim "Time" "T" 400) @
--
mkDim :: String -> String -> Integer -> Q [Dec]
mkDim dimStr prettyStr n = do
let dimName = mkName dimStr
newtypeDec <- mkDimNewtype deriveListDim dimName
-- isDimDec <- mkIsDimInstance dimName unitName
dimIdDec <- mkDimIdTypeInstance dimName n
showDimDec <- mkShowDimInstance dimName dimStr prettyStr
return $ [newtypeDec] ++ dimIdDec ++ showDimDec
----------------------------------- Prefixes -----------------------------------
-- | Make a newtype of the form:
--
-- @
-- newtype Milli (u :: Unit) a = Milli (u a)
-- deriving (Eq, Ord, Num, Fractional, Floating, Real
-- , RealFrac, RealFloat, Functor)
-- deriving Show via MetaPrefix Milli u a
-- deriving ShowUnit via MetaPrefix Milli u
-- @
--
mkPrefixNewtype :: Monad m => [Name] -> Name -> m Dec
mkPrefixNewtype l prefixName = pure $
NewtypeD
[]
prefixName
[ KindedTV (mkName "u") BndrReq (ConT ''Unit)
, PlainTV (mkName "a") BndrReq
]
Nothing
(NormalC prefixName
[ ( Bang NoSourceUnpackedness NoSourceStrictness
, AppT (VarT (mkName "u")) (VarT (mkName "a"))
)
])
[ DerivClause
(Just NewtypeStrategy)
(map ConT l)
, DerivClause
(Just (ViaStrategy viaType))
[ConT ''Show]
, DerivClause
(Just (ViaStrategy viaTypeNoA))
[ConT ''ShowUnit]
]
where
viaType =
foldl AppT (ConT ''MetaPrefix)
[ ConT prefixName
, VarT (mkName "u")
, VarT (mkName "a")
]
viaTypeNoA =
foldl AppT (ConT ''MetaPrefix)
[ ConT prefixName
, VarT (mkName "u")
]
-- | Make a deriving instance of the form
--
-- @
-- deriving via MetaPrefix Milli u instance IsUnit u => IsUnit (Milli u )
-- @
--
mkPrefixIsUnitInstance :: Name -> Q [Dec]
mkPrefixIsUnitInstance prefixName = [d|
deriving via MetaPrefix $(conT prefixName) u instance IsUnit u => IsUnit ($(conT prefixName) u)
|]
-- | Make an instance of the form
--
-- @
-- instance ShowPrefix Milli where
-- type ShowPrefixType Milli = Text "m"
-- showPrefix = "Milli"
-- prettyPrefix = "m"
-- @
--
mkShowPrefixInstance :: Quote m => Name -> String -> String -> m [Dec]
mkShowPrefixInstance prefixName prefixStr prettyStr = [d|
instance ShowPrefix $(conT prefixName) where
type ShowPrefixType $(conT prefixName) = Text $(pure (LitT (StrTyLit prettyStr)))
showPrefix = $(litE (StringL prefixStr))
prettyPrefix = $(litE (StringL prettyStr))
|]
-- | Make an instance of the form
--
-- @
-- instance Fractional a => PrefixFactor Kilo a where
-- prefixFactor = 1000
-- {-# INLINE prefixFactor #-}
--
mkPrefixFactorInstance :: Name -> Rational -> Q [Dec]
mkPrefixFactorInstance prefixName fctr = [d|
instance Fractional a => PrefixFactor $(conT prefixName) a where
prefixFactor = $(litE (RationalL fctr))
{-# INLINE prefixFactor #-}
|]
-- | Make an instance of the form
--
-- @
-- instance ConversionFactor u a => ConversionFactor (Milli u) a where
-- factor = factor @(MetaPrefix Milli u)
-- {-# INLINE factor #-}
-- @
--
mkPrefixConvFactorInstance :: Name -> Q [Dec]
mkPrefixConvFactorInstance prefixName = [d|
instance ConversionFactor u a => ConversionFactor ($(conT prefixName) u) a where
factor = factor @(MetaPrefix $(conT prefixName) u)
{-# INLINE factor #-}
|]
-- | Make an instance of the form
--
-- @
-- instance (ConvertibleUnit u a, Fractional a)
-- => ConvertibleUnit (Milli u) a where
-- toBaseUnit = prefixToBaseUnit
-- {-# INLINE toBaseUnit #-}
-- fromBaseUnit = prefixFromBaseUnit
-- {-# INLINE fromBaseUnit #-}
-- @
--
mkPrefixConvUnitInstance :: Name -> Q [Dec]
mkPrefixConvUnitInstance prefixName = [d|
instance (ConvertibleUnit u a, Fractional a)
=> ConvertibleUnit ($(conT prefixName) u) a where
toBaseUnit = prefixToBaseUnit
{-# INLINE toBaseUnit #-}
fromBaseUnit = prefixFromBaseUnit
{-# INLINE fromBaseUnit #-}
|]
-- | Make a unit prefix.
--
-- [Usage:]
--
-- @
-- \$(mkPrefix "Kilo" "k" 1000)
-- @
--
mkPrefix :: String -> String -> Rational -> Q [Dec]
mkPrefix prefixStr prettyStr fctr = do
let prefixName = mkName prefixStr
newtypeDec <- mkPrefixNewtype deriveList prefixName
isUnitDec <- mkPrefixIsUnitInstance prefixName
showPrefixDec <- mkShowPrefixInstance prefixName prefixStr prettyStr
factorDec <- mkPrefixFactorInstance prefixName fctr
convFactorDec <- mkPrefixConvFactorInstance prefixName
convUnitDec <- mkPrefixConvUnitInstance prefixName
return $ [newtypeDec] ++ isUnitDec ++ showPrefixDec
++ factorDec ++ convFactorDec ++ convUnitDec