packages feed

sum-type-boilerplate (empty) → 0.1.0

raw patch · 8 files changed

+455/−0 lines, 8 filesdep +HUnitdep +basedep +hlint

Dependencies added: HUnit, base, hlint, hspec, template-haskell

Files

+ CHANGELOG.md view
@@ -0,0 +1,6 @@+# sum-type-boilerplate Changelog++## 0.1.0++* Initial release. Includes a TH function to create a sum type and two TH+  functions to convert between sum types.
+ LICENSE.md view
@@ -0,0 +1,19 @@+Copyright (c) 2017 David Reaver++Permission is hereby granted, free of charge, to any person obtaining a copy of+this software and associated documentation files (the "Software"), to deal in+the Software without restriction, including without limitation the rights to+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies+of the Software, and to permit persons to whom the Software is furnished to do+so, subject to the following conditions:++The above copyright notice and this permission notice shall be included in all+copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE+SOFTWARE.
+ README.md view
@@ -0,0 +1,68 @@+# sum-type-boilerplate: A Haskell sum type library++[![CircleCI](https://circleci.com/gh/jdreaver/sum-type-boilerplate.svg?style=svg)](https://circleci.com/gh/jdreaver/sum-type-boilerplate)++This library allows users to use Template Haskell to easily construct and+manipulate sum types. It was born out of the author's desire to reduce the+boilerplate associated with sum types while keeping the type safety they+provide.++## Sum Types++A sum type (also called+a [tagged union](https://en.wikipedia.org/wiki/Tagged_union)) looks like this+in Haskell:++```haskell+data MySum+  = MySumTypeA TypeA+  | MySumTypeB TypeB+  | MySumTypeC TypeC+```++Constructing this when you have a large number of types can be error-prone if+you intend to have a uniform naming scheme, and if you intend to only have each+type present once.++If you have many sum types with overlapping sets of types, then functions to+convert between them can be full of boilerplate.++```haskell+data OtherSum+  = OtherSumTypeA TypeA+  | OtherSumTypeB TypeB++otherSumToMySum :: OtherSum -> MySum+otherSumToMySum (OtherSumTypeA typeA) = MySumTypeA typeA+otherSumToMySum (OtherSumTypeB typeB) = MySumTypeB typeB++mySumToOtherSum :: MySum -> Maybe OtherSum+mySumToOtherSum (MySumTypeA typeA) = Just $ OtherSumTypeA typeA+mySumToOtherSum (MySumTypeB typeB) = Just $ OtherSumTypeB typeB+mySumToOtherSum other = Nothing+```++The boilerplate in examples isn't too bad, but consider writing this when your+sum type has dozens of types in it!++## Where does this library come in?++Using `sum-type-boilerplate`, you can reduce all of the previous code into the+following:++```haskell+constructSumType "MySum" defaultSumTypeOptions [''TypeA, ''TypeB, ''TypeC]+constructSumType "OtherSum" defaultSumTypeOptions [''TypeA, ''TypeB]+sumTypeConverter "otherSumToMySum" ''OtherSum ''MySum+partialSumTypeConverter "mySumToOtherSum" ''MySum ''OtherSum+```++## More features++* This library has an extremely simple implementation. (In fact, it is useful+  as an example if you are learning `template-haskell` for the first time.)+* The only dependency is on `template-haskell`. That means adding it as a+  dependency probably doesn't introduce transitive dependencies.+* The template haskell used here just produces vanilla Haskell data types. No+  crazy type-level magic is going on. That means if you want to ditch this+  library later on, just copy the generated code into your project.
+ library/SumTypes/TH.hs view
@@ -0,0 +1,232 @@+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TemplateHaskell #-}++module SumTypes.TH+  ( -- * Constructing sum types+    constructSumType+  , SumTypeOptions+  , defaultSumTypeOptions+  , sumTypeOptionsTagOptions+  , SumTypeTagOptions (..)+  , sumTypeOptionsConstructorStrictness+  , SumTypeConstructorStrictness (..)+    -- * Converting between sum types+  , sumTypeConverter+  , partialSumTypeConverter+  ) where++import Language.Haskell.TH++-- | This is a template haskell function that creates a sum type from a list of+-- types. Here is an example:+--+-- > data TypeA = TypeA+-- > data TypeB = TypeB+-- > data TypeC = TypeC+-- >+-- > constructSumType "MySum" defaultSumTypeOptions [''TypeA, ''TypeB, ''TypeC]+--+-- This will produce the following sum type:+--+-- > data MySum+-- >   = MySumTypeA TypeA+-- >   | MySumTypeB TypeB+-- >   | MySumTypeC TypeC+--+-- Note that you can use standalone deriving to derive any instances you want:+--+-- > deriving instance Show MySum+-- > deriving instance Eq MySum+constructSumType :: String -> SumTypeOptions -> [Name] -> Q [Dec]+constructSumType typeName SumTypeOptions{..} types = do+  let+    strictness = constructorStrictness sumTypeOptionsConstructorStrictness+    mkConstructor name =+      NormalC+      (constructorName sumTypeOptionsTagOptions typeName name)+      [(Bang NoSourceUnpackedness strictness, ConT name)]+    constructors = map mkConstructor types+  return [DataD [] (mkName typeName) [] Nothing constructors []]++-- | Options for 'constructSumType'. Note that the constructor for this type is+-- not exported, please use 'defaultSumTypeOptions'. (This is done for+-- the sake of backwards compatibility in case we add options.)+data SumTypeOptions+  = SumTypeOptions+  { sumTypeOptionsTagOptions :: SumTypeTagOptions+  , sumTypeOptionsConstructorStrictness :: SumTypeConstructorStrictness+  }++-- | Default options for 'SumTypeOptions'+--+-- @+-- 'SumTypeOptions'+-- { 'sumTypeOptionsTagOptions' = 'PrefixTagsWithTypeName'+-- , 'sumTypeOptionsConstructorStrictness' = 'LazySumTypeConstructors'+-- }+-- @+defaultSumTypeOptions :: SumTypeOptions+defaultSumTypeOptions =+  SumTypeOptions+  { sumTypeOptionsTagOptions = PrefixTagsWithTypeName+  , sumTypeOptionsConstructorStrictness = LazySumTypeConstructors+  }++-- | This type specifies how 'constructSumType' will generate the tags for each+-- type.+data SumTypeTagOptions+  = PrefixTagsWithTypeName+    -- ^ This option generates tags with the sum type name prefixed to each+    -- tag.+  | AppendTypeNameToTags+    -- ^ This option generates tags with the sum type name appended to each+    -- tag.+  | ConstructTagName (String -> String)+    -- ^ Uses the given function to construct an arbitrary tag name. The+    -- argument to this function is the name of the tagged type.++constructorName :: SumTypeTagOptions -> String -> Name -> Name+constructorName PrefixTagsWithTypeName typeName = mkName . (typeName ++) . nameBase+constructorName AppendTypeNameToTags typeName = mkName . (++ typeName) . nameBase+constructorName (ConstructTagName mkConstructor) _ = mkName . mkConstructor . nameBase++-- | Defines if the constructors for the sum type should be lazy or strict.+data SumTypeConstructorStrictness+  = LazySumTypeConstructors+    -- ^ Constructors will be lazy+  | StrictSumTypeConstructors+    -- ^ Constructors will be strict+  deriving (Show, Eq)++constructorStrictness :: SumTypeConstructorStrictness -> SourceStrictness+constructorStrictness LazySumTypeConstructors = NoSourceStrictness+constructorStrictness StrictSumTypeConstructors = SourceStrict++-- | This template haskell function creates a conversion function between two+-- sum types. It works by matching up constructors that share the same inner+-- type. Note that all types in the source sum type must be present in the+-- target sum type, or you will get an error.+--+-- > data MySum+-- >   = MySumTypeA TypeA+-- >   | MySumTypeB TypeB+-- >   | MySumTypeC TypeC+-- >+-- > data OtherSum+-- >   = OtherSumTypeA TypeA+-- >   | OtherSumTypeB TypeB+-- >+-- > sumTypeConverter "otherSumToMySum" ''OtherSum ''MySum+--+-- This will producing the following code:+--+-- > otherSumToMySum :: OtherSum -> MySum+-- > otherSumToMySum (OtherSumTypeA typeA) = MySumTypeA typeA+-- > otherSumToMySum (OtherSumTypeB typeB) = MySumTypeB typeB+sumTypeConverter :: String -> Name -> Name -> Q [Dec]+sumTypeConverter functionName sourceType targetType = do+  bothConstructors <- matchTypeConstructors sourceType targetType+  let+    funcName = mkName functionName+  funcClauses <- mapM mkSerializeFunc bothConstructors+  typeDecl <- [t| $(conT sourceType) -> $(conT targetType) |]+  return+    [ SigD funcName typeDecl+    , FunD funcName funcClauses+    ]++-- | Similar to 'sumTypeConverter', except not all types in the source sum type+-- need to be present in the target sum type.+--+-- Note that this doesn't produce a partial function in the Haskell sense; you+-- won't get an 'error' with the generated function on any arguments. The word+-- partial is used mathematically to denote that not all types from the source+-- sum type are present in the target sum type.+--+-- > data MySum+-- >   = MySumTypeA TypeA+-- >   | MySumTypeB TypeB+-- >   | MySumTypeC TypeC+-- >+-- > data OtherSum+-- >   = OtherSumTypeA TypeA+-- >   | OtherSumTypeB TypeB+-- >+-- > partialSumTypeConverter "mySumToOtherSum" ''MySum ''OtherSum+--+-- This will producing the following code:+--+-- > mySumToOtherSum :: MySum -> Maybe OtherSum+-- > mySumToOtherSum (MySumTypeA typeA) = Just $ OtherSumTypeA typeA+-- > mySumToOtherSum (MySumTypeB typeB) = Just $ OtherSumTypeB typeB+-- > mySumToOtherSum other = Nothing+partialSumTypeConverter :: String -> Name -> Name -> Q [Dec]+partialSumTypeConverter functionName sourceType targetType = do+  bothConstructors <- matchTypeConstructors targetType sourceType+  let+    funcName = mkName functionName+    wildcardClause = Clause [WildP] (NormalB (ConE 'Nothing)) []+  funcClauses <- mapM mkDeserializeFunc bothConstructors+  typeDecl <- [t| $(conT sourceType) -> Maybe $(conT targetType) |]++  return+    [ SigD funcName typeDecl+    , FunD funcName (funcClauses ++ [wildcardClause])+    ]++matchTypeConstructors :: Name -> Name -> Q [BothConstructors]+matchTypeConstructors sourceType targetType = do+  sourceConstructors <- typeConstructors sourceType+  targetConstructors <- typeConstructors targetType+  mapM (matchConstructor targetConstructors) sourceConstructors++-- | Extract the constructors and types for the given sum type.+typeConstructors :: Name -> Q [(Type, Name)]+typeConstructors typeName = do+  info <- reify typeName+  case info of+    (TyConI (DataD _ _ _ _ constructors _)) -> mapM go constructors+      where+        go (NormalC name []) = fail $ "Constructor " ++ nameBase name ++ " doesn't have any arguments"+        go (NormalC name [(_, type')]) = return (type', name)+        go (NormalC name _) = fail $ "Constructor " ++ nameBase name ++ " has more than one argument"+        go _ = fail $ "Invalid constructor in " ++ nameBase typeName+    _ -> fail $ nameBase typeName ++ " must be a sum type"++-- | Find the corresponding target constructor for a given source constructor.+matchConstructor :: [(Type, Name)] -> (Type, Name) -> Q BothConstructors+matchConstructor targetConstructors (type', sourceConstructor) = do+  targetConstructor <-+    maybe+    (fail $ "Can't find constructor in target type corresponding to " ++ nameBase sourceConstructor)+    return+    (lookup type' targetConstructors)+  return $ BothConstructors type' sourceConstructor targetConstructor++-- | Utility type to hold the source and target constructors for a given type.+data BothConstructors =+  BothConstructors+  { innerType :: Type+  , sourceConstructor :: Name+  , targetConstructor :: Name+  }++-- | Construct the TH function 'Clause' for the serialization function for a+-- given type.+mkSerializeFunc :: BothConstructors -> Q Clause+mkSerializeFunc BothConstructors{..} = do+  varName <- newName "value"+  let+    patternMatch = ConP sourceConstructor [VarP varName]+    constructor = AppE (ConE targetConstructor) (VarE varName)+  return $ Clause [patternMatch] (NormalB constructor) []++-- | Construct the TH function 'Clause' for the deserialization function for a+-- given type.+mkDeserializeFunc :: BothConstructors -> Q Clause+mkDeserializeFunc BothConstructors{..} = do+  varName <- newName "value"+  let+    patternMatch = ConP targetConstructor [VarP varName]+    constructor = AppE (ConE 'Just) (AppE (ConE sourceConstructor) (VarE varName))+  return $ Clause [patternMatch] (NormalB constructor) []
+ sum-type-boilerplate.cabal view
@@ -0,0 +1,70 @@+-- This file has been generated from package.yaml by hpack version 0.17.0.+--+-- see: https://github.com/sol/hpack++name:           sum-type-boilerplate+version:        0.1.0+synopsis:       Library for reducing the boilerplate involved with sum types+description:    Library for reducing the boilerplate involved in creating and manipulating sum types+category:       Types,TH+stability:      experimental+homepage:       https://github.com/jdreaver/sum-type-boilerplate#readme+bug-reports:    https://github.com/jdreaver/sum-type-boilerplate/issues+author:         David Reaver+maintainer:     David Reaver+license:        MIT+license-file:   LICENSE.md+build-type:     Simple+cabal-version:  >= 1.10++extra-source-files:+    CHANGELOG.md+    README.md++source-repository head+  type: git+  location: https://github.com/jdreaver/sum-type-boilerplate++library+  hs-source-dirs:+      library+  ghc-options: -Wall+  build-depends:+      base >= 4.9 && < 5+    , template-haskell+  exposed-modules:+      SumTypes.TH+  default-language: Haskell2010++test-suite spec+  type: exitcode-stdio-1.0+  main-is: Spec.hs+  hs-source-dirs:+      tests+      library+  ghc-options: -Wall+  build-depends:+      base >= 4.9 && < 5+    , template-haskell+    , hspec+    , HUnit+  other-modules:+      HLint+      SumTypes.THSpec+      SumTypes.TH+  default-language: Haskell2010++test-suite style+  type: exitcode-stdio-1.0+  main-is: HLint.hs+  hs-source-dirs:+      tests+  ghc-options: -Wall+  build-depends:+      base >= 4.9 && < 5+    , template-haskell+    , hlint+  other-modules:+      Spec+      SumTypes.THSpec+  default-language: Haskell2010
+ tests/HLint.hs view
@@ -0,0 +1,18 @@+module Main (main) where++import Language.Haskell.HLint (hlint)+import System.Exit (exitFailure, exitSuccess)++import Prelude (String, IO, null)++arguments :: [String]+arguments =+  [ "library"+  , "tests"+  , "-i=Redundant do"+  ]++main :: IO ()+main = do+  hints <- hlint arguments+  if null hints then exitSuccess else exitFailure
+ tests/Spec.hs view
@@ -0,0 +1,1 @@+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
+ tests/SumTypes/THSpec.hs view
@@ -0,0 +1,41 @@+{-# LANGUAGE StandaloneDeriving #-}+{-# LANGUAGE TemplateHaskell #-}++module SumTypes.THSpec (spec) where++import Test.Hspec++import SumTypes.TH++data TypeA = TypeA deriving (Show, Eq)+data TypeB = TypeB deriving (Show, Eq)+data TypeC = TypeC deriving (Show, Eq)++constructSumType "MySum" defaultSumTypeOptions [''TypeA, ''TypeB, ''TypeC]+constructSumType "OtherSum" defaultSumTypeOptions [''TypeA, ''TypeB]+sumTypeConverter "otherSumToMySum" ''OtherSum ''MySum+partialSumTypeConverter "mySumToOtherSum" ''MySum ''OtherSum++deriving instance Show MySum+deriving instance Eq MySum++deriving instance Show OtherSum+deriving instance Eq OtherSum++spec :: Spec+spec = do+  describe "constructSumType" $ do+    it "compiles without breaking" $ do+      -- The real utility in this test is the fact that it compiles+      length [MySumTypeA TypeA, MySumTypeB TypeB, MySumTypeC TypeC] `shouldBe` 3++  describe "sumTypeConverter" $ do+    it "properly converts between types" $ do+      otherSumToMySum (OtherSumTypeA TypeA) `shouldBe` MySumTypeA TypeA+      otherSumToMySum (OtherSumTypeB TypeB) `shouldBe` MySumTypeB TypeB++  describe "partialSumTypeConverter" $ do+    it "properly converts between types" $ do+      mySumToOtherSum (MySumTypeA TypeA) `shouldBe` Just (OtherSumTypeA TypeA)+      mySumToOtherSum (MySumTypeB TypeB) `shouldBe` Just (OtherSumTypeB TypeB)+      mySumToOtherSum (MySumTypeC TypeC) `shouldBe` Nothing