constable (empty) → 0.1.0.0
raw patch · 5 files changed
+198/−0 lines, 5 filesdep +basedep +constabledep +doctest
Dependencies added: base, constable, doctest
Files
- CHANGELOG.md +5/−0
- LICENSE +30/−0
- constable.cabal +41/−0
- src/Constable.hs +118/−0
- test/Main.hs +4/−0
+ CHANGELOG.md view
@@ -0,0 +1,5 @@+# Revision history for constable++## 0.1.0.0 -- YYYY-mm-dd++* First version. Released on an unsuspecting world.
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright (c) 2022, Aaron Allen++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 Aaron Allen 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.
+ constable.cabal view
@@ -0,0 +1,41 @@+cabal-version: 3.0+name: constable+version: 0.1.0.0+synopsis: A safe interface for Const summarization+description: A safe interface for using the @Const@ functor to do summarization. See the @Constable@ module for details.+license: BSD-3-Clause+license-file: LICENSE+author: Aaron Allen+maintainer: aaronallen8455@gmail.com+bug-reports: https://github.com/aaronallen8455/constable/issues+-- copyright:+category: Development+build-type: Simple+extra-doc-files: CHANGELOG.md+-- extra-source-files:++common warnings+ ghc-options: -Wall+ -Wno-unticked-promoted-constructors++library+ import: warnings+ exposed-modules: Constable+ -- other-modules:+ -- other-extensions:+ build-depends: base <= 5+ hs-source-dirs: src+ default-language: Haskell2010++test-suite constable-test+ import: warnings+ default-language: Haskell2010+ -- other-modules:+ -- other-extensions:+ type: exitcode-stdio-1.0+ hs-source-dirs: test+ main-is: Main.hs+ build-depends:+ base,+ constable,+ doctest
+ src/Constable.hs view
@@ -0,0 +1,118 @@+{-# LANGUAGE PatternSynonyms #-}+{-# LANGUAGE UndecidableInstances #-}+{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE TypeFamilies #-}+{-| This module provides an interface for the 'Const.Const' functor which facilitates+ using it safely for summarization via the applicative instance @Monoid m => Applicative (Const m)@.++ As an example, say we have a record of numbers and a function that sums up+ the fields of that record. We want to leverage the type system to ensure+ that if the record changes (field are added or removed), then the summing+ function will not compile until it is updated to match.++ As a first attempt, we might write something like this:++ @+ import Data.Functor.Const+ import Data.Monoid++ data Parts = MkParts+ { part1 :: Int+ , part2 :: Int+ , part3 :: Int+ }++ sumParts :: Parts -> Int+ sumParts parts = getSum . getConst '$'+ MkParts+ '<$>' Const (Sum $ part1 parts)+ '<*>' Const (Sum $ part2 parts)+ '<*>' Const (Sum $ part3 parts)+ @++ At first glance it looks like this accomplishes our goal, but there is a+ serious bug. If we remove a field from the record then @sumParts@ will+ indeed fail to compile, but if we add a field then it will compile without+ issue! This is because we didn't explicitly provide a type to the @Const@+ expression being constructed and so the compiler infers it to be a function+ type rather than the fully applied type that we expect. This means we must+ always remember to add explicit type signatures or type applications+ anywhere we use this pattern.++ If we instead use the 'getConstSafe' version of 'Const.getConst' exported by this+ module then we don't need to worry about this issue because the compiler+ will complain if the inferred type of a @Const@ is for a function type.++ If you use HLint, you can go one step further and ban the use of the+ "unsafe" 'Const.getConst' in your codebase by way of an HLint rule:++ @+ - functions:+ - {name: getConst, within: []}+ @+-}+module Constable+ ( -- * API+ getConstSafe+ , mkConst+ , mkConstF+ , Const.Const(Constable.Const)+ -- * Internal+ , FullyAppliedConst+ ) where++import Data.Coerce (coerce)+import qualified Data.Functor.Const as Const+import Data.Kind (Type, Constraint)+import GHC.TypeLits (TypeError, ErrorMessage(..))++-- | A constraint enforcing that 'getConstSafe' is only used with non-partially+-- applied constructors.+type family FullyAppliedConst (a :: Type) :: Constraint where+ FullyAppliedConst (a -> b) =+ TypeError (Text "Const used with partially applied constructor: '"+ :<>: ShowType (a -> b)+ :<>: Text "'")+ FullyAppliedConst a = ()++-- | Unwraps 'Const.Const'. Throws a type error if that 'Const.Const' has a+-- function type for its second argument.+getConstSafe :: FullyAppliedConst b => Const.Const a b -> a+getConstSafe = coerce++-- | Smart constructor for 'Const.Const' that enforces that the value it contains is+-- built from a value of the result type. This provides an extra measure of type safety.+--+-- @+-- data Summable = MkSummable+-- { s1 :: Int+-- , s2 :: Double+-- , s3 :: Integer+-- } deriving (Show)+--+-- test :: Summable+-- test = Summable+-- { s1 = 1+-- , s2 = 2+-- , s3 = 3+-- }+--+-- 'getConstSafe' '$' MkSummable+-- '<$>' mkConst (s1 test) (Sum . fromIntegral)+-- '<*>' mkConst (s2 test) Sum+-- '<*>' mkConst (s3 test) (Sum . fromIntegral)+-- @+mkConst :: a -> (a -> m) -> Const.Const m a+mkConst a toM = coerce (toM a)++-- | Smart constructor for 'Const.Const' like 'mkConst' but where the argument is the+-- result type applied to some type constructor @f@.+mkConstF :: f a -> (f a -> m) -> Const.Const m a+mkConstF fa toM = coerce (toM fa)++-- | A pattern synonym for 'Const.Const' that upholds the fully applied+-- constructor invariant for its second type argument.+pattern Const :: FullyAppliedConst b => a -> Const.Const a b+pattern Const a = Const.Const a+{-# COMPLETE Const #-}
+ test/Main.hs view
@@ -0,0 +1,4 @@+import Test.DocTest++main :: IO ()+main = doctest [ "-isrc" , "test/Test.hs" ]