ban-instance (empty) → 0.1.0.0
raw patch · 7 files changed
+270/−0 lines, 7 filesdep +ban-instancedep +basedep +template-haskellsetup-changed
Dependencies added: ban-instance, base, template-haskell
Files
- ChangeLog.md +5/−0
- LICENSE +30/−0
- README.md +75/−0
- Setup.hs +2/−0
- ban-instance.cabal +55/−0
- src/Language/Haskell/Instance/Ban.hs +76/−0
- test/Spec.hs +27/−0
+ ChangeLog.md view
@@ -0,0 +1,5 @@+# Changelog for ban-instance++## 0.1.0.0 - 2019-11-08++* Initial Release.
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright Data61 (c) 2017++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++ * Redistributions of source code must retain the above copyright+ notice, this list of conditions and the following disclaimer.++ * Redistributions in binary form must reproduce the above+ copyright notice, this list of conditions and the following+ disclaimer in the documentation and/or other materials provided+ with the distribution.++ * Neither the name of Data61 nor the names of other+ contributors may be used to endorse or promote products derived+ from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,75 @@+# ban-instance - For when a type should never be an instance of a class++++[](https://travis-ci.org/qfpl/ban-instance)++## Synopsis++```haskell+{-# LANGUAGE TemplateHaskell #-}++-- The generated code requires at least these extensions+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE UndecideableInstances #-}++import Lanuage.Haskell.Instance.Ban++data Foo = -- ...++-- Declare that Foo should never have a ToJSON instance+$(banInstance [t|ToJSON Foo|] "why ToJSON Foo should never be defined")+```++Code that attempts to use the banned instance will generate a custom+error message:++```+ • Attempt to use banned instance (ToJSON Foo)+ Reason for banning: why ToJSON Foo should never be defined+ Instance banned at [moduleName] filePath:lineNumber+```++## Motivation++Banning an instance allows the programmer to actively declare that+this instance should never be defined, and provide a reason why. In+terms of what programs the compiler will accept, banning an instance+is the same as leaving it undefined.++Our main use case is banning `ToJSON`/`FromJSON` instances on "core"+data structures to ensure serialisation/deserialisation is defined at+API boundaries. We have systems which send and receive values of+similar types over multiple different APIs, and which need to vary+their JSON representations independently to allow upgrades. Defining+serialisation on core data types means that changes to the+`ToJSON`/`FromJSON` instance can cause breakage at the API layer of+some unrelated system, on the other side of the codebase. Better to+ban `ToJSON`/`FromJSON` on the core data types, and define types for+presentation that live alongside the rest of the API:++```haskell+-- In some "core types" module:+data Foo = -- ...+$(banInstance [t|ToJSON Foo|] "use a newtype wrapper at the API layer")+$(banInstance [t|FromJSON Foo|] "use a newtype wrapper at the API layer")++-- In the module for V1 of the API:+newtype V1 a = V1 a++instance ToJSON (V1 Foo) where -- ...+instance FromJSON (V1 Foo) where -- ...++-- In the module for V2 of the API:+data V2 a = V2 a++instance ToJSON (V2 Foo) where -- ...+instance FromJSON (V2 Foo) where -- ...+```++## Limitations++* There is currently no support for type classes with associated types+ or associated data types.+* Type quotations `[t|...|]` do not support free variables+ ([GHC#5616](https://gitlab.haskell.org/ghc/ghc/issues/5616)).
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ ban-instance.cabal view
@@ -0,0 +1,55 @@+name: ban-instance+version: 0.1.0.0+synopsis: For when a type should never be an instance of a class+description:+ <<https://raw.githubusercontent.com/qfpl/assets/master/data61-transparent-bg.png>>+ .+ Banning an instance allows the programmer to actively declare that+ an instance should never be defined, and provide a reason why:+ .+ @+ data Foo = -- ...++ -- Declare that Foo should never have a ToJSON instance+ $(banInstance [t|ToJSON Foo|] "why ToJSON Foo should never be defined")+ @++category: Haskell+homepage: https://github.com/qfpl/ban-instance#readme+bug-reports: https://github.com/qfpl/ban-instance/issues+author: Jack Kelly, Alex Mason+maintainer: jack.kelly@data61.csiro.au+copyright: Copyright (C) 2017 Data61+license: BSD3+license-file: LICENSE+build-type: Simple+cabal-version: >= 1.10+tested-with: GHC == 8.0.2+ || == 8.2.2+ || == 8.4.4+ || == 8.6.5+ || == 8.8.1++extra-source-files: ChangeLog.md+ README.md++source-repository head+ type: git+ location: https://github.com/qfpl/ban-instance++library+ hs-source-dirs: src+ ghc-options: -Wall+ build-depends: base >= 4.7 && < 4.14+ , template-haskell >= 2.11 && < 2.16+ exposed-modules: Language.Haskell.Instance.Ban+ default-language: Haskell2010++test-suite ban-instance-test+ type: exitcode-stdio-1.0+ main-is: Spec.hs+ hs-source-dirs: test+ ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N+ build-depends: base >= 4.7 && < 4.14+ , ban-instance+ default-language: Haskell2010
+ src/Language/Haskell/Instance/Ban.hs view
@@ -0,0 +1,76 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeOperators #-}+++{-|+Module : Language.Haskell.Instance.Ban+Description : Declare that a typeclass instance+Copyright : (c) 2017, Commonwealth Scientific and Industrial Research Organisation+License : BSD3+Maintainer : jack.kelly@data61.csiro.au+Stability : experimental+Portability : Non-Portable+-}++module Language.Haskell.Instance.Ban (banInstance) where++import Data.Maybe (mapMaybe)+import GHC.TypeLits+import Language.Haskell.TH.Lib+import Language.Haskell.TH.Ppr+import Language.Haskell.TH.Syntax++-- TODO: Mark instances as deprecated so haddock sees them.+-- TODO: Overlappable instances?+-- | Ban an instance of a typeclass; code which tries to use the+-- banned instance will fail at compile time. This works by generating+-- an instance that depends on a custom type error:+--+-- @+-- instance TypeError (..) => ToJSON Foo where+-- ...+-- @+--+-- Use it like this:+--+-- @+-- \$(banInstance [t|ToJSON Foo|] "why ToJSON Foo should never be defined")+-- @+banInstance+ :: TypeQ+ -- ^ The instance you want to ban.+ -- Most easily written with a type-quote: @[t|ToJSON Foo|]@+ -> String -- ^ The reason that this instance is banned.+ -> DecsQ+banInstance constraintQ message = do+ loc <- qLocation+ ClassI (ClassD _ _ _ _ classDecs) _ <- className <$> constraintQ >>= reify+ let context :: CxtQ+ context = cxt [[t|TypeError ('Text "Attempt to use banned instance (" ':<>: 'ShowType $(constraintQ) ':<>: 'Text ")"+ ':$$: 'Text "Reason for banning: " ':<>: 'Text $(symbol message)+ ':$$: 'Text "Instance banned at " ':<>: 'Text $(symbol $ formatLocation loc)+ ':$$: 'Text ""+ )|]]+ pure <$> instanceD context constraintQ (convertClassDecs classDecs)++symbol :: String -> TypeQ+symbol = litT . strTyLit++formatLocation :: Loc -> String+formatLocation Loc{..} = concat ["[", loc_package, ":", loc_module, "] ", loc_filename, ":", show $ fst loc_start]++className :: Type -> Name+className topTy = go topTy where+ go (AppT ty _) = className ty+ go (ConT name) = name+ go _ = error $ "Cannot determine class name for type: " ++ pprint topTy++convertClassDecs :: [Dec] -> [DecQ]+convertClassDecs = mapMaybe go where+ -- TODO: Support type/data families?+ go (SigD name _) = Just $ funD name [clause [] (normalB [|undefined|]) []]+ go DefaultSigD{} = Nothing+ go _ = error "Banning instances only supported for classes \+ \that contain only functions. Patches welcome."
+ test/Spec.hs view
@@ -0,0 +1,27 @@+{-# OPTIONS_GHC -Wno-unused-matches -Wno-unused-top-binds #-}++{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FunctionalDependencies #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE UndecidableInstances #-}++import Language.Haskell.Instance.Ban++-- Test code generation. We define a class and ban an instance of it,+-- checking that the generated code throws no warnings. If this file+-- compiles cleanly, the "test" has passed.++class TestClass a b | a -> b where+ testFunction :: a -> b++$(banInstance [t|TestClass Char Int|] "because it's really bad")++-- Test that we haven't overlapped other instances by mistake.+instance TestClass Int Int where+ testFunction = const 0++main :: IO ()+main = const (testFunction '3' :: Int) $ pure ()