{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE TemplateHaskell #-} -- | -- The data types creator of exceptions in the compile type. -- -- It has the record of "cause" and "clue". module Control.Exception.Throwable.TH ( declareException ) where import Data.Char (toLower, isUpper, isPunctuation, isNumber) import Language.Haskell.TH ( Bang(..), DecsQ, newName, mkName , Dec(..), TyVarBndr(..), Con(..), Type(..) , SourceUnpackedness(..), SourceStrictness(..) , Q, Name, Exp(..) , Lit(..), Pat(..), Clause(..), Body(..) ) -- | -- @ -- data IOException' a = IOException -- { ioExceptionCause :: String -- , ioExceptionClue :: a -- } -- ==> do -- a <- newName "a" -- ExceptionDataNames { typeName = mkName "IOException'" -- , causeRecordName = mkName "ioExceptionCause" -- , clueRecordName = mkName "ioExceptionClue" -- } -- @ data ExceptionDataNames = ExceptionDataNames { typeName :: String , causeRecordName :: String , clueRecordName :: String } -- | -- Convert string of 'PascalCase' to 'camelCase'. -- If no 'PascalCase' string or multi byte string is given, undefined behavior is happended. -- -- >>> pascalToCamelCase "IOException" -- "ioException" -- >>> pascalToCamelCase "IOException'" -- "ioException'" -- >>> pascalToCamelCase "FOO2BarException" -- "foO2BarException" -- >>> pascalToCamelCase "I2SException" -- "i2SException" -- >>> pascalToCamelCase "Foo2BarException" -- "foo2BarException" -- >>> pascalToCamelCase "IndexOutOfBoundsException" -- "indexOutOfBoundsException" -- >>> pascalToCamelCase "IllegalArgumentException'" -- "illegalArgumentException'" -- >>> pascalToCamelCase "Exception1" -- "exception1" pascalToCamelCase :: String -> String pascalToCamelCase pascalCase | upperIsCont pascalCase = forUpperContCase pascalCase -- e.g. "IOException'", "FOOBarException", "FOO2BarException" | signInMiddle pascalCase = forSignMiddleCase pascalCase -- e.g. "I2SException", "Foo2BarException" ("Exception1" is not) | otherwise = forCasualCase pascalCase -- e.g. "IndexOutOfBoundsException", "Exception1", where -- Are the upper characters continuing ? upperIsCont :: String -> Bool upperIsCont "" = False upperIsCont (_:a:_) = isUpper a upperIsCont (_:_) = False -- Is the sign put in the middle ? signInMiddle :: String -> Bool signInMiddle = any isNumber . init . tail forUpperContCase :: String -> String forUpperContCase pascalCase = -- IOException' let (pascalCase', signs) = span (not . isPunctuation) pascalCase -- ("IOException", "'") (p:ascalCase) = pascalCase' -- ('I':"OException) p' = toLower p -- 'i' (ascal, case_) = span isUpper ascalCase -- ("OE", "xception") (l:acsa) = reverse ascal -- ('E':"O") asca = map toLower $ reverse acsa -- "o" in p':asca ++ l:case_ ++ signs -- ('i':"o") ++ ('E':xception) ++ "'" == "ioException'" ;P forSignMiddleCase :: String -> String forSignMiddleCase pascal0Case = let (pascal, _0Case) = span (not . isNumber) pascal0Case pascal' = map toLower pascal in pascal' ++ _0Case forCasualCase :: String -> String forCasualCase pascalCase = let (p:ascalCase) = pascalCase p' = toLower p in p' : ascalCase -- | -- Declare simple concrete exception datat type in the compile time. -- -- @exceptionName@ must be PascalCase -- (e.g> "IOException'", "IndexOutOfBoundsException". NG> "ioException'", "indexOutOfBoundsException") . -- -- And @exceptionName@ should have the suffix of "Exception". declareException :: String -> DecsQ declareException exceptionName = do typeParam <- newName "a" let typeNames = getTypeNames exceptionName dataDec = defineDatatype typeNames typeParam showInstanceDec <- defineShowInstanceFor typeNames exceptionInstancDec <- defineExceptionInstanceFor typeNames fakeConstructorDec <- defineFakeConstructorFor typeNames return [ dataDec , showInstanceDec , exceptionInstancDec , fakeConstructorDec ] where -- The natural strategy of the evaluation noBang :: Bang noBang = Bang NoSourceUnpackedness NoSourceStrictness -- Create the value of @ExceptionDataNames@ by the type name. -- That type name will be executed the instantiation for @Exception@. getTypeNames :: String -> ExceptionDataNames getTypeNames exceptionName = let (e:xceptionName) = exceptionName --TODO: Guard empty pattern camelExceptionName = (toLower e) : xceptionName in ExceptionDataNames { typeName = exceptionName , causeRecordName = camelExceptionName ++ "Cause" , clueRecordName = camelExceptionName ++ "Clue" } -- Define a data of an exception. -- the data is defined by @ExceptionDataNames@. defineDatatype :: ExceptionDataNames -> Name -> Dec defineDatatype (ExceptionDataNames {..}) a = let exception = mkName typeName causeRecord = mkName causeRecordName clueRecord = mkName clueRecordName in DataD [] exception [PlainTV a] Nothing [ RecC exception [ (causeRecord, noBang, ConT $ mkName "String") , (clueRecord, noBang, VarT a) ] ] [] -- Define an instance of a data of @ExceptionDataNames@ for @Show@. defineShowInstanceFor :: ExceptionDataNames -> Q Dec defineShowInstanceFor exceptionDataNames@(ExceptionDataNames {..}) = do let showClass = mkName "Show" exception = mkName typeName a <- newName "a" showImpl <- declareShowFunc exceptionDataNames return $ InstanceD Nothing [AppT (ConT showClass) (VarT a)] (AppT (ConT showClass) (AppT (ConT exception) (VarT a))) [showImpl] -- Define an instance of a data of @ExceptionDataNames@ for @Exception@. defineExceptionInstanceFor :: ExceptionDataNames -> Q Dec defineExceptionInstanceFor exceptionDataNames@(ExceptionDataNames {..}) = do let typeableClass = mkName "Typeable" showClass = mkName "Show" exceptionClass = mkName "Exception" exception = mkName typeName a <- newName "a" return $ InstanceD Nothing [ AppT (ConT typeableClass) (VarT a) , AppT (ConT showClass) (VarT a) ] (AppT (ConT exceptionClass) (AppT (ConT exception) (VarT a))) [] -- Define @show@ function implementation for @defineShowInstanceFor@. declareShowFunc :: ExceptionDataNames -> Q Dec declareShowFunc ExceptionDataNames {..} = do let exception = mkName typeName showFunc = mkName "show" cause <- newName "cause" return $ FunD showFunc [ -- show Clause [ConP exception [VarP cause, WildP]] (NormalB -- (FooException cause _) = (InfixE (Just . LitE . StringL $ typeName ++ ": ") (VarE $ mkName "++") (Just $ AppE (VarE showFunc) (VarE cause))) -- show ("FooException: " ++ show cause) ) [] ] -- Define the casual data constructor. -- Like @Control.Exception.Throwable.generalException@ without name field. defineFakeConstructorFor :: ExceptionDataNames -> Q Dec defineFakeConstructorFor ExceptionDataNames {..} = do let fConstructor = mkName $ pascalToCamelCase exceptionName exception = mkName typeName a <- newName "a" return $ FunD fConstructor [ Clause [VarP a] (NormalB $ AppE (AppE (ConE exception) (VarE a)) (TupE [])) [] ]