validated-literals 0.1.0 → 0.2.0
raw patch · 3 files changed
+163/−15 lines, 3 files
Files
- ValidLiterals.hs +71/−14
- ValidLiterals/Class.hs +66/−0
- validated-literals.cabal +26/−1
ValidLiterals.hs view
@@ -1,36 +1,93 @@-{-# LANGUAGE DefaultSignatures #-}+-------------------------------------------------------------------------------+-- |+-- Module : ValidLiterals+-- Copyright : (C) 2015 Merijn Verstraaten+-- License : BSD-style (see the file LICENSE)+-- Maintainer : Merijn Verstraaten <merijn@inconsistent.nl>+-- Stability : experimental+-- Portability : portable+--+-- To disallow invalid input it is common to define (new)types with hidden+-- data constructors. Forcing the user to go through a smart-constructor that+-- enforces invariants and returns @Maybe ResultType@, preventing the+-- construction of data with invalid values.+--+-- However, it is __also__ common to want to include literal values of such+-- types in source text. Things of textual literals for HTML, HTTP, etc.+-- In such cases smart-constructors force us to handle potential conversion+-- failures at runtime, or abusing functions like @fromJust@ to break away all+-- the safety smart-constructors provide. All this despite the fact that we+-- can statically know at compile time that the conversion will always succeed+-- or always fails.+--+-- This package provides a typeclasses for using TH to validate the+-- correctness of provided literals at compile. This lets you define, e.g.,+-- @newtype Even = Even Integer@ and write:+--+-- @+-- x :: Even+-- x = $$(valid 38)+-- @+--+-- This will check, at compile time, that the provided 'Integer' is, in fact,+-- even and unwrap it from 'Maybe', avoiding the runtime check.+------------------------------------------------------------------------------- {-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE TemplateHaskell #-}-module ValidLiterals where+module ValidLiterals+ ( Validate (fromLiteral)+ , valid+ , validInteger+ , validRational+ , validString+ , validList+ ) where -import Data.Maybe-import Language.Haskell.TH import Language.Haskell.TH.Syntax -class Validate a b where- fromLiteral :: a -> Maybe b-- spliceValid :: a -> b -> Q (TExp b)- default spliceValid :: Lift b => a -> b -> Q (TExp b)- spliceValid _ v = [|| v ||]+import ValidLiterals.Class +-- | The core function of ValidLiterals, use this together with Typed Template+-- Haskell splices to insert validated literals into your code. For example, if+-- we assume @newtype ASCII = ASCII Char@ where @ASCII@ should only contain+-- ASCII characters, we would write:+--+-- @+-- {-\# LANGUAGE TemplateHaskell #-}+--+-- import ValidLiterals+--+-- x :: ASCII+-- x = $$(valid 'c')+-- @ valid :: Validate a b => a -> Q (TExp b) valid input = case fromLiteral input of Nothing -> fail "Invalid input used for type-safe validated literal!" Just result -> spliceValid input result +-- | Integer literals lead to obnoxious defaulting complaints by GHC, by+-- using this function you can force the defaulting to 'Integer', silencing+-- these warnings.+--+-- Since 'Integral' literals use @fromInteger :: Num a => Integer -> a@ this+-- function cannot cost you any precision. validInteger :: Validate Integer b => Integer -> Q (TExp b) validInteger = valid +-- | Same as 'validInteger', but for 'Fractional' values.+--+-- Since 'Fractional' literals use+-- @fromRational :: Fractional a => Rational -> a@ this function cannot cost+-- you any precision. validRational :: Validate Rational b => Rational -> Q (TExp b) validRational = valid +-- | Same as 'validInteger', but for when enabling @OverloadedStrings@ makes+-- 'String' literals polymorphic. validString :: Validate String b => String -> Q (TExp b) validString = valid +-- | Same as 'validInteger', but for when enabling @OverloadedLists@ makes list+-- literals polymorphic. validList :: Validate [a] b => [a] -> Q (TExp b) validList = valid--hackySpliceValid :: (Lift a, Validate a b) => a -> b -> Q (TExp b)-hackySpliceValid v _ = [|| fromJust (fromLiteral v) ||]
+ ValidLiterals/Class.hs view
@@ -0,0 +1,66 @@+-------------------------------------------------------------------------------+-- |+-- Module : ValidLiterals+-- Copyright : (C) 2015 Merijn Verstraaten+-- License : BSD-style (see the file LICENSE)+-- Maintainer : Merijn Verstraaten <merijn@inconsistent.nl>+-- Stability : experimental+-- Portability : portable+--+-- __WARNING: For implementors of new 'Validate' instances only!__+--+-- This module exposes functions whose use is easily screwed up. Users of+-- validated literals should use the interface exported by "ValidLiterals".+-- This module only exists for implementers of new 'Validate' instances.+-------------------------------------------------------------------------------+{-# LANGUAGE DefaultSignatures #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE TemplateHaskell #-}+module ValidLiterals.Class where++import Data.Maybe+import Language.Haskell.TH+import Language.Haskell.TH.Syntax++-- | Class for validated, compile-time, partial conversions from type 'a' to+-- 'b'.+class Validate a b where+ -- | Converts 'a' values into validated 'b' values+ fromLiteral :: a -> Maybe b++ -- | __WARNING: Don't use! For implementors of 'Validate' instances only!__+ --+ -- Produces the actual Typed Template Haskell splice for the validated+ -- value to splice into the source text. Since you may want to implement a+ -- 'Validate' instance for types that cannot have 'Lift' instances, this+ -- function receives __both__ the original 'a' value and the resulting 'b'+ -- as input.+ --+ -- This allows it to either:+ --+ -- 1. Splice the resulting 'b' into the source using it's Lift+ -- instance, or a custom splice.+ -- 2. Splice the initial 'a' into the source using it's Lift instance,+ -- and perform the conversion again at runtime, and coercing via,+ -- e.g., 'fromJust'. Since 'fromLiteral' is pure (right?!) this should+ -- be perfectly safe.+ --+ -- Clearly, splicing the result directly is much safer (it avoids any+ -- shenanigans with 'unsafePerformIO') and more efficient (no conversion at+ -- runtime). However, this is just not always possible, since, for example,+ -- 'ByteString' does not have a 'Lift' instance and we may still want to+ -- have validated newtypes of 'ByteString'.+ --+ -- __Default implementation:__ The default implementation (using+ -- DefaultSignatures) uses 'b''s 'Lift' instance to do the efficient thing+ -- of directly splicing the resulting 'b' into the source.+ spliceValid :: a -> b -> Q (TExp b)+ default spliceValid :: Lift b => a -> b -> Q (TExp b)+ spliceValid _ v = [|| v ||]++-- | Default implementation for 'spliceValid', uses the 'Lift' instance for 'a'+-- and 'fromJust' to redo the conversion at runtime and (unsafely) coerce from+-- 'Maybe b' to 'b'. . Since 'fromLiteral' is pure (right?!) this should be+-- perfectly safe.+hackySpliceValid :: (Lift a, Validate a b) => a -> b -> Q (TExp b)+hackySpliceValid v _ = [|| fromJust (fromLiteral v) ||]
validated-literals.cabal view
@@ -1,5 +1,5 @@ Name: validated-literals-Version: 0.1.0+Version: 0.2.0 Homepage: https://github.com/merijn/validated-literals Bug-Reports: https://github.com/merijn/validated-literals/issues@@ -19,6 +19,30 @@ Synopsis: Compile-time checking for partial smart-constructors Description:+ To disallow invalid input it is common to define (new)types with hidden+ data constructors. Forcing the user to go through a smart-constructor that+ enforces invariants and returns @Maybe ResultType@, preventing the+ construction of data with invalid values.+ .+ However, it is __also__ common to want to include literal values of such+ types in source text. Things of textual literals for HTML, HTTP, etc.+ In such cases smart-constructors force us to handle potential conversion+ failures at runtime, or abusing functions like @fromJust@ to break away all+ the safety smart-constructors provide. All this despite the fact that we+ can statically know at compile time that the conversion will always succeed+ or always fails.+ .+ This package provides a typeclasses for using TH to validate the+ correctness of provided literals at compile. This lets you define, e.g.,+ @newtype Even = Even Integer@ and write:+ .+ @+ x :: Even+ x = $$(valid 38)+ @+ .+ This will check, at compile time, that the provided @Integer@ is, in fact,+ even and unwrap it from @Maybe@, avoiding the runtime check. Extra-Source-Files: examples/ByteString.hs@@ -34,6 +58,7 @@ MultiParamTypeClasses, TemplateHaskell Exposed-Modules: ValidLiterals+ ValidLiterals.Class Other-Modules: Build-Depends: base >=4.8 && <4.9