packages feed

exon 0.5.0.0 → 1.0.0.0

raw patch · 17 files changed

+1026/−288 lines, 17 filesdep +criteriondep +generics-sopdep +type-errors-prettydep −textPVP ok

version bump matches the API change (PVP)

Dependencies added: criterion, generics-sop, type-errors-pretty

Dependencies removed: text

API changes (from Hackage documentation)

- Exon: appendSegment :: (Exon tag a, Semigroup a) => Result a -> Segment a -> Result a
- Exon: concatSegments :: (Exon tag a, Monoid a) => NonEmpty (Segment a) -> a
- Exon: convertSegment :: (Exon tag a, IsString a) => Segment a -> Result a
- Exon: data ExonDefault
- Exon: data KeepWhitespace
- Exon: insertWhitespace :: Exon tag a => Result a -> String -> Segment a -> Result a
- Exon: isEmpty :: Exon tag a => a -> Bool
- Exon.Class.Exon: appendSegment :: (Exon tag a, Semigroup a) => Result a -> Segment a -> Result a
- Exon.Class.Exon: concatKeepWs :: forall tag a. Monoid a => Exon tag a => NonEmpty (Segment a) -> a
- Exon.Class.Exon: concatSegments :: (Exon tag a, Monoid a) => NonEmpty (Segment a) -> a
- Exon.Class.Exon: convertKeepWs :: IsString a => Segment a -> Result a
- Exon.Class.Exon: convertSegment :: (Exon tag a, IsString a) => Segment a -> Result a
- Exon.Class.Exon: data ExonDefault
- Exon.Class.Exon: data KeepWhitespace
- Exon.Class.Exon: insertWhitespace :: Exon tag a => Result a -> String -> Segment a -> Result a
- Exon.Class.Exon: instance (GHC.Base.Monoid a, Data.String.IsString a) => Exon.Class.Exon.Exon Exon.Class.Exon.ExonDefault a
- Exon.Class.Exon: instance (GHC.Base.Monoid a, Data.String.IsString a) => Exon.Class.Exon.Exon Exon.Class.Exon.KeepWhitespace a
- Exon.Class.Exon: instance Exon.Class.Exon.Exon Exon.Class.Exon.ExonDefault (GHC.Base.String -> GHC.Base.String)
- Exon.Class.Exon: instance Exon.Class.Exon.Exon Exon.Class.Exon.ExonDefault Data.ByteString.Internal.ByteString
- Exon.Class.Exon: instance Exon.Class.Exon.Exon Exon.Class.Exon.ExonDefault Data.Text.Internal.Text
- Exon.Class.Exon: instance Exon.Class.Exon.Exon Exon.Class.Exon.ExonDefault GHC.Base.String
- Exon.Class.Exon: instance Exon.Class.Exon.Exon Exon.Class.Exon.ExonDefault Incipit.String.Conversion.LByteString
- Exon.Class.Exon: instance Exon.Class.Exon.Exon Exon.Class.Exon.ExonDefault Incipit.String.Conversion.LText
- Exon.Class.Exon: isEmpty :: Exon tag a => a -> Bool
+ Exon: ExonUse :: a -> ExonUse a
+ Exon: SkipWs :: a -> SkipWs a
+ Exon: [$sel:exonUse:ExonUse] :: ExonUse a -> a
+ Exon: class ExonAppend (result :: Type) (builder :: Type)
+ Exon: class ExonBuild (result :: Type) (inner :: Type)
+ Exon: class ExonBuilder (inner :: Type) (builder :: Type) | inner -> builder
+ Exon: class ExonSegment (result :: Type) (builder :: Type)
+ Exon: class ExonString (result :: Type) (builder :: Type)
+ Exon: class ToSegment a b
+ Exon: exonAppend :: ExonAppend result builder => builder -> builder -> Result builder
+ Exon: exonBuild :: ExonBuild result inner => NonEmpty (Segment inner) -> inner
+ Exon: exonBuilder :: ExonBuilder inner builder => inner -> builder
+ Exon: exonBuilderExtract :: ExonBuilder inner builder => Result builder -> inner
+ Exon: exonProcess :: Exon result => NonEmpty (Segment result) -> result
+ Exon: exonSegment :: ExonSegment result builder => Segment builder -> Result builder
+ Exon: exonString :: (ExonString result builder, IsString builder) => String -> Result builder
+ Exon: exonWhitespace :: ExonString result builder => String -> Result builder
+ Exon: exun :: QuasiQuoter
+ Exon: intron :: QuasiQuoter
+ Exon: newtype ExonUse a
+ Exon: newtype SkipWs a
+ Exon: skipWs :: SkipWs a -> a
+ Exon: toSegment :: ToSegment a b => a -> b
+ Exon.Class.Exon: ExonUse :: a -> ExonUse a
+ Exon.Class.Exon: SkipWs :: a -> SkipWs a
+ Exon.Class.Exon: [$sel:exonUse:ExonUse] :: ExonUse a -> a
+ Exon.Class.Exon: class ExonAppend (result :: Type) (builder :: Type)
+ Exon.Class.Exon: class ExonBuild (result :: Type) (inner :: Type)
+ Exon.Class.Exon: class ExonBuilder (inner :: Type) (builder :: Type) | inner -> builder
+ Exon.Class.Exon: class ExonSegment (result :: Type) (builder :: Type)
+ Exon.Class.Exon: class ExonString (result :: Type) (builder :: Type)
+ Exon.Class.Exon: exonAppend :: ExonAppend result builder => builder -> builder -> Result builder
+ Exon.Class.Exon: exonAppendResult :: forall result builder. ExonAppend result builder => Result builder -> Result builder -> Result builder
+ Exon.Class.Exon: exonBuild :: ExonBuild result inner => NonEmpty (Segment inner) -> inner
+ Exon.Class.Exon: exonBuilder :: ExonBuilder inner builder => inner -> builder
+ Exon.Class.Exon: exonBuilderExtract :: ExonBuilder inner builder => Result builder -> inner
+ Exon.Class.Exon: exonProcess :: Exon result => NonEmpty (Segment result) -> result
+ Exon.Class.Exon: exonProcessWith :: forall wrapped result. Exon wrapped => (result -> wrapped) -> (wrapped -> result) -> NonEmpty (Segment result) -> result
+ Exon.Class.Exon: exonSegment :: ExonSegment result builder => Segment builder -> Result builder
+ Exon.Class.Exon: exonString :: (ExonString result builder, IsString builder) => String -> Result builder
+ Exon.Class.Exon: exonWhitespace :: ExonString result builder => String -> Result builder
+ Exon.Class.Exon: instance (Exon.Class.Exon.ExonAppend result builder, Exon.Class.Exon.ExonSegment result builder, Exon.Class.Exon.ExonBuilder inner builder) => Exon.Class.Exon.ExonBuild result inner
+ Exon.Class.Exon: instance (Exon.Class.Newtype.OverNewtypes result inner, Exon.Class.Exon.ExonBuild result inner) => Exon.Class.Exon.Exon result
+ Exon.Class.Exon: instance (GHC.Base.Monoid builder, result GHC.Types.~ builder) => Exon.Class.Exon.ExonBuilder result builder
+ Exon.Class.Exon: instance Data.String.IsString a => Data.String.IsString (Exon.Class.Exon.ExonUse a)
+ Exon.Class.Exon: instance Data.String.IsString a => Data.String.IsString (Exon.Class.Exon.SkipWs a)
+ Exon.Class.Exon: instance Data.String.IsString a => Exon.Class.Exon.ExonString result a
+ Exon.Class.Exon: instance Data.String.IsString builder => Exon.Class.Exon.ExonString (Exon.Class.Exon.SkipWs result) builder
+ Exon.Class.Exon: instance Exon.Class.Exon.ExonAppend result (GHC.Base.String -> GHC.Base.String)
+ Exon.Class.Exon: instance Exon.Class.Exon.ExonBuilder Data.ByteString.Internal.ByteString Data.ByteString.Builder.Internal.Builder
+ Exon.Class.Exon: instance Exon.Class.Exon.ExonBuilder Data.Text.Internal.Text Data.Text.Internal.Builder.Builder
+ Exon.Class.Exon: instance Exon.Class.Exon.ExonBuilder Incipit.String.Conversion.LByteString Data.ByteString.Builder.Internal.Builder
+ Exon.Class.Exon: instance Exon.Class.Exon.ExonBuilder Incipit.String.Conversion.LText Data.Text.Internal.Builder.Builder
+ Exon.Class.Exon: instance Exon.Class.Exon.ExonBuilder a builder => Exon.Class.Exon.ExonBuilder (Exon.Class.Exon.ExonUse a) builder
+ Exon.Class.Exon: instance Exon.Class.Exon.ExonString result (GHC.Base.String -> GHC.Base.String)
+ Exon.Class.Exon: instance Exon.Class.Exon.ExonString result builder => Exon.Class.Exon.ExonSegment result builder
+ Exon.Class.Exon: instance GHC.Base.Semigroup builder => Exon.Class.Exon.ExonAppend result builder
+ Exon.Class.Exon: instance GHC.Classes.Eq a => GHC.Classes.Eq (Exon.Class.Exon.ExonUse a)
+ Exon.Class.Exon: instance GHC.Classes.Eq a => GHC.Classes.Eq (Exon.Class.Exon.SkipWs a)
+ Exon.Class.Exon: instance GHC.Generics.Generic (Exon.Class.Exon.SkipWs a)
+ Exon.Class.Exon: instance GHC.Show.Show a => GHC.Show.Show (Exon.Class.Exon.ExonUse a)
+ Exon.Class.Exon: instance GHC.Show.Show a => GHC.Show.Show (Exon.Class.Exon.SkipWs a)
+ Exon.Class.Exon: newtype ExonUse a
+ Exon.Class.Exon: newtype SkipWs a
+ Exon.Class.Exon: skipWs :: SkipWs a -> a
+ Exon.Class.Newtype: class OverNewtype (current :: Type) (wrapped :: Maybe Type) (inner :: Type) | current -> inner
+ Exon.Class.Newtype: class OverNewtypes (result :: Type) (inner :: Type) | result -> inner
+ Exon.Class.Newtype: instance (Exon.Generic.IsNewtype result wrapped, Exon.Class.Newtype.OverNewtype result wrapped inner) => Exon.Class.Newtype.OverNewtypes result inner
+ Exon.Class.Newtype: instance (Exon.Generic.OverNt current wrapped, Exon.Generic.IsNewtype wrapped next, Exon.Class.Newtype.OverNewtype wrapped next inner) => Exon.Class.Newtype.OverNewtype current ('GHC.Maybe.Just wrapped) inner
+ Exon.Class.Newtype: instance Exon.Class.Newtype.OverNewtype current 'GHC.Maybe.Nothing current
+ Exon.Class.Newtype: overNewtype :: OverNewtype current wrapped inner => (NonEmpty (Segment inner) -> inner) -> NonEmpty (Segment current) -> current
+ Exon.Class.Newtype: overNewtypes :: OverNewtypes result inner => (NonEmpty (Segment inner) -> inner) -> NonEmpty (Segment result) -> result
+ Exon.Class.ToSegment: class ToSegment a b
+ Exon.Class.ToSegment: instance (Exon.Class.ToSegment.NoGenericMessage a b, a GHC.Types.~ b) => Exon.Class.ToSegment.NewtypeSegment 'GHC.Maybe.Nothing a b
+ Exon.Class.ToSegment: instance (Exon.Generic.IsNewtype a wrapped, Exon.Class.ToSegment.NewtypeSegment wrapped a b) => Exon.Class.ToSegment.ToSegment a b
+ Exon.Class.ToSegment: instance (GHC.Generics.Generic a, Generics.SOP.GGP.GFrom a, Generics.SOP.GGP.GCode a GHC.Types.~ '[ '[b]], Exon.Class.ToSegment.ToSegment b c) => Exon.Class.ToSegment.NewtypeSegment ('GHC.Maybe.Just b) a c
+ Exon.Class.ToSegment: instance Data.String.IsString a => Exon.Class.ToSegment.ToSegment Data.ByteString.Internal.ByteString a
+ Exon.Class.ToSegment: instance Data.String.IsString a => Exon.Class.ToSegment.ToSegment Data.Text.Internal.Text a
+ Exon.Class.ToSegment: instance Data.String.IsString a => Exon.Class.ToSegment.ToSegment GHC.Base.String a
+ Exon.Class.ToSegment: instance Data.String.IsString a => Exon.Class.ToSegment.ToSegment Incipit.String.Conversion.LByteString a
+ Exon.Class.ToSegment: instance Data.String.IsString a => Exon.Class.ToSegment.ToSegment Incipit.String.Conversion.LText a
+ Exon.Class.ToSegment: toSegment :: ToSegment a b => a -> b
+ Exon.Data.RawSegment: AutoExpSegment :: String -> RawSegment
+ Exon.Data.Segment: instance Data.String.IsString (Exon.Data.Segment.Segment a)
+ Exon.Data.Segment: instance GHC.Base.Functor Exon.Data.Segment.Segment
+ Exon.Generic: instance (wrapped GHC.Types.~ 'GHC.Maybe.Just d) => Exon.Generic.GDatatypeInfoIsNewtype '[ '[d]] ('Generics.SOP.Type.Metadata.Newtype m n c) wrapped
+ Exon.Generic: instance (wrapped GHC.Types.~ 'GHC.Maybe.Nothing) => Exon.Generic.GDatatypeInfoIsNewtype dss info wrapped
+ Exon.Generic: instance Exon.Generic.GDatatypeInfoIsNewtype (Generics.SOP.GGP.GCode d) (Generics.SOP.GGP.GDatatypeInfoOf d) wrapped => Exon.Generic.IsNewtype d wrapped
+ Exon.Quote: exonSegments :: QuasiQuoter
+ Exon.Quote: exun :: QuasiQuoter
+ Exon.Quote: intron :: QuasiQuoter
- Exon: class Exon (tag :: Type) (a :: Type)
+ Exon: class Exon (result :: Type)
- Exon: exonWith :: Q Type -> QuasiQuoter
+ Exon: exonWith :: Maybe (Q Exp, Q Exp) -> Bool -> Bool -> QuasiQuoter
- Exon.Class.Exon: class Exon (tag :: Type) (a :: Type)
+ Exon.Class.Exon: class Exon (result :: Type)
- Exon.Quote: exonWith :: Q Type -> QuasiQuoter
+ Exon.Quote: exonWith :: Maybe (Q Exp, Q Exp) -> Bool -> Bool -> QuasiQuoter

Files

+ benchmark/Main.hs view
@@ -0,0 +1,43 @@+module Main where++import Criterion.Main (bench, bgroup, defaultMain, whnf)+import qualified Data.Text.Lazy.Builder as Text+import Data.Text.Lazy.Builder (toLazyText)+import Exon.Class.Exon (exonProcess)+import Exon.Data.Segment (Segment)+import qualified Exon.Data.Segment as Segment (Segment (Expression))++newtype Str =+  Str Text+  deriving stock (Eq, Show, Generic)+  deriving newtype (IsString)++newtype Nam =+  Nam Str+  deriving stock (Eq, Show, Generic)+  deriving newtype (IsString)++s2 :: IsString a => a+s2 =+  "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"++segments :: NonEmpty (Segment Nam)+segments =+  (const (Segment.Expression s2)) <$> [1 :: Int .. 1000]++strings :: [Text]+strings =+  replicate 1000 s2++bench_manual :: [Text] -> Text+bench_manual s =+  toStrict (toLazyText (foldMap Text.fromText s))++main :: IO ()+main =+  defaultMain [+    bgroup "exon" [+      bench "build" (whnf exonProcess segments),+      bench "manual" (whnf bench_manual strings)+    ]+  ]
exon.cabal view
@@ -5,8 +5,8 @@ -- see: https://github.com/sol/hpack  name:           exon-version:        0.5.0.0-synopsis:       Monoidal Quasiquote Interpolation+version:        1.0.0.0+synopsis:       Customizable Quasiquote Interpolation description:    See https://hackage.haskell.org/package/exon/docs/Exon.html category:       String homepage:       https://github.com/tek/exon#readme@@ -26,10 +26,13 @@   exposed-modules:       Exon       Exon.Class.Exon+      Exon.Class.Newtype+      Exon.Class.ToSegment       Exon.Combinators       Exon.Data.RawSegment       Exon.Data.Result       Exon.Data.Segment+      Exon.Generic       Exon.Parse       Exon.Quote   hs-source-dirs:@@ -100,11 +103,12 @@   build-depends:       base >=4.12 && <5     , flatparse >=0.3.5.1+    , generics-sop     , haskell-src-exts <1.24     , haskell-src-meta <0.9     , incipit-base >=0.2     , template-haskell-    , text+    , type-errors-pretty   mixins:       base hiding (Prelude)     , incipit-base (IncipitBase as Prelude)@@ -116,9 +120,11 @@   main-is: Main.hs   other-modules:       Exon.Test.BasicTest-      Exon.Test.KeepWsTest+      Exon.Test.BuildTest+      Exon.Test.NewtypeTest       Exon.Test.Quote       Exon.Test.ShowsPrecTest+      Exon.Test.SkipWsTest   hs-source-dirs:       test   default-extensions:@@ -192,7 +198,85 @@     , tasty     , tasty-hedgehog     , template-haskell-    , text+  mixins:+      base hiding (Prelude)+    , incipit-base (IncipitBase as Prelude)+    , incipit-base hiding (IncipitBase)+  default-language: Haskell2010++benchmark exon-bench+  type: exitcode-stdio-1.0+  main-is: Main.hs+  hs-source-dirs:+      benchmark+  default-extensions:+      AllowAmbiguousTypes+      ApplicativeDo+      BangPatterns+      BinaryLiterals+      BlockArguments+      ConstraintKinds+      DataKinds+      DefaultSignatures+      DeriveAnyClass+      DeriveDataTypeable+      DeriveFoldable+      DeriveFunctor+      DeriveGeneric+      DeriveLift+      DeriveTraversable+      DerivingStrategies+      DerivingVia+      DisambiguateRecordFields+      DoAndIfThenElse+      DuplicateRecordFields+      EmptyCase+      EmptyDataDecls+      ExistentialQuantification+      FlexibleContexts+      FlexibleInstances+      FunctionalDependencies+      GADTs+      GeneralizedNewtypeDeriving+      InstanceSigs+      KindSignatures+      LambdaCase+      LiberalTypeSynonyms+      MultiParamTypeClasses+      MultiWayIf+      NamedFieldPuns+      OverloadedLabels+      OverloadedLists+      OverloadedStrings+      PackageImports+      PartialTypeSignatures+      PatternGuards+      PatternSynonyms+      PolyKinds+      QuantifiedConstraints+      QuasiQuotes+      RankNTypes+      RecordWildCards+      RecursiveDo+      RoleAnnotations+      ScopedTypeVariables+      StandaloneDeriving+      TemplateHaskell+      TupleSections+      TypeApplications+      TypeFamilies+      TypeFamilyDependencies+      TypeOperators+      TypeSynonymInstances+      UndecidableInstances+      UnicodeSyntax+      ViewPatterns+  ghc-options: -Wall -Wredundant-constraints -Wincomplete-uni-patterns -Wmissing-deriving-strategies -Widentities -Wunused-packages+  build-depends:+      base >=4.12 && <5+    , criterion+    , exon+    , incipit-base >=0.2   mixins:       base hiding (Prelude)     , incipit-base (IncipitBase as Prelude)
lib/Exon.hs view
@@ -1,51 +1,236 @@--- |Monoidal Quasiquote Interpolation+-- |Customizable Quasiquote Interpolation module Exon (+  -- * Introduction   -- $intro++  -- * Implicit conversion #conversion#+  -- $conversion+   exon,   exonws,+  intron,+  exun,   exonWith,++  -- * Performance+  -- $performance+   -- * Customizing Concatenation-  module Exon.Class.Exon,+  -- $custom++  Exon (..),+  ExonBuild (..),+  ExonAppend (..),+  ExonSegment (..),+  ExonBuilder (..),+  ExonString (..),+  ToSegment (..),+  SkipWs (..),+  skipWs,+  ExonUse (..),++  -- * Type inference+  -- $inference+   -- * Combinators   module Exon.Combinators,+   -- * Data Types   module Exon.Data.Segment,   module Exon.Data.Result, ) where -import Exon.Class.Exon (Exon (..), ExonDefault, KeepWhitespace)+import Exon.Class.Exon (+  Exon (..),+  ExonAppend (..),+  ExonBuild (..),+  ExonBuilder (..),+  ExonSegment (..),+  ExonString (..),+  ExonUse (..),+  SkipWs (..),+  skipWs,+  )+import Exon.Class.ToSegment (ToSegment (..)) import Exon.Combinators (intercalate) import Exon.Data.Result (Result (..)) import Exon.Data.Segment (Segment (..))-import Exon.Quote (exon, exonWith, exonws)+import Exon.Quote (exon, exonWith, exun, intron, exonws) --- $intro--- This Haskell library provides quasiquote string interpolation with customizable concatenation for arbitrary types.------ The default case uses `Monoid` and `IsString`:------ @--- import Exon (exon)--- import Data.Text (toUpper)------ newtype Name =---   Name Text---   deriving newtype (Show, Monoid, IsString)------ instance Semigroup Name where---   Name l <> Name r = Name (l <> " | " <> r)------ lastName :: Name--- lastName = "Fry"------ up :: Name -> Name--- up (Name name) = Name (toUpper name)------ >>> [exon|Philip J. #{up lastName}|]--- Name "Philip | J. | FRY"------ @------ Individual segments are tokenized at whitespace boundaries, expressions between `#{` and `}` are inserted verbatim.------ The default implementation ignores whitespace when concatenating, while it is preserved for `GHC.String`, `Text` etc.+{- $intro+This Haskell library provides quasiquote string interpolation with customizable concatenation for arbitrary types.++A quasiquote with the quoter 'exon' is transformed into a chain of concatenations, allowing expressions to be+interpolated between the characters @#{@ and @}@.+A common use case is plain text interpolation:++>>> :set -XOverloadedStrings+>>> animal = "snake"+>>> location = "a tree"+>>> [exon|#{animal} in #{location}|]+"snake in a tree"++The quote is effectively converted to a sequence of 'Semigroup' concatenations:++>>> animal <> " " <> "in" <> " " <> location+"snake in a tree"++It is precisely converted to an intermediate data structure, 'Segment', and concatenated using customizable classes:++>>> exonProcess [Expression animal, String " in ", Expression location]+"snake in a tree"++The default implementation uses `Monoid` and `IsString`, resulting in something like:++>>> foldl' (<>) mempty [animal, fromString " ", fromString "in", fromString " ", location] :: String+"snake in a tree"++This allows any type that implements those two classes to be used as the result:++>>> newtype Str = Str Text deriving stock (Show) deriving newtype (IsString, Semigroup, Monoid)+>>> [exon|#{animal} in #{location}|] :: Str+Str "snake in a tree"++The astute reader might notice that it is unclear what type @animal@ is expected to be – it hasn't been annotated, yet+it works for both the quote without type annotation as well as the one of type @Str@.++The reason for this phenomenon is that GHC types @animal = "snake"@ as @animal :: IsString a => a@ thanks to+@OverloadedStrings@.+When the quote is written without annotation, the evaluator defaults to @String@ in order to print it (for example,+@hls-eval-plugin@ or GHCi).++When the two names are used in the quote with @Str@, they will be instantiated as @Str@ as well.++One neat application of Exon is for writing 'Text.Show.showsPrec' methods, whose type is @t'String' -> t'String'@ and+which are used to stringify a type with automatic parentheses when contained in a larger type:++> data Numbers =+>   Numbers Int (Maybe Int) Value+>+> instance Show Numbers where+>   showsPrec d Numbers number maybeNumber value =+>     showParen (d > 10)+>       [exon|Numbers #{showsPrec 11 number} #{showsPrec 11 maybeNumber} #{showsPrec 11 value}|]++-}++{- $conversion+Values of different types can be interpolated if they meet one of two conditions:++- They are instances of 'Generic' and representationally equal to the result type (i.e. newtypes)+- They are one of the common stringly types: t'String', 'Text', 'LText', 'ByteString' and 'LByteString'++Exon requires auto-converted values to be interpolated with a different bracket, @##{@, to avoid accidents:++>>> newtype Str = Str Text deriving stock (Show) deriving newtype (IsString, Semigroup, Monoid)+>>> newtype Animal = Animal ByteString deriving stock (Generic)+>>> animal = Animal "lemur"+>>> location = "a tree"+>>> [exon|##{animal} in #{location}|] :: Str+Str "lemur in a tree"++In this case, the two conditions are even combined – the @Animal@ is unwrapped, UTF-8-decoded to @Text@, and rewrapped+into @Str@.++If you absolutely want to splice newtypes with the same brackets, you can use the quoter 'exun' (the @un@ stands for+@unsafe@).+-}++{- $performance+Generic string concatenation can be quite expensive.+If a quote's result type is 'Text', as in:++>>> [exon|#{"x"} #{"y"}|] :: Text++then the resulting concatenation will use the 'Semigroup' operation for 'Text', which has @O(n)@ complexity for each+pair of appended strings.++In order to improve performance, Exon allows the type used for concatenation (the /builder/) to differ from the result+type, which is implemented by the class 'ExonBuilder'.+For example, the instance for 'Text' selects 'Text.Builder' as the builder type and converts the segments and the result+accordingly.+In a quote of 1000 segments, this improves performance by a factor of 100!++However, if the result type is a newtype, the 'Text' builder will not be used:++>>> newtype Str = Str Text deriving stock (Show) deriving newtype (IsString, Semigroup, Monoid)+>>> [exon|#{"x"} #{"y"}|] :: Str++This restriction can be circumvented by making @Str@ an instance of 'Generic' – in that case, Exon will unwrap the type+(even multiply nested newtypes) and use the builder associated with the inner type.+@Str@ doesn't even need to derive 'IsString', 'Semigroup' and 'Monoid' in this case, as all the operations are performed+on 'Text.Builder'.++In principle, this conversion could be done with 'Coercible' as well, but type inference is really bad with that method.++/Note/ that when using [generic segment conversion]("Exon#g:conversion") in conjunction with this, the result type must+also derive 'IsString'.+-}++{- $custom+Quote types don't have to be transparent wrappers for strings.+Concatenation can be changed in a type's 'Semigroup':++>>> import Data.Text (toUpper)+>>> newtype Name = Name Text deriving newtype (Show, IsString)+>>> instance Semigroup Name where Name l <> Name r = Name (l <> " | " <> r)+>>> deriving instance Monoid Name+>>> lastName = Name "Fry"+>>> [intron|Philip J. #{lastName}|]+"Philip | J. | Fry"++This example uses the quoter 'intron', which ignores whitespace.+The 'Semigroup' then inserts custom separators.++The same result can be achieved by using the whitespace-aware quoter 'exonws' and providing a specialized instance of+some of Exon's classes, like 'ExonString':++> newtype Name = Name Text deriving stock (Generic)+> instance ExonString Name Text.Builder where+>   exonWhitespace _ = Result " | "++This example additionally uses the 'Generic' newtype unwrapping feature, allowing the customization to be performed+directly in the efficient builder type.+-}++{- $inference+The type of an expression segment is usually known, for example when the interpolated value is an argument of the+enclosing function:++>>> f :: Text -> Text; f t = [exon|value: #{t}|]+>>> f "snake"+"value: snake"++Here both the interpolated expression and the quote's result type are known.++However, expressions may be polymorphic:++>>> t :: IsString a => a; t = "snake"+>>> f :: Text; f = [exon|value: #{t}|]+>>> f+"value: snake"++The quasiquote parser turns this into the expression:++> [Segment.String "value: ", Segment.Expression t] :: Text++The list of segments has the known type 'Text', obtained from the type signature of @f@.+In this case, the type of @t@ is instantiated as 'Text' as well, causing no type inference problems.++However, if the quote is used polymorphically as well, as in:++> printThing :: Show a => a -> IO ()+> printThing [exon|value: #{t}|]++the type of the segment list would be @∀ a . IsString => [a]@, which would not be possible to concatenate, so @t@ must+be annotated.++Since the segment list is typechecked like any other expression, an expression segment with known type will determine+the result type of an otherwise polymorphic quote:++>>> newtype Str = Str Text deriving stock (Show) deriving newtype (IsString, Semigroup, Monoid)+>>> [exon|pre #{"x" :: Str} post|]+Str "pre x post"++The result type is unambiguously fixed to @Str@.+-}
lib/Exon/Class/Exon.hs view
@@ -1,203 +1,265 @@ -- |Description: Internal module Exon.Class.Exon where +import qualified Data.ByteString.Builder as ByteString+import Data.ByteString.Builder (toLazyByteString)+import qualified Data.Text.Lazy.Builder as Text+import Data.Text.Lazy.Builder (toLazyText) import Text.Show (showString) +import Exon.Class.Newtype (OverNewtypes, overNewtypes) import Exon.Data.Result (Result (Empty, Result)) import qualified Exon.Data.Segment as Segment import Exon.Data.Segment (Segment) --- |The tag for the default quoter 'Exon.exon'.-data ExonDefault---- |The tag for the quoter 'Exon.exonws', keeping whitespace verbatim.-data KeepWhitespace--{- |-This class is responsible for combining segments of an interpolated string, allowing users to define their own rules-for how the result is constructed.-The default implementation converts each literal part with 'IsString' and uses the result type's 'Monoid' to-concatenate them.--The raw parts are encoded as 'Segment', getting combined into a 'Result'.+-- |Wrapping a quote type with this causes whitespace to be ignored.+--+-- @since 1.0.0.0+newtype SkipWs a =+  SkipWs a+  deriving stock (Eq, Show, Generic)+  deriving newtype (IsString) -The default for 'convertSegment' skips whitespace by encoding it into the 'Result' constructor 'Empty', which is a-unit object.-To change this behavior, it can be easily overridden:+-- |Defined separately because TH chokes on the selector.+--+-- @since 1.0.0.0+skipWs :: SkipWs a -> a+skipWs (SkipWs a) =+  a -@-newtype Thing = Thing String deriving newtype (IsString, Semigroup, Monoid, Show)+-- |Wrapping a quote type with this causes @a@ to be used irrespective of whether it is an unwrappable newtype.+--+-- @since 1.0.0.0+newtype ExonUse a =+  ExonUse { exonUse :: a }+  deriving stock (Eq, Show)+  deriving newtype (IsString) -instance Exon ExonDefault Thing where-  convertSegment = \case-    Segment.String s -> Result (Thing s)-    Segment.Expression thing -> Result thing-    Segment.Whitespace _ -> Result (Thing " >>> ")+-- |This class converts a segment into a builder.+--+-- A builder is an auxiliary data type that may improve performance when concatenating segments, like 'Text.Builder'.+-- The default instance uses no builder and is implemented as 'id'.+--+-- @since 1.0.0.0+class ExonBuilder (inner :: Type) (builder :: Type) | inner -> builder where+  -- |Construct a builder from the newtype-unwrapped result type.+  exonBuilder :: inner -> builder -  insertWhitespace s1 ws s2 =-    appendSegment @ExonDefault (appendSegment @ExonDefault s1 (Segment.Whitespace ws)) s2-@--}-class Exon (tag :: Type) (a :: Type) where+  -- |Convert the result of the builder concatenation back to the newtype-unwrapped result type.+  exonBuilderExtract :: Result builder -> inner -  -- |This check is used to allow empty expression segments to be skipped when they are empty.-  -- The default is to never skip expressions.-  isEmpty :: a -> Bool-  isEmpty =-    const False+instance {-# overlappable #-} (+    Monoid builder,+    result ~ builder+  ) => ExonBuilder result builder where+  exonBuilder =+    id+  {-# inline exonBuilder #-}+  exonBuilderExtract =+    fold+  {-# inline exonBuilderExtract #-} -  -- |Convert a 'Segment' to a 'Result'.-  -- The default implementation uses 'IsString' and ignores whitespace, returning 'Empty'.-  convertSegment :: Segment a -> Result a+instance (+    ExonBuilder a builder+  ) => ExonBuilder (ExonUse a) builder where+  exonBuilder =+    exonBuilder @a . exonUse+  exonBuilderExtract =+    ExonUse . exonBuilderExtract -  default convertSegment :: IsString a => Segment a -> Result a-  convertSegment = \case-    Segment.String a ->-      Result (fromString a)-    Segment.Expression a | isEmpty @tag a ->-      Empty-    Segment.Expression a ->-      Result a-    Segment.Whitespace _ ->-      Empty+instance ExonBuilder Text Text.Builder where+  exonBuilder =+    Text.fromText+  {-# inline exonBuilder #-}+  exonBuilderExtract =+    foldMap (toStrict . toLazyText)+  {-# inline exonBuilderExtract #-} -  -- |Append a 'Segment' to a 'Result'.-  -- The default implementation uses '(<>)'.-  appendSegment :: Result a -> Segment a -> Result a+instance ExonBuilder LText Text.Builder where+  exonBuilder =+    Text.fromLazyText+  exonBuilderExtract =+    foldMap toLazyText -  default appendSegment :: Semigroup a => Result a -> Segment a -> Result a-  appendSegment z a =-    z <> convertSegment @tag a+instance ExonBuilder ByteString ByteString.Builder where+  exonBuilder =+    ByteString.byteString+  exonBuilderExtract =+    foldMap (toStrict . toLazyByteString) -  -- |Append whitespace and a 'Segment' to a 'Result', i.e. joining two parts of the interpolation by whitespace.-  -- The default implementation ignores the whitespace, calling 'appendSegment' with the second argument.-  insertWhitespace :: Result a -> String -> Segment a -> Result a+instance ExonBuilder LByteString ByteString.Builder where+  exonBuilder =+    ByteString.lazyByteString+  exonBuilderExtract =+    foldMap toLazyByteString -  default insertWhitespace :: Result a -> String -> Segment a -> Result a-  insertWhitespace s1 _ =-    appendSegment @tag s1+-- |This class generalizes 'IsString' for use in 'ExonSegment'.+--+-- When a plain text segment (not interpolated) is processed, it is converted to the result type, which usually happens+-- via 'fromString'.+--+-- For the type of 'Text.Show.showsPrec' (@'String' -> 'String'@), there is no instance of 'IsString', so this class+-- provides an instance that works around that by calling 'showString'.+--+-- @since 1.0.0.0+class ExonString (result :: Type) (builder :: Type) where+  -- |Convert a 'String' to the builder type.+  exonString :: String -> Result builder -  -- |The entry point for concatenation, taking a list of segments parsed from the interpolation.-  -- The default implementation skips leading whitespace and calls 'appendSegment' and 'insertWhitespace' to-  -- concatenate.-  concatSegments :: NonEmpty (Segment a) -> a+  default exonString :: IsString builder => String -> Result builder+  exonString =+    Result . fromString+  {-# inline exonString #-} -  default concatSegments :: Monoid a => NonEmpty (Segment a) -> a-  concatSegments (h :| t) =-    fold (spin (convertSegment @tag h) t)-    where-      spin :: Result a -> [Segment a] -> Result a-      spin Empty = \case-        [] ->-          Empty-        Segment.Whitespace _ : ss ->-          spin Empty ss-        s1 : ss ->-          spin (convertSegment @tag s1) ss-      spin (Result s1) = \case-        [] ->-          Result s1-        Segment.Whitespace _ : (Segment.Expression a) : ss | isEmpty @tag a ->-          spin (Result s1) ss-        Segment.Whitespace ws : s2 : ss ->-          spin (insertWhitespace @tag (Result s1) ws s2) ss-        [Segment.Whitespace _] ->-          (Result s1)-        s2 : ss ->-          spin (appendSegment @tag (Result s1) s2) ss+  -- |Convert a 'String' containing whitespace to the builder type.+  -- This is only used by whitespace-aware quoters, like 'Exon.exonws' or 'Exon.intron'.+  exonWhitespace :: String -> Result builder -instance {-# overlappable #-} (-    Monoid a,-    IsString a-  ) => Exon ExonDefault a where+  default exonWhitespace :: String -> Result builder+  exonWhitespace =+    exonString @result @builder+  {-# inline exonWhitespace #-} --- |Variant of 'convertSegment' that preserves whitespace verbatim.-convertKeepWs ::-  IsString a =>-  Segment a ->-  Result a-convertKeepWs = \case-  Segment.String a ->-    Result (fromString a)-  Segment.Expression a ->-    Result a-  Segment.Whitespace a ->-    Result (fromString a)+instance {-# overlappable #-} IsString a => ExonString result a where --- |Variant of 'concatSegments' that preserves whitespace verbatim.-concatKeepWs ::-  ∀ tag a .-  Monoid a =>-  Exon tag a =>-  NonEmpty (Segment a) ->-  a-concatKeepWs =-  fold . foldl' (appendSegment @tag) Empty+-- |The instance for the type used by 'Text.Show.showsPrec'.+instance ExonString result (String -> String) where+  exonString =+    Result . showString+  {-# inline exonString #-} +-- |The instance used when the result type is wrapped in 'SkipWs', which is done by 'Exon.intron'.+--+-- It returns 'Empty' for any whitespace. instance (-    Monoid a,-    IsString a-  ) => Exon KeepWhitespace a where-  convertSegment =-    convertKeepWs--  concatSegments =-    concatKeepWs @KeepWhitespace--instance Exon ExonDefault String where-  convertSegment =-    convertKeepWs--  concatSegments =-    concatKeepWs @ExonDefault+    IsString builder+  ) => ExonString (SkipWs result) builder where+  exonWhitespace _ =+    Empty+  {-# inline exonWhitespace #-} -instance Exon ExonDefault Text where-  convertSegment =-    convertSegment @KeepWhitespace+-- |This class converts a 'Segment' to a builder.+--+-- The default implementation performs the following conversions for the differnet segment variants:+--+-- - [Segment.String]('Segment.String') and [Segment.Whitespace]('Segment.Whitespace') are plain 'String's parsed+-- literally from the quasiquote.+-- They are converted to the builder type by 'fromString' (handled by 'ExonString').+--+-- - [Segment.Whitespace]('Segment.Whitespace') is ignored when the quoter 'Exon.intron' was used.+--+-- - [Segment.Expression]('Segment.Expression') contains a value of the builder type, which is returned as-is.+--+-- @since 1.0.0.0+class ExonSegment (result :: Type) (builder :: Type) where+  -- |Convert literal string segments to the result type.+  exonSegment :: Segment builder -> Result builder -  concatSegments =-    concatSegments @KeepWhitespace+instance {-# overlappable #-} (+    ExonString result builder+  ) => ExonSegment result builder where+    exonSegment = \case+      Segment.String a ->+        exonString @result a+      Segment.Expression a ->+        Result a+      Segment.Whitespace a ->+        exonWhitespace @result a+    {-# inline exonSegment #-} -instance Exon ExonDefault LText where-  convertSegment =-    convertSegment @KeepWhitespace+-- |This class handles concatenation of segments, which might be a builder or the result type.+--+-- The default instance simply uses '(<>)', and there is only one special instance for @'String' -> 'String'@, the type+-- used by 'Text.Show.showsPrec'.+--+-- @since 1.0.0.0+class ExonAppend (result :: Type) (builder :: Type) where+  -- |Concatenate two segments of the builder type.+  exonAppend :: builder -> builder -> Result builder -  concatSegments =-    concatSegments @KeepWhitespace+instance {-# overlappable #-} (+    Semigroup builder+  ) => ExonAppend result builder where+  exonAppend z a =+    Result (z <> a)+  {-# inline exonAppend #-} -instance Exon ExonDefault ByteString where-  convertSegment =-    convertSegment @KeepWhitespace+instance ExonAppend result (String -> String) where+  exonAppend z a =+    Result (z . a)+  {-# inline exonAppend #-} -  concatSegments =-    concatSegments @KeepWhitespace+-- |Wrapper for 'exonAppend' that handles the 'Empty' case.+--+-- @since 1.0.0.0+exonAppendResult ::+  ∀ result builder .+  ExonAppend result builder =>+  Result builder ->+  Result builder ->+  Result builder+exonAppendResult (Result z) (Result a) =+  exonAppend @result z a+exonAppendResult z Empty =+  z+exonAppendResult Empty a =+  a+{-# inline exonAppendResult #-} -instance Exon ExonDefault LByteString where-  convertSegment =-    convertSegment @KeepWhitespace+-- |This class implements the 'Segment' concatenation logic.+--+-- 1. Each 'Segment.Expression' is converted to the builder type by 'ExonBuilder'.+-- 2. Each 'Segment.String' and 'Segment.Whitespace' is converted to the builder type by 'ExonSegment' and 'ExonString'.+-- 3. The segments are folded over 'ExonAppend'.+-- 4. The result is converted from the builder type to the original type by 'ExonBuilder'.+--+-- Each step may be overridden individually+--+-- @since 1.0.0.0+class ExonBuild (result :: Type) (inner :: Type) where+  -- |Concatenate a list of 'Segment's.+  exonBuild :: NonEmpty (Segment inner) -> inner -  concatSegments =-    concatSegments @KeepWhitespace+instance {-# overlappable #-} (+    ExonAppend result builder,+    ExonSegment result builder,+    ExonBuilder inner builder+  ) => ExonBuild result inner where+  exonBuild =+    exonBuilderExtract .+    foldl1 (exonAppendResult @result) .+    fmap (exonSegment @result . fmap exonBuilder)+  {-# inline exonBuild #-} -instance Exon ExonDefault (String -> String) where-  convertSegment = \case-    Segment.String a ->-      Result (showString a)-    Segment.Expression a | isEmpty @ExonDefault a ->-      Empty-    Segment.Expression a ->-      Result a-    Segment.Whitespace ws ->-      Result (showString ws)+-- |This class is the main entry point for Exon.+--+-- The default instance unwraps all newtypes that are 'Generic' and passes the innermost type to 'ExonBuild'.+--+-- The original type is also used as a parameter to 'ExonBuild', so customizations can be based on it.+class Exon (result :: Type) where+  -- |Concatenate a list of 'Segment's.+  --+  -- @since 1.0.0.0+  exonProcess :: NonEmpty (Segment result) -> result -  appendSegment z a =-    case (z, convertSegment @ExonDefault a) of-      (Result z', Result a') ->-        Result (z' . a')-      (z', Empty) ->-        z'-      (Empty, a') ->-        a'+instance {-# overlappable #-} (+    OverNewtypes result inner,+    ExonBuild result inner+  ) => Exon result where+    exonProcess =+      overNewtypes @result (exonBuild @result)+    {-# inline exonProcess #-} -  concatSegments =-    concatKeepWs @ExonDefault+-- |Call 'exonProcess', but unwrap the arguments and rewrap the result using the supplied functions.+--+-- @since 1.0.0.0+exonProcessWith ::+  ∀ wrapped result .+  Exon wrapped =>+  (result -> wrapped) ->+  (wrapped -> result) ->+  NonEmpty (Segment result) ->+  result+exonProcessWith unwrap wrap =+  wrap . exonProcess @wrapped . fmap (fmap unwrap)+{-# inline exonProcessWith #-}
+ lib/Exon/Class/Newtype.hs view
@@ -0,0 +1,38 @@+-- |Description: Internal+module Exon.Class.Newtype where++import Exon.Data.Segment (Segment)+import Exon.Generic (IsNewtype, OverNt)+import Unsafe.Coerce (unsafeCoerce)++-- |Internal auxiliary class that applies a function to the value inside of a nested chain of 'Generic' newtypes.+class OverNewtype (current :: Type) (wrapped :: Maybe Type) (inner :: Type) | current -> inner where+  overNewtype :: (NonEmpty (Segment inner) -> inner) -> NonEmpty (Segment current) -> current++instance (+    OverNt current wrapped,+    IsNewtype wrapped next,+    OverNewtype wrapped next inner+  ) => OverNewtype current ('Just wrapped) inner where+    overNewtype f segments =+      unsafeCoerce (overNewtype @wrapped @next @inner f (fmap unsafeCoerce <$> segments))+    {-# inline overNewtype #-}++instance OverNewtype current 'Nothing current where+  overNewtype =+    id+  {-# inline overNewtype #-}++-- |Internal auxiliary class that applies a function to the value inside of a nested chain of 'Generic' newtypes.+--+-- The method only passes its arguments to 'overNewtypes', but the class hides the intermediate parameter.+class OverNewtypes (result :: Type) (inner :: Type) | result -> inner where+  overNewtypes :: (NonEmpty (Segment inner) -> inner) -> NonEmpty (Segment result) -> result++instance (+    IsNewtype result wrapped,+    OverNewtype result wrapped inner+  ) => OverNewtypes result inner where+    overNewtypes =+      overNewtype @result @wrapped+    {-# inline overNewtypes #-}
+ lib/Exon/Class/ToSegment.hs view
@@ -0,0 +1,95 @@+{-# options_haddock prune #-}++-- |Description: Internal+module Exon.Class.ToSegment where++import GHC.TypeLits (ErrorMessage)+import Generics.SOP (SOP (SOP), unZ, I (I), NP ((:*), Nil))+import Generics.SOP.GGP (GCode, GFrom, gfrom)+import Type.Errors.Pretty (type (%), type (<>))++import Exon.Generic (IsNewtype)++class NewtypeSegment (wrapped :: Maybe Type) a b where+  newtypeSegment :: a -> b++instance (+    Generic a,+    GFrom a,+    GCode a ~ '[ '[b]],+    ToSegment b c+  ) => NewtypeSegment ('Just b) a c where+    newtypeSegment (gfrom -> SOP (unZ -> I b :* Nil)) =+      toSegment b++type family Q (a :: k) :: ErrorMessage where+  Q a = "‘" <> a <> "’"++type family NoGenericMessage (a :: Type) (b :: Type) :: Constraint where+  NoGenericMessage a a = a ~ a+  NoGenericMessage a b =+    TypeError (+      "Found an expression of type " <> Q a <> " in a quote of type " <> Q b <> "." %+      "If " <> Q a <> " is a newtype of " <> Q b <> " that should be converted automatically," %+      "you need to add " <> Q "deriving (Generic)" <> " to its declaration." %+      "You can also implement " <> Q ("instance ToSegment " <> a <> " " <> b) <> " for custom interpolation."+    )++instance (+    NoGenericMessage a b,+    a ~ b+  ) => NewtypeSegment 'Nothing a b where+  newtypeSegment =+    id++-- |This class determines how an expression is converted to an interpolation quote's result type.+--+-- For a quote like @[exon|a #{exp :: T} c|] :: R@, the instance @ToSegment T R@ is used to turn @T@ into @R@.+-- Aside from specialized instances for stringly types, the default implementation uses 'Generic' to unwrap newtypes+-- that either match the result type exactly, or uses 'fromString' for result types that implement 'IsString'.+--+-- So given:+--+-- >>> newtype T = T Text deriving newtype (Generic)+-- >>> newtype R = R Text deriving newtype (IsString, Semigroup, Monoid)+--+-- the quote from earlier would generically unwrap @T@ and use 'fromString' to construct an @R@.+class ToSegment a b where+  toSegment :: a -> b++instance {-# incoherent #-} (+    IsNewtype a wrapped,+    NewtypeSegment wrapped a b+  ) => ToSegment a b where+  toSegment =+    newtypeSegment @wrapped++instance {-# overlappable #-} (+    IsString a+  ) => ToSegment String a where+  toSegment =+    fromString . toString++instance {-# overlappable #-} (+    IsString a+  ) => ToSegment Text a where+  toSegment =+    fromString . toString++instance {-# overlappable #-} (+    IsString a+  ) => ToSegment LText a where+  toSegment =+    fromString . toString++instance {-# overlappable #-} (+    IsString a+  ) => ToSegment ByteString a where+  toSegment =+    fromString . decodeUtf8++instance {-# overlappable #-} (+    IsString a+  ) => ToSegment LByteString a where+  toSegment =+    fromString . decodeUtf8
lib/Exon/Data/RawSegment.hs view
@@ -9,4 +9,6 @@   StringSegment String   |   ExpSegment String+  |+  AutoExpSegment String   deriving stock (Eq, Show)
lib/Exon/Data/Segment.hs view
@@ -9,4 +9,8 @@   Whitespace String   |   Expression a-  deriving stock (Eq, Show)+  deriving stock (Eq, Show, Functor)++instance IsString (Segment a) where+  fromString =+    String
+ lib/Exon/Generic.hs view
@@ -0,0 +1,46 @@+{-# options_haddock prune #-}++-- |Description: Internal+module Exon.Generic where++import Generics.SOP (All2, I (I), NP (Nil, (:*)), NS (S, Z), SOP (SOP), Top)+import Generics.SOP.GGP (GCode, GDatatypeInfoOf, GFrom, GTo, gfrom, gto)+import Generics.SOP.Type.Metadata (DatatypeInfo (Newtype))++type ReifySOP (a :: Type) (ass :: [[Type]]) =+  (Generic a, GTo a, GCode a ~ ass, All2 Top ass)++type ConstructSOP (a :: Type) (ass :: [[Type]]) =+  (Generic a, GFrom a, GCode a ~ ass, All2 Top ass)++type ReifyNt (a :: Type) (b :: Type) =+  ReifySOP a '[ '[b] ]++type GenNt (a :: Type) (b :: Type) =+  ConstructSOP a '[ '[b] ]++type OverNt (a :: Type) (b :: Type) =+  (ReifyNt a b, GenNt a b)++unwrap ::+  GenNt a b =>+  a ->+  b+unwrap a =+  case gfrom a of+    SOP (Z (I b :* Nil)) -> b+    SOP (S n) -> case n of++wrap ::+  ReifyNt a b =>+  b ->+  a+wrap b =+  gto (SOP (Z (I b :* Nil)))++class GDatatypeInfoIsNewtype (dss :: [[Type]]) (info :: DatatypeInfo) (wrapped :: Maybe Type) | dss info -> wrapped+instance {-# incoherent #-} wrapped ~ 'Nothing => GDatatypeInfoIsNewtype dss info wrapped+instance wrapped ~ 'Just d => GDatatypeInfoIsNewtype '[ '[d]] ('Newtype m n c) wrapped++class IsNewtype d (wrapped :: Maybe Type) | d -> wrapped+instance GDatatypeInfoIsNewtype (GCode d) (GDatatypeInfoOf d) wrapped => IsNewtype d wrapped
lib/Exon/Parse.hs view
@@ -20,14 +20,14 @@   runParserS,   satisfy,   some_,-  withSpan,   string,   takeRest,+  withSpan,   (<|>),   ) import Prelude hiding (empty, span, (<|>)) -import Exon.Data.RawSegment (RawSegment (ExpSegment, StringSegment, WsSegment))+import Exon.Data.RawSegment (RawSegment (AutoExpSegment, ExpSegment, StringSegment, WsSegment))  type Parser =   FlatParse.Parser Int Text@@ -69,32 +69,62 @@         0 -> unit         cur -> put (cur - 1) *> $(char '}') *> expr -interpolation :: Parser RawSegment-interpolation =+autoInterpolation :: Parser RawSegment+autoInterpolation =+  $(string "##{") *> (AutoExpSegment <$> span expr) <* $(char '}')++verbatimInterpolation :: Parser RawSegment+verbatimInterpolation =   $(string "#{") *> (ExpSegment <$> span expr) <* $(char '}')  untilTokenEnd :: Parser () untilTokenEnd =   branch $(char '\\') (anyChar *> untilTokenEnd) $-  finishBefore ($(string "#{") <|> void ws) $+  finishBefore ($(string "##{") <|> $(string "#{")) $   eof <|> (anyChar *> untilTokenEnd) +untilTokenEndWs :: Parser ()+untilTokenEndWs =+  branch $(char '\\') (anyChar *> untilTokenEndWs) $+  finishBefore ($(string "##{") <|> $(string "#{") <|> void ws) $+  eof <|> (anyChar *> untilTokenEndWs)+ text :: Parser RawSegment text =   StringSegment <$> span untilTokenEnd +textWs :: Parser RawSegment+textWs =+  StringSegment <$> span untilTokenEndWs+ segment :: Parser RawSegment segment =-  branch eof empty (whitespace <|> interpolation <|> text)+  branch eof empty (autoInterpolation <|> verbatimInterpolation <|> text) +segmentWs :: Parser RawSegment+segmentWs =+  branch eof empty (whitespace <|> autoInterpolation <|> verbatimInterpolation <|> textWs)+ parser :: Parser [RawSegment] parser =   FlatParse.many segment -parse :: String -> Either Text [RawSegment]-parse =-  runParserS parser 0 0 >>> \case+parserWs :: Parser [RawSegment]+parserWs =+  FlatParse.many segmentWs++parseWith :: Parser [RawSegment] -> String -> Either Text [RawSegment]+parseWith p =+  runParserS p 0 0 >>> \case     OK a _ "" -> Right a     OK _ _ u -> Left ("unconsumed: " <> decodeUtf8 u)     Fail -> Left "fail"     Err e -> Left e++parse :: String -> Either Text [RawSegment]+parse =+  parseWith parser++parseWs :: String -> Either Text [RawSegment]+parseWs =+  parseWith parserWs
lib/Exon/Quote.hs view
@@ -17,10 +17,11 @@ import Language.Haskell.TH.Quote (QuasiQuoter (QuasiQuoter)) import Language.Haskell.TH.Syntax (Quasi) -import Exon.Class.Exon (ExonDefault, KeepWhitespace, concatSegments)-import Exon.Data.RawSegment (RawSegment (ExpSegment, StringSegment, WsSegment))+import Exon.Class.Exon (SkipWs (SkipWs), exonProcess, exonProcessWith, skipWs)+import Exon.Class.ToSegment (toSegment)+import Exon.Data.RawSegment (RawSegment (AutoExpSegment, ExpSegment, StringSegment, WsSegment)) import qualified Exon.Data.Segment as Segment-import Exon.Parse (parse)+import Exon.Parse (parse, parseWs)  exonError ::   ToString e =>@@ -32,12 +33,13 @@  segmentsQ ::   QOrIO m =>+  Bool ->   String ->   m (NonEmpty RawSegment)-segmentsQ "" =+segmentsQ _ "" =   exonError ("empty interpolation" :: String)-segmentsQ s =-  parse s & fmap nonEmpty & \case+segmentsQ whitespace s =+  (if whitespace then parseWs else parse) s & fmap nonEmpty & \case     Right (Just segs) -> pure segs     Right Nothing -> pure (pure (StringSegment ""))     Left err -> exonError err@@ -65,80 +67,133 @@  reifySegments ::   QOrIO m =>+  Bool ->   NonEmpty RawSegment ->   m (NonEmpty Exp)-reifySegments segs = do+reifySegments unsafe segs = do   expCon <- runQ [e|Segment.Expression|]+  expToSegment <- runQ [e|toSegment|]   for segs \case     StringSegment s ->       runQ [e|Segment.String s|]+    ExpSegment s | unsafe -> do+      e <- reifyExp s+      pure (AppE expCon (AppE expToSegment e))     ExpSegment s -> do       e <- reifyExp s       pure (AppE expCon e)+    AutoExpSegment s -> do+      e <- reifyExp s+      pure (AppE expCon (AppE expToSegment e))     WsSegment s ->      runQ [e|Segment.Whitespace s|]  quoteExpWith ::   QOrIO m =>-  Q TH.Type ->+  Maybe (Q TH.Exp, Q TH.Exp) ->+  Bool ->+  Bool ->   String ->   m Exp-quoteExpWith tag code = do-  raw <- segmentsQ code-  hseg :| segs <- reifySegments raw-  conc <- runQ [e|concatSegments @($tag)|]+quoteExpWith wrapper whitespace unsafe code = do+  raw <- segmentsQ whitespace code+  hseg :| segs <- reifySegments unsafe raw+  conc <- runQ $ maybe [e|exonProcess|] wrapped wrapper   consE <- runQ [e|(:|)|]   pure (AppE conc (InfixE (Just hseg) consE (Just (ListE segs))))+  where+    wrapped (wrap, unwrap) = do+      [e|exonProcessWith ($wrap) $(unwrap)|]  quoteExp ::   QOrIO m =>+  Bool ->+  Bool ->   String ->   m Exp quoteExp =-  quoteExpWith [t|ExonDefault|]+  quoteExpWith Nothing --- |Constructor for a quasiquoter for an arbitrary tag.+quoteSegments ::+  QOrIO m =>+  String ->+  m Exp+quoteSegments code = do+  raw <- segmentsQ True code+  hseg :| segs <- reifySegments False raw+  consE <- runQ [e|(:|)|]+  pure (InfixE (Just hseg) consE (Just (ListE segs)))++-- |Constructor for a quasiquoter that wraps all segments with the first expression and unwraps the result with the+-- second. ----- This can be used to define quoters with custom logic, requiring an instance of 'Exon.Class.Exon' for the given type:+-- This can be used to define quoters with custom logic by providing instances of any of the classes in+-- "Exon.Class.Exon" with the result type argument set to the wrapper type: ----- >>> import Exon.Class.Exon (Exon(..))+-- >>> import Exon.Class.Exon (ExonString (..)) -- >>> import Exon.Data.Segment (Segment(String))--- >>> data Nl--- >>> instance (Monoid a, IsString a) => Exon Nl a where insertWhitespace s1 _ s2 = appendSegment @Nl (appendSegment @Nl s1 (String "\n")) s2--- >>> exonnl = exonWith [t|Nl|]+-- >>> import qualified Data.Text.Lazy.Builder as Text+-- >>> newtype Nl = Nl Text deriving (Generic)+-- >>> getNl (Nl t) = t+-- >>> instance ExonString Nl Text.Builder where exonWhitespace _ = exonString @Nl "\n"+-- >>> exonnl = exonWith (Just ([e|Nl|], [e|getNl|])) True False -- >>> [exonnl|one   two     three|] -- "one\ntwo\nthree" -- -- @since 0.2.0.0-exonWith :: Q TH.Type -> QuasiQuoter-exonWith tag =-  QuasiQuoter (quoteExpWith tag) (err "pattern") (err "type") (err "decl")+exonWith ::+  Maybe (Q TH.Exp, Q TH.Exp) ->+  Bool ->+  Bool ->+  QuasiQuoter+exonWith wrapper whitespace unsafe =+  QuasiQuoter (quoteExpWith wrapper whitespace unsafe) (err "pattern") (err "type") (err "decl")   where     err :: String -> String -> Q a     err tpe _ =       exonError ("Cannot quote " <> tpe) --- |A quasiquoter that allows interpolation, concatenating the resulting segments monoidally.+-- |A quasiquoter that allows interpolation, concatenating the resulting segments with '(<>)' or a an arbitrary+-- user-defined implementation.+-- See the [introduction]("Exon") for details. ----- >>> [exon|write #{show @Text (5 :: Int)} lines of code|] :: Text+-- >>> [exon|write #{show (5 :: Int)} lines of ##{"code" :: ByteString}|] :: Text -- "write 5 lines of code"------ The default implementation for any non-stringly type uses 'IsString' to construct the literal segments and 'mappend'--- to combine them, ignoring whitespace segments.+exon :: QuasiQuoter+exon =+  exonWith Nothing False False++-- |Unsafe version of 'exon', allowing automatic conversion with the same splice brackets as matching types. ----- >>> newtype Part = Part Text deriving newtype (Show, Semigroup, Monoid, IsString)+-- @since 1.0.0.0+exun :: QuasiQuoter+exun =+  exonWith Nothing False True++-- |A variant of 'exon' that ignores all literal whitespace in the quote (not in interpolated expressions). ----- >>> [exon|x #{Part "y"}z|] :: Part--- "xyz"+-- > [intron|x|] === skipWs [exonws|x|] ----- This behavior can be customized by writing an instance of 'Exon.Exon'.-exon :: QuasiQuoter-exon =-  exonWith [t|ExonDefault|]+-- @since 1.0.0.0+intron :: QuasiQuoter+intron =+  exonWith (Just ([e|SkipWs|], [e|skipWs|])) True False --- |A variant of 'exon' that always keeps whitespace verbatim.+-- |A variant of 'exon' that creates segments for each sequence of whitespace characters that can be processed+-- differently by 'Exon.ExonAppend', 'Exon.ExonSegment' or 'Exon.ExonString'. ----- @since 0.2.0.0+-- @since 1.0.0.0 exonws :: QuasiQuoter exonws =-  exonWith [t|KeepWhitespace|]+  exonWith Nothing True False++-- |Internal debugging quoter that produces the raw segments.+--+-- @since 1.0.0.0+exonSegments :: QuasiQuoter+exonSegments =+  QuasiQuoter quoteSegments (err "pattern") (err "type") (err "decl")+  where+    err :: String -> String -> Q a+    err tpe _ =+      exonError ("Cannot quote " <> tpe)
test/Exon/Test/BasicTest.hs view
@@ -3,10 +3,10 @@ import Data.Text (toUpper) import Hedgehog (TestT, (===)) -import Exon.Class.Exon (Exon(..), ExonDefault)+import Exon.Class.Exon (ExonSegment (exonSegment), SkipWs (SkipWs), skipWs)+import Exon.Data.Result (Result (Result)) import qualified Exon.Data.Segment as Segment-import Exon.Quote (exon)-import Exon.Data.Result (Result(Result))+import Exon.Quote (exon, exonws, intron)  newtype Mon =   Mon String@@ -38,26 +38,26 @@  test_basic :: TestT IO () test_basic = do-  Mon "fooandbar" === [exon|foo+  Mon "fooandbar" === [intron|foo     #{var} bar|]   "foo and \\#{bar}" === ([exon|foo #{var} \#{bar}|] :: Text)-  "Philip | J. | FRY" === [exon|Philip J. #{up lastName}|]+  "Philip | J. | FRY" === [intron|Philip J. #{up lastName}|]+  "Philip | J. | FRY" === skipWs [exonws|Philip J. #{SkipWs (up lastName)}|]+  "Philip J. | FRY" === [exon|Philip J.#{up lastName}|]+  ("abc" :: Text) === skipWs [exonws|a  ##{"b" :: Text} #{"c"}|]   where     var :: IsString a => a     var =       "and" -newtype Thing = Thing String deriving newtype (IsString, Semigroup, Monoid, Show, Eq)+newtype Thing = Thing String deriving stock (Generic) deriving newtype (IsString, Show, Eq) -instance Exon ExonDefault Thing where-  convertSegment = \case-    Segment.String s -> Result (Thing s)+instance ExonSegment Thing String where+  exonSegment = \case+    Segment.String s -> Result s     Segment.Expression thing -> Result thing-    Segment.Whitespace _ -> Result (Thing " >>> ")--  insertWhitespace s1 ws s2 =-    appendSegment @ExonDefault (appendSegment @ExonDefault s1 (Segment.Whitespace ws)) s2+    Segment.Whitespace _ -> Result " >>> "  test_customWhitespace :: TestT IO () test_customWhitespace =-  Thing "1 >>> 2 >>> 3" === [exon|1 #{Thing "2"}    3|]+  Thing "1 >>> 2 >>> 3" === [exonws|1 #{Thing "2"}    3|]
+ test/Exon/Test/BuildTest.hs view
@@ -0,0 +1,33 @@+module Exon.Test.BuildTest where++import Hedgehog (TestT, (===))++import Exon.Class.Exon (SkipWs (SkipWs), exonProcess, exonProcessWith, skipWs)+import Exon.Class.ToSegment (toSegment)+import Exon.Data.Segment (Segment (Expression, Whitespace))++newtype Str =+  Str String+  deriving stock (Eq, Show)+  deriving newtype (IsString, Semigroup, Monoid)++segments ::+  IsString a =>+  NonEmpty (Segment a)+segments =+  [+    Whitespace " ",+    "pre",+    Whitespace " ",+    Expression (toSegment "exp"),+    Whitespace "  ",+    "post",+    Whitespace " "+  ]++test_build :: TestT IO ()+test_build = do+  " pre exp  post " === exonProcess @Text segments+  "preexppost" === exonProcessWith @(SkipWs Text) SkipWs skipWs segments+  " pre exp  post " === exonProcess @Str segments+  "preexppost" === exonProcessWith @(SkipWs Str) SkipWs skipWs segments
− test/Exon/Test/KeepWsTest.hs
@@ -1,21 +0,0 @@-module Exon.Test.KeepWsTest where--import Hedgehog (TestT, (===))--import Exon.Quote (exon, exonws)--newtype Mon =-  Mon String-  deriving stock (Eq, Show)-  deriving newtype (IsString, Semigroup, Monoid)--test_keepWs :: TestT IO ()-test_keepWs = do-  Mon "foo\n    and bar" === [exonws|foo-    #{var} bar|]-  "foo and bar" === [exon|foo #{var @Text} bar|]-  "foo and bar" === [exon|foo #{var @ByteString} bar|]-  where-    var :: IsString a => a-    var =-      "and"
+ test/Exon/Test/NewtypeTest.hs view
@@ -0,0 +1,57 @@+module Exon.Test.NewtypeTest where++import Hedgehog (TestT, (===))++import Exon.Class.Exon (exonUse, skipWs)+import Exon.Quote (exon, exonws, exun)++newtype Nt =+  Nt Text+  deriving stock (Eq, Show, Generic)++newtype Str =+  Str Text+  deriving stock (Eq, Show)+  deriving newtype (IsString, Semigroup, Monoid)++newtype StrGen =+  StrGen Text+  deriving stock (Eq, Show, Generic)+  deriving newtype (IsString)++newtype BS =+  BS ByteString+  deriving stock (Eq, Show, Generic)+  deriving newtype (IsString)++newtype StrUse =+  StrUse StrGen+  deriving stock (Eq, Show, Generic)+  deriving newtype (IsString)++instance Semigroup StrUse where+  StrUse (StrGen l) <> StrUse (StrGen r) =+    StrUse (StrGen (l <> "|" <> r))++instance Monoid StrUse where+  mempty =+    StrUse (StrGen "")++nt :: Nt+nt =+  Nt "nt"++test_newtype :: TestT IO ()+test_newtype = do+  ("pre nt Text" :: Text) === [exon|pre ##{nt} #{"Text" :: Text}|]+  ("pre nt Nt \"nt\"" :: Text) === [exon|pre ##{nt} #{show nt}|]+  ("pre nt Nt \"nt\" IsString" :: Text) === [exon|pre ##{nt} #{show nt} #{"IsString"}|]+  ("pre nt Nt \"nt\" IsString Text" :: Text) === [exon|pre ##{nt} #{show nt} #{"IsString"} #{"Text" :: Text}|]+  ("pre nt Nt \"nt\" Text Str" :: Str) === [exon|pre ##{nt} #{show nt} ##{"Text" :: Text} #{"Str" :: Str}|]+  ("pre nt Nt \"nt\" Text Str" :: StrGen) === [exon|pre ##{nt} #{show nt} ##{"Text" :: Text} #{"Str" :: StrGen}|]+  ("pre BS IsString ByteString" :: ByteString) === [exon|pre ##{"BS" :: BS} #{"IsString"} #{"ByteString" :: ByteString}|]+  ("pre BS IsString LByteString" :: Str) === [exon|pre ##{"BS" :: BS} #{"IsString"} ##{"LByteString" :: LByteString}|]+  ("pre BS IsString LByteString" :: Str) === [exun|pre #{"BS" :: BS} #{"IsString"} #{"LByteString" :: LByteString}|]+  ("pre|post" :: StrUse) === exonUse (skipWs [exonws|pre ##{"post" :: Text}|])+  ("prepost" :: StrUse) === skipWs [exonws|pre ##{"post" :: Text}|]+  ("pre post" :: Str) === [exon|pre ##{StrUse "post"}|]
+ test/Exon/Test/SkipWsTest.hs view
@@ -0,0 +1,21 @@+module Exon.Test.SkipWsTest where++import Hedgehog (TestT, (===))++import Exon.Quote (exon, intron)++newtype Mon =+  Mon String+  deriving stock (Eq, Show)+  deriving newtype (IsString, Semigroup, Monoid)++test_skipWs :: TestT IO ()+test_skipWs = do+  Mon "foo\n    and bar" === [exon|foo+    #{var} bar|]+  Mon "fooandbar" === [intron|foo+    #{var} bar|]+  where+    var :: IsString a => a+    var =+      "and"
test/Main.hs view
@@ -1,8 +1,10 @@ module Main where  import Exon.Test.BasicTest (test_basic, test_customWhitespace)-import Exon.Test.KeepWsTest (test_keepWs)+import Exon.Test.BuildTest (test_build)+import Exon.Test.NewtypeTest (test_newtype) import Exon.Test.ShowsPrecTest (test_showsPrec)+import Exon.Test.SkipWsTest (test_skipWs) import Hedgehog (TestT, property, test, withTests) import Test.Tasty (TestName, TestTree, defaultMain, testGroup) import Test.Tasty.Hedgehog (testProperty)@@ -17,10 +19,12 @@ tests :: TestTree tests =   testGroup "all" [+    unitTest "build" test_build,     unitTest "basic" test_basic,     unitTest "custom whitespace handling" test_customWhitespace,     unitTest "concat showsPrec fragments" test_showsPrec,-    unitTest "keep whitespace quoter" test_keepWs+    unitTest "skip whitespace quoter" test_skipWs,+    unitTest "segment newtype conversion" test_newtype   ]  main :: IO ()