Skip to content

Commit 684e681

Browse files
committed
WIP: initial interpolating quasi quoter via typeclass
This is a WIP idea I’ve had that’s related to I think GHC string interpolation should ultimately work. Instead of defaulting to IsString and having a fixed amount of output formats we can use, we want the user to provide three things: A) The output type `out`, which has to be `Semigroup` for concatenation B) An instance `Interpolate a out` for each type that should be able to be interpolated into `out`. C) An instance `Interpolate Text out` for interpolating raw strings This way the user can provide domain-specific instances and prevent some problematic interpolations. This is just an initial idea, not intended to be merged.
1 parent 9d66572 commit 684e681

File tree

5 files changed

+108
-19
lines changed

5 files changed

+108
-19
lines changed

PyF.cabal

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ extra-source-files:
2121

2222
library
2323
exposed-modules:
24+
Test
2425
PyF
2526
PyF.Class
2627
PyF.Formatters

src/PyF.hs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
{-# LANGUAGE FlexibleInstances #-}
33
{-# LANGUAGE TemplateHaskell #-}
44
{-# LANGUAGE ViewPatterns #-}
5+
{-# LANGUAGE MultiParamTypeClasses #-}
6+
{-# LANGUAGE GADTs #-}
57

68
-- | A lot of quasiquoters to format and interpolate string expressions.
79
module PyF
@@ -30,7 +32,13 @@ import Data.Char (isSpace)
3032
import Data.List (intercalate)
3133
import Language.Haskell.TH.Quote (QuasiQuoter (..))
3234
import PyF.Class
33-
import PyF.Internal.QQ (Config (..), expQQ, toExp, toExpPlain, wrapFromString)
35+
import PyF.Internal.QQ (Config (..), expQQ, toExp, toExpPlain, wrapFromString, toExpPlain')
36+
import Language.Haskell.TH (pprint, runQ, extsEnabled, Loc (..))
37+
import Language.Haskell.TH.Syntax (location)
38+
import qualified Language.Haskell.TH.Syntax as TH
39+
import Language.Haskell.TH (Code(..))
40+
import Language.Haskell.TH (liftCode)
41+
import Language.Haskell.TH (listE)
3442

3543
-- | Generic formatter, can format an expression to any @t@ as long as
3644
-- @t@ is an instance of 'IsString'.
@@ -135,3 +143,4 @@ mkFormatter name config = expQQ name (toExp config)
135143
-- 'fmtConfig' and 'strConfig' for examples.
136144
mkFormatterPlain :: String -> Config -> QuasiQuoter
137145
mkFormatterPlain name config = expQQ name (toExpPlain config)
146+

src/PyF/Class.hs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
{-# LANGUAGE TypeApplications #-}
66
{-# LANGUAGE TypeFamilies #-}
77
{-# LANGUAGE UndecidableInstances #-}
8+
{-# LANGUAGE ExistentialQuantification #-}
89

910
-- | You want to add formatting support for your custom type. This is the right module.
1011
--
@@ -48,6 +49,7 @@ import qualified Data.Time
4849
import Data.Word
4950
import Numeric.Natural
5051
import PyF.Formatters
52+
import Data.Data (Proxy (Proxy))
5153

5254
-- * Default formatting classification
5355

@@ -203,3 +205,16 @@ instance {-# OVERLAPPABLE #-} (Integral t) => PyfFormatIntegral t where
203205
-- 97
204206
instance PyfFormatIntegral Char where
205207
pyfFormatIntegral f s p g v = formatIntegral f s p g (ord v)
208+
209+
210+
211+
class Interpolate a into where
212+
interpolateInto :: a -> into
213+
214+
instance Interpolate a a where
215+
interpolateInto = id
216+
217+
data Interpolatable into = forall a. (Interpolate a into) => Interpolatable (Proxy into) a
218+
219+
instance Interpolate (Interpolatable into) into where
220+
interpolateInto (Interpolatable Proxy a) = interpolateInto a

src/PyF/Internal/QQ.hs

Lines changed: 35 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,21 @@
1515
{-# LANGUAGE UndecidableInstances #-}
1616
{-# LANGUAGE ViewPatterns #-}
1717
{-# OPTIONS_GHC -Wno-name-shadowing #-}
18+
{-# LANGUAGE LambdaCase #-}
1819

1920
-- | This module uses the python mini language detailed in
2021
-- 'PyF.Internal.PythonSyntax' to build an template haskell expression
2122
-- representing a formatted string.
2223
module PyF.Internal.QQ
23-
( toExp,
24-
toExpPlain,
25-
Config (..),
26-
wrapFromString,
27-
expQQ,
28-
)
24+
-- ( toExp,
25+
-- toExpPlain,
26+
-- toExpPlain',
27+
-- toFormatPlain,
28+
-- ItemPlain(..),
29+
-- Config (..),
30+
-- wrapFromString,
31+
-- expQQ,
32+
-- )
2933
where
3034

3135
import Control.Monad.Reader
@@ -107,6 +111,9 @@ import Text.Parsec.Error
107111
import Text.Parsec.Pos (initialPos)
108112
import Text.ParserCombinators.Parsec.Error (Message (..))
109113
import Unsafe.Coerce (unsafeCoerce)
114+
import qualified Data.Text as Text
115+
import Data.List.NonEmpty (NonEmpty((:|)), nonEmpty)
116+
import Data.Semigroup (Semigroup(sconcat))
110117

111118
-- | Configuration for the quasiquoter
112119
data Config = Config
@@ -168,28 +175,31 @@ toExp Config {delimiters = expressionDelimiters, postProcess} s = do
168175

169176
-- | Parse a string and return a formatter for it
170177
toExpPlain :: Config -> String -> Q Exp
171-
toExpPlain Config {delimiters = expressionDelimiters, postProcess} s = do
178+
toExpPlain config s = do
172179
loc <- location
173180
exts <- extsEnabled
174-
let context = ParsingContext expressionDelimiters exts
181+
toExpPlain' loc s exts config
175182

183+
toExpPlain' :: Loc -> String -> [Extension] -> Config -> Q Exp
184+
toExpPlain' loc s exts Config {delimiters = expressionDelimiters, postProcess} = do
176185
-- Setup the parser so it matchs the real original position in the source
177186
-- code.
178187
let filename = loc_filename loc
179188
let initPos = setSourceColumn (setSourceLine (initialPos filename) (fst $ loc_start loc)) (snd $ loc_start loc)
189+
let context = ParsingContext expressionDelimiters exts
180190
case runReader (runParserT (setPosition initPos >> parseGenericFormatStringPlain) () filename s) context of
181191
Left err -> do
182192
reportParserErrorAt err
183193
-- returns a dummy exp, so TH continues its life. This TH code won't be
184194
-- executed anyway, there is an error
185-
[|()|]
195+
[|interpolateInto Text.empty|]
186196
Right items -> do
187197
checkResult <- checkVariablesPlain items
188198
case checkResult of
189-
Nothing -> postProcess (goFormatPlain items)
199+
Nothing -> goFormatPlain items
190200
Just (srcSpan, msg) -> do
191201
reportErrorAt srcSpan msg
192-
[|()|]
202+
[|interpolateInto Text.empty|]
193203

194204

195205
findFreeVariablesInFormatMode :: Maybe FormatMode -> [(SrcSpan, RdrName)]
@@ -377,9 +387,14 @@ goFormat items = foldl1 sappendQ <$> mapM toFormat items
377387

378388
goFormatPlain :: [ItemPlain] -> Q Exp
379389
-- We special case on empty list in order to generate an empty string
380-
goFormatPlain [] = pure $ LitE (StringL "") -- see [Empty String Lifting]
381-
goFormatPlain items = [|mconcat $(ListE <$> mapM toFormatPlain items)|]
390+
goFormatPlain items = case nonEmpty items of
391+
Nothing -> [|interpolateInto Text.empty|] -- see [Empty String Lifting]
392+
Just items -> do
393+
let items' = fmap toFormatPlain items
394+
[|$(nonEmptyE items')|]
382395

396+
nonEmptyE :: NonEmpty (Q Exp) -> Q Exp
397+
nonEmptyE (x :| xs) = [|sconcat ($(x) :| $(listE xs))|]
383398

384399
-- | call `<>` between two 'Exp'
385400
sappendQ :: Exp -> Exp -> Exp
@@ -395,11 +410,13 @@ toFormat (Replacement (_, expr) y) = do
395410

396411

397412
toFormatPlain :: ItemPlain -> Q Exp
398-
toFormatPlain (RawPlain x) = pure $ LitE (StringL x) -- see [Empty String Lifting]
399-
toFormatPlain (ReplacementPlain (_, expr)) = do
400-
f <- [|pyfToString|]
401-
pure $ f `AppE` expr
402-
413+
toFormatPlain item = do
414+
let tyProxy = SigE (ConE 'Proxy) (AppT (ConT ''Proxy) (ConT ''Text.Text))
415+
case item of
416+
(RawPlain x) -> [|interpolateInto $[|Text.pack x|]|]
417+
(ReplacementPlain (_, expr)) -> do
418+
exprTyped <- [|$(pure expr)|]
419+
[|interpolateInto $(pure exprTyped)|]
403420

404421
-- | Default precision for floating point
405422
defaultFloatPrecision :: Maybe Int

src/Test.hs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
{-# LANGUAGE OverloadedStrings #-}
2+
{-# LANGUAGE TypeApplications #-}
3+
{-# LANGUAGE TemplateHaskell #-}
4+
{-# LANGUAGE MultiParamTypeClasses #-}
5+
6+
module Test where
7+
import PyF
8+
import Data.Text
9+
import Language.Haskell.TH (pprint, Loc (..), runQ)
10+
import PyF.Internal.QQ
11+
import Language.Haskell.TH (stringE)
12+
import qualified Data.List.NonEmpty as NE
13+
import Data.Semigroup (All)
14+
import Data.Text.Lazy.Builder (Builder)
15+
import qualified Data.Text.Lazy.Builder as Builder
16+
import Data.ByteString (ByteString)
17+
import qualified Data.Text.Lazy.Builder.Int as Builder.Int
18+
19+
20+
-- test = do
21+
-- let t = "abc" :: Text
22+
-- [int|bac|] :: Text
23+
24+
25+
-- foo = do
26+
-- -- res <- runQ $ toExpPlain' Loc { loc_filename = "<interactive>", loc_package = "main", loc_module = "Main", loc_start = (1, 1), loc_end = (1, 1) } "abc" [] fmtConfig
27+
-- res <- runQ @IO $ toFormatPlain (RawPlain "abc")
28+
-- putStrLn (pprint $ res)
29+
30+
bar = $(
31+
-- s <- stringE "abc"
32+
-- toFormatPlain (ReplacementPlain (undefined, s))
33+
nonEmptyE (NE.singleton [| "abc" |])
34+
35+
) :: Text
36+
37+
baz :: Builder
38+
baz = do
39+
let t = 32 :: Int
40+
[int|abc{t}|]
41+
42+
43+
instance Interpolate Text Builder where
44+
interpolateInto = Builder.fromText
45+
46+
instance Interpolate Int Builder where
47+
interpolateInto = Builder.Int.decimal

0 commit comments

Comments
 (0)