string-interpolate 0.0.1.0 → 0.1.0.0
raw patch · 6 files changed
+445/−183 lines, 6 filesdep +Interpolationdep +interpolatedstring-perl6
Dependencies added: Interpolation, interpolatedstring-perl6
Files
- CHANGELOG.md +6/−0
- README.md +63/−80
- bench/Bench.hs +161/−34
- src/lib/Data/String/Interpolate/Conversion.hs +96/−1
- string-interpolate.cabal +7/−4
- test/Spec.hs +112/−64
CHANGELOG.md view
@@ -2,6 +2,12 @@ ## Unreleased +## v0.1.0.0 (2019-03-17)+++ Add support for using Text and ByteString `Builder`s as both sinks+ and sources++ Add support for interpolating Chars without the surrounding quotes+ ## v0.0.1.0 (2019-03-10) + Initial release
README.md view
@@ -1,4 +1,4 @@-# string-interpolate+# string-interpolate [](https://gitlab.com/williamyaoh/string-interpolate/commits/master) [](http://hackage.haskell.org/package/string-interpolate) [](https://gitlab.com/williamyaoh/string-interpolate/blob/master/LICENSE) Haskell having 5 different textual types in common use (String, strict and lazy Text, strict and lazy ByteString) means that doing any kind of string@@ -71,16 +71,35 @@ Wrap anything you want to be interpolated with `#{}`: ```haskell+λ> name = "William"+λ> [i|Hello, #{name}!|] :: String+>>> "Hello, William!"+```++You can interpolate in anything which implements `Show`:++```haskell λ> import Data.Time λ> now <- getCurrentTime λ> [i|The current time is #{now}.|] :: String >>> "The current time is 2019-03-10 18:58:40.573892546 UTC." ``` +...and interpolate into anything which implements `IsString`.+ string-interpolate *must* know what concrete type it's producing; it cannot be used to generate a `IsString a => a`. If you're using string-interpolate from GHCi, make sure to add type signatures to toplevel usages! +Strings and characters are always interpolated without surrounding quotes.++```haskell+λ> verb = 'c'+λ> noun = "sea"+λ> [i|We went to go #{verb} the #{noun}.|] :: String+>>> "We went to go c the sea."+```+ You can also interpolate arbitrary expressions: ```haskell@@ -105,96 +124,60 @@ * [formatting](https://hackage.haskell.org/package/formatting) * Text.Printf, from base * [neat-interpolation](https://hackage.haskell.org/package/neat-interpolation)+* [Interpolation](http://hackage.haskell.org/package/Interpolation)+* [interpolatedstring-perl6](http://hackage.haskell.org/package/interpolatedstring-perl6-1.0.1) Of these, Text.Printf isn't exception-safe, and neat-interpolation can only-produce Text values. interpolate and formatting solve the same problem of+produce Text values. interpolate, formatting, Interpolation, and+interpolatedstring-perl6 provide different solutions to the problem of providing a general way of interpolating any value, into any kind of text. ### Features -| | string-interpolate | interpolate | formatting |-|------------------------------------------|--------------------|-------------|------------|-| String/Text support | ✅ | ✅ | ✅ |-| ByteString support | ✅ | ✅ | ❌ |-| Can interpolate arbitrary Show instances | ✅ | ✅ | ✅ |-| Unicode-aware | ✅ | ❌ | ✅ |--### Performance--Overall: string-interpolate seems to be on-par with or faster than existing-interpolation libraries for all text cases.--Testing on my local machine, creating small (<100 character) Strings shows-string-interpolate as on-par with interpolate and significantly (> 20X) faster-than formatting:--```-benchmarking Small Strings Bench/string-interpolate-time 8.548 ns (8.543 ns .. 8.554 ns)- 1.000 R² (1.000 R² .. 1.000 R²)-mean 8.516 ns (8.504 ns .. 8.527 ns)-std dev 35.90 ps (29.23 ps .. 43.63 ps)--benchmarking Small Strings Bench/interpolate-time 9.047 ns (9.042 ns .. 9.052 ns)- 1.000 R² (1.000 R² .. 1.000 R²)-mean 9.028 ns (9.019 ns .. 9.036 ns)-std dev 27.06 ps (21.88 ps .. 34.20 ps)--benchmarking Small Strings Bench/formatting-time 222.3 ns (221.7 ns .. 223.1 ns)- 1.000 R² (1.000 R² .. 1.000 R²)-mean 222.1 ns (221.6 ns .. 222.8 ns)-std dev 1.902 ns (1.530 ns .. 2.446 ns)-```--I suspect the poor performance of formatting is caused by using `(++)` to-concatenate Strings instead of ShowS.--Doing the same test, but generating small Text and ByteStrings shows-string-interpolate to be on-par with formatting, and significantly (> 20x) faster-than interpolate.+| | string-interpolate | interpolate | formatting | Interpolation | interpolatedstring-perl6 |+|------------------------------------------ |-------------------- |------------- |------------ |--------------- |-------------------------- |+| String/Text support | ✅ | ✅ | ✅ | ⚠️ | ✅ |+| ByteString support | ✅ | ✅ | ❌ | ⚠️ | ✅ |+| Can interpolate arbitrary Show instances | ✅ | ✅ | ✅ | ✅ | ✅ |+| Unicode-aware | ✅ | ❌ | ✅ | ❌ | ❌ |+| Multiline strings | ❌ | ❌ | ❌ | ✅ | ✅ | -For Text:+⚠ `Interpolation` supports all five textual formats, but doesn't allow you+ to mix and match; that is, you can't interpolate a String into an output+ string of type Text, and vice versa. -```-benchmarking Small Text Bench/string-interpolate-time 173.7 ns (173.1 ns .. 174.1 ns)- 1.000 R² (1.000 R² .. 1.000 R²)-mean 173.1 ns (172.8 ns .. 173.5 ns)-std dev 1.230 ns (1.042 ns .. 1.486 ns)+### Performance -benchmarking Small Text Bench/interpolate-time 4.398 μs (4.371 μs .. 4.425 μs)- 1.000 R² (1.000 R² .. 1.000 R²)-mean 4.403 μs (4.389 μs .. 4.421 μs)-std dev 54.07 ns (41.42 ns .. 80.40 ns)+Overall: **string-interpolate** is competitive with the fastest interpolation+libraries, only getting outperformed by **Interpolation** and+**interpolatedstring-perl6**, and even then mostly on ByteStrings. Since+these two libraries don't handle Unicode and **string-interpolate** converts+things to UTF-8, some slowdown is to be expected here. -benchmarking Small Text Bench/formatting-time 243.1 ns (242.5 ns .. 243.8 ns)- 1.000 R² (1.000 R² .. 1.000 R²)-mean 242.8 ns (242.4 ns .. 243.4 ns)-std dev 1.665 ns (1.443 ns .. 1.982 ns)-```+We run three benchmarks: small string interpolation (<100 chars) with a single+interpolation parameter; small strings with multiple interpolation parameters,+and large string (~100KB) interpolation. Each of these benchmarks is then+run against `String`, strict `Text`, and strict `ByteString`. -For ByteString (formatting doesn't support ByteStrings):+| | **string-interpolate** | **interpolate** | **formatting** | **Interpolation** | **interpolatedstring-perl6** |+|-------------------------- |------------------------ |----------------- |---------------- |------------------- |------------------------------ |+| small String | 1x | 1x | 2x | 1x | 1x |+| multi interp, String | 1x | 7x | 2.3x | 0.63x | 0.63x |+| small Text | 1x | 28x | 1.5x | 2.2x | 2.2x |+| multi interp, Text | 1x | 22x | 1.6x | 2.9x | 2.9x |+| large Text | 1x | 30,000x | 1x | 80x | 80x |+| small ByteString | 1x | 15x | N/A | 0.35x | 0.35x |+| multi interp, ByteString | 1x | 10x | N/A | 0.5x | 0.5x |+| large ByteString | 1x | 100,000x | N/A | 1.6x | 1.6x | -```-benchmarking Small ByteString Bench/string-interpolate-time 297.6 ns (295.8 ns .. 299.8 ns)- 1.000 R² (0.999 R² .. 1.000 R²)-mean 299.8 ns (298.0 ns .. 302.3 ns)-std dev 7.176 ns (5.417 ns .. 9.651 ns)-variance introduced by outliers: 33% (moderately inflated)+(We don't bother running tests on large `String`s, because no one is working+with data that large using `String` anyways.) -benchmarking Small ByteString Bench/interpolate-time 4.389 μs (4.352 μs .. 4.424 μs)- 1.000 R² (0.999 R² .. 1.000 R²)-mean 4.374 μs (4.350 μs .. 4.398 μs)-std dev 79.48 ns (66.66 ns .. 96.14 ns)-variance introduced by outliers: 18% (moderately inflated)-```+In particular, notice that **Interpolation** and **interpolatedstring-perl6**+blow up on large Text; **string-interpolation** and **formatting** have+consistent performance across all benchmarks, with string-interpolation leading+the pack in `Text` cases. -Here interpolate is the poor performer, with the performance loss caused because-it converts all values to String before using `fromString` to convert them to-the target type.+All results were tested on my local machine. If you'd like to attempt to replicate+the results, the benchmarks are in `bench/` and can be run with a simple+`stack bench`.
bench/Bench.hs view
@@ -2,69 +2,196 @@ {-# LANGUAGE PackageImports #-} {-# LANGUAGE QuasiQuotes #-} -import Criterion ( bench, bgroup, whnf )+import Criterion ( bench, bgroup, env, nf ) import Criterion.Main ( defaultMain ) import qualified Data.ByteString as B import qualified Data.Text as T -import qualified "string-interpolate" Data.String.Interpolate as SI-import qualified "interpolate" Data.String.Interpolate.IsString as I-import "formatting" Formatting ( (%) )-import qualified "formatting" Formatting as F-import qualified "formatting" Formatting.ShortFormatters as F+import qualified "string-interpolate" Data.String.Interpolate as SI+import qualified "interpolate" Data.String.Interpolate.IsString as I+import "Interpolation" Data.String.Interpolation as N+import "formatting" Formatting ( (%) )+import qualified "formatting" Formatting as F+import qualified "formatting" Formatting.ShortFormatters as F+import "interpolatedstring-perl6" Text.InterpolatedString.Perl6 as P +import Test.QuickCheck+ ----------------------------------------------------------------------------------- Interpolating short Strings+-- Interpolating Strings -------------------------------------------------------------------------------- -shortStringSI :: String -> String-shortStringSI str = [SI.i|A fine day to die, #{str}.|]+stringSI :: String -> String+stringSI str = [SI.i|A fine day to die, #{str}.|] -shortStringI :: String -> String-shortStringI str = [I.i|A fine day to die, #{str}.|]+stringI :: String -> String+stringI str = [I.i|A fine day to die, #{str}.|] -shortStringF :: String -> String-shortStringF = F.formatToString ("A fine day to die, " % F.s % ".")+stringF :: String -> String+stringF = F.formatToString ("A fine day to die, " % F.s % ".") +stringN :: String -> String+stringN s = [str|A fine day to die, $s$.|]++stringP :: String -> String+stringP str = [qc|A fine day to die, {str}.|]+ ----------------------------------------------------------------------------------- Interpolating short Text+-- Interpolating Text -------------------------------------------------------------------------------- -shortTextSI :: T.Text -> T.Text-shortTextSI t = [SI.i|A fine day to die, #{t}.|]+textSI :: T.Text -> T.Text+textSI t = [SI.i|A fine day to die, #{t}.|] -shortTextI :: T.Text -> T.Text-shortTextI t = [I.i|A fine day to die, #{t}.|]+textI :: T.Text -> T.Text+textI t = [I.i|A fine day to die, #{t}.|] -shortTextF :: T.Text -> T.Text-shortTextF = F.sformat ("A fine day to die, " % F.st % ".")+textF :: T.Text -> T.Text+textF = F.sformat ("A fine day to die, " % F.st % ".") +textN :: T.Text -> T.Text+textN t = [str|A fine day to die, $t$.|]++textP :: T.Text -> T.Text+textP t = [qc|A fine day to die, {t}.|]+ ----------------------------------------------------------------------------------- Interpolating short ByteString+-- Interpolating ByteString -------------------------------------------------------------------------------- -shortByteStringSI :: B.ByteString -> B.ByteString-shortByteStringSI b = [SI.i|A fine day to die, #{b}.|]+byteStringSI :: B.ByteString -> B.ByteString+byteStringSI b = [SI.i|A fine day to die, #{b}.|] -shortByteStringI :: B.ByteString -> B.ByteString-shortByteStringI b = [I.i|A fine day to die, #{b}.|]+byteStringI :: B.ByteString -> B.ByteString+byteStringI b = [I.i|A fine day to die, #{b}.|] +byteStringN :: B.ByteString -> B.ByteString+byteStringN b = [str|A fine day to die, $b$.|]++byteStringP :: B.ByteString -> B.ByteString+byteStringP b = [qc|A fine day to die, {b}.|]++--------------------------------------------------------------------------------+-- Multiple String interpolations+--------------------------------------------------------------------------------++multiStringSI :: (Int, String, Bool) -> String+multiStringSI (x, y, z) = [SI.i| foo #{x} bar #{y} baz #{z} quux |]++multiStringI :: (Int, String, Bool) -> String+multiStringI (x, y, z) = [I.i| foo #{x} bar #{y} baz #{z} quux |]++multiStringF :: (Int, String, Bool) -> String+multiStringF (x, y, z) =+ F.formatToString (" foo " % F.d % " bar " % F.s % " baz " % F.sh % " quux ") x y z++multiStringN :: (Int, String, Bool) -> String+multiStringN (x, y, z) = [str| foo $:x$ bar $y$ baz $:z$ quux |]++multiStringP :: (Int, String, Bool) -> String+multiStringP (x, y, z) = [qc| foo {x} bar {y} baz {z} quux |]++--------------------------------------------------------------------------------+-- Multiple Text interpolations+--------------------------------------------------------------------------------++multiTextSI :: (Int, T.Text, Bool) -> T.Text+multiTextSI (x, y, z) = [SI.i| foo #{x} bar #{y} baz #{z} quux |]++multiTextI :: (Int, T.Text, Bool) -> T.Text+multiTextI (x, y, z) = [I.i| foo #{x} bar #{y} baz #{z} quux |]++multiTextF :: (Int, T.Text, Bool) -> T.Text+multiTextF (x, y, z) =+ F.sformat (" foo " % F.d % " bar " % F.st % " baz " % F.sh % " quux ") x y z++multiTextN :: (Int, T.Text, Bool) -> T.Text+multiTextN (x, y, z) = [str| foo $:x$ bar $y$ baz $:z$ quux |]++multiTextP :: (Int, T.Text, Bool) -> T.Text+multiTextP (x, y, z) = [qc| foo {x} bar {y} baz {z} quux |]++--------------------------------------------------------------------------------+-- Multiple ByteString interpolations+--------------------------------------------------------------------------------++multiByteStringSI :: (Int, B.ByteString, Bool) -> B.ByteString+multiByteStringSI (x, y, z) = [SI.i| foo #{x} bar #{y} baz #{z} quux |]++multiByteStringI :: (Int, B.ByteString, Bool) -> B.ByteString+multiByteStringI (x, y, z) = [I.i| foo #{x} bar #{y} baz #{z} quux |]++multiByteStringN :: (Int, B.ByteString, Bool) -> B.ByteString+multiByteStringN (x, y, z) = [str| foo $:x$ bar $y$ baz $:z$ quux |]++multiByteStringP :: (Int, B.ByteString, Bool) -> B.ByteString+multiByteStringP (x, y, z) = [qc| foo {x} bar {y} baz {z} quux |]+ main :: IO ()-main = defaultMain+main = defaultMain $ [ bgroup "Small Strings Bench" $- [ bench "string-interpolate" $ whnf shortStringSI "William"- , bench "interpolate" $ whnf shortStringI "William"- , bench "formatting" $ whnf shortStringF "William"+ [ bench "string-interpolate" $ nf stringSI "William"+ , bench "interpolate" $ nf stringI "William"+ , bench "formatting" $ nf stringF "William"+ , bench "Interpolation" $ nf stringN "William"+ , bench "interpolatedstring-perl6" $ nf stringP "William" ] , bgroup "Small Text Bench" $- [ bench "string-interpolate" $ whnf shortTextSI "William"- , bench "interpolate" $ whnf shortTextI "William"- , bench "formatting" $ whnf shortTextF "William"+ [ bench "string-interpolate" $ nf textSI "William"+ , bench "interpolate" $ nf textI "William"+ , bench "formatting" $ nf textF "William"+ , bench "Interpolation" $ nf textN "William"+ , bench "interpolatedstring-perl6" $ nf textP "William" ] , bgroup "Small ByteString Bench" $- [ bench "string-interpolate" $ whnf shortByteStringSI "William"- , bench "interpolate" $ whnf shortByteStringI "William"+ [ bench "string-interpolate" $ nf byteStringSI "William"+ , bench "interpolate" $ nf byteStringI "William" -- "formatting" doesn't support ByteStrings.+ , bench "Interpolation" $ nf byteStringN "William"+ , bench "interpolatedstring-perl6" $ nf byteStringP "William" ]+ , bgroup "Multiple Interpolations String Bench" $+ [ bench "string-interpolate" $ nf multiStringSI (42, "CATALLAXY", True)+ , bench "interpolate" $ nf multiStringI (42, "CATALLAXY", True)+ , bench "formatting" $ nf multiStringF (42, "CATALLAXY", True)+ , bench "Interpolation" $ nf multiStringN (42, "CATALLAXY", True)+ , bench "interpolatedstring-perl6" $ nf multiStringP (42, "CATALLAXY", True)+ ]+ , bgroup "Multiple Interpolations Text Bench" $+ [ bench "string-interpolate" $ nf multiTextSI (42, "CATALLAXY", True)+ , bench "interpolate" $ nf multiTextI (42, "CATALLAXY", True)+ , bench "formatting" $ nf multiTextF (42, "CATALLAXY", True)+ , bench "Interpolation" $ nf multiTextN (42, "CATALLAXY", True)+ , bench "interpolatedstring-perl6" $ nf multiTextP (42, "CATALLAXY", True)+ ]+ , bgroup "Multiple Interpolations ByteString Bench" $+ [ bench "string-interpolate" $ nf multiByteStringSI (42, "CATALLAXY", True)+ , bench "interpolate" $ nf multiByteStringI (42, "CATALLAXY", True)+ -- "formatting" doesn't support ByteStrings.+ , bench "Interpolation" $ nf multiByteStringN (42, "CATALLAXY", True)+ , bench "interpolatedstring-perl6" $ nf multiByteStringP (42, "CATALLAXY", True)+ ]+ , env largeishText $ \ ~t -> bgroup "Largeish Text Bench" $+ [ bench "string-interpolate" $ nf textSI t+ , bench "interpolate" $ nf textI t+ , bench "formatting" $ nf textF t+ , bench "Interpolation" $ nf textN t+ , bench "interpolatedstring-perl6" $ nf textP t+ ]+ , env largeishByteString $ \ ~bs -> bgroup "Largeish ByteString Bench" $+ [ bench "string-interpolate" $ nf byteStringSI bs+ , bench "interpolate" $ nf byteStringI bs+ -- "formatting" doesn't support ByteStrings.+ , bench "Interpolation" $ nf byteStringN bs+ , bench "interpolatedstring-perl6" $ nf byteStringP bs+ ] ]++largeishText :: IO T.Text+largeishText =+ generate $ T.pack <$> Prelude.take 100000 <$> infiniteListOf arbitrary++largeishByteString :: IO B.ByteString+largeishByteString =+ generate $ B.pack <$> Prelude.take 100000 <$> infiniteListOf arbitrary
src/lib/Data/String/Interpolate/Conversion.hs view
@@ -24,7 +24,7 @@ import qualified "utf8-string" Data.ByteString.UTF8 as UTF8 import "base" Text.Read ( readMaybe )-import "base" Text.Show ( ShowS, showString )+import "base" Text.Show ( ShowS, showString, showChar ) -- | -- We wrap the builders in B so that we can add a phantom type parameter.@@ -37,8 +37,10 @@ type family IsCustomSink dst where IsCustomSink T.Text = 'True IsCustomSink LT.Text = 'True+ IsCustomSink LT.Builder = 'True IsCustomSink B.ByteString = 'True IsCustomSink LB.ByteString = 'True+ IsCustomSink LB.Builder = 'True IsCustomSink _ = 'False -- | Something that can be interpolated into.@@ -80,6 +82,13 @@ build _ (B l) (B r) = B $ l <> r finalize _ = LT.toLazyText . unB +instance InterpSink 'True LT.Builder where+ type Builder 'True LT.Builder = LT.Builder++ ofString _ = B . LT.fromString+ build _ (B l) (B r) = B $ l <> r+ finalize _ = unB+ instance InterpSink 'True B.ByteString where type Builder 'True B.ByteString = LB.Builder @@ -94,71 +103,146 @@ build _ (B l) (B r) = B $ l <> r finalize _ = LB.toLazyByteString . unB +instance InterpSink 'True LB.Builder where+ type Builder 'True LB.Builder = LB.Builder++ ofString _ = B . LB.lazyByteString . unUTF8 . convertText+ build _ (B l) (B r) = B $ l <> r+ finalize _ = unB+ instance {-# OVERLAPPABLE #-} (Show src, IsString dst, IsCustomSink dst ~ 'False) => Interpolatable 'False src dst where interpolate _ = B . shows+instance {-# OVERLAPS #-} (IsString dst, IsCustomSink dst ~ 'False) => Interpolatable 'False Char dst where+ interpolate _ = B . showChar instance {-# OVERLAPS #-} (IsString dst, IsCustomSink dst ~ 'False) => Interpolatable 'False String dst where interpolate _ = B . showString instance {-# OVERLAPS #-} (IsString dst, IsCustomSink dst ~ 'False) => Interpolatable 'False T.Text dst where interpolate _ = B . showString . T.unpack instance {-# OVERLAPS #-} (IsString dst, IsCustomSink dst ~ 'False) => Interpolatable 'False LT.Text dst where interpolate _ = B . showString . LT.unpack+instance {-# OVERLAPS #-} (IsString dst, IsCustomSink dst ~ 'False) => Interpolatable 'False LT.Builder dst where+ interpolate _ = B . showString . LT.unpack . LT.toLazyText instance {-# OVERLAPS #-} (IsString dst, IsCustomSink dst ~ 'False) => Interpolatable 'False B.ByteString dst where interpolate _ = B . showString . UTF8.toString instance {-# OVERLAPS #-} (IsString dst, IsCustomSink dst ~ 'False) => Interpolatable 'False LB.ByteString dst where interpolate _ = B . showString . LUTF8.toString+instance {-# OVERLAPS #-} (IsString dst, IsCustomSink dst ~ 'False) => Interpolatable 'False LB.Builder dst where+ interpolate _ = B . showString . LUTF8.toString . LB.toLazyByteString instance {-# OVERLAPPABLE #-} Show src => Interpolatable 'True src T.Text where interpolate _ = B . LT.fromString . show+instance {-# OVERLAPS #-} Interpolatable 'True Char T.Text where+ interpolate _ = B . LT.singleton instance {-# OVERLAPS #-} Interpolatable 'True String T.Text where interpolate _ = B . LT.fromString instance {-# OVERLAPS #-} Interpolatable 'True T.Text T.Text where interpolate _ = B . LT.fromText instance {-# OVERLAPS #-} Interpolatable 'True LT.Text T.Text where interpolate _ = B . LT.fromLazyText+instance {-# OVERLAPS #-} Interpolatable 'True LT.Builder T.Text where+ interpolate _ = B instance {-# OVERLAPS #-} Interpolatable 'True B.ByteString T.Text where interpolate _ = B . bsToTextBuilder instance {-# OVERLAPS #-} Interpolatable 'True LB.ByteString T.Text where interpolate _ = B . lbsToTextBuilder+instance {-# OVERLAPS #-} Interpolatable 'True LB.Builder T.Text where+ interpolate _ = B . lbsToTextBuilder . LB.toLazyByteString instance {-# OVERLAPPABLE #-} Show src => Interpolatable 'True src LT.Text where interpolate _ = B . LT.fromString . show+instance {-# OVERLAPS #-} Interpolatable 'True Char LT.Text where+ interpolate _ = B . LT.singleton instance {-# OVERLAPS #-} Interpolatable 'True String LT.Text where interpolate _ = B . LT.fromString instance {-# OVERLAPS #-} Interpolatable 'True T.Text LT.Text where interpolate _ = B . LT.fromText instance {-# OVERLAPS #-} Interpolatable 'True LT.Text LT.Text where interpolate _ = B . LT.fromLazyText+instance {-# OVERLAPS #-} Interpolatable 'True LT.Builder LT.Text where+ interpolate _ = B instance {-# OVERLAPS #-} Interpolatable 'True B.ByteString LT.Text where interpolate _ = B . bsToTextBuilder instance {-# OVERLAPS #-} Interpolatable 'True LB.ByteString LT.Text where interpolate _ = B . lbsToTextBuilder+instance {-# OVERLAPS #-} Interpolatable 'True LB.Builder LT.Text where+ interpolate _ = B . lbsToTextBuilder . LB.toLazyByteString +instance {-# OVERLAPPABLE #-} Show src => Interpolatable 'True src LT.Builder where+ interpolate _ = B . LT.fromString . show+instance {-# OVERLAPS #-} Interpolatable 'True Char LT.Builder where+ interpolate _ = B . LT.singleton+instance {-# OVERLAPS #-} Interpolatable 'True String LT.Builder where+ interpolate _ = B . LT.fromString+instance {-# OVERLAPS #-} Interpolatable 'True T.Text LT.Builder where+ interpolate _ = B . LT.fromText+instance {-# OVERLAPS #-} Interpolatable 'True LT.Text LT.Builder where+ interpolate _ = B . LT.fromLazyText+instance {-# OVERLAPS #-} Interpolatable 'True LT.Builder LT.Builder where+ interpolate _ = B+instance {-# OVERLAPS #-} Interpolatable 'True B.ByteString LT.Builder where+ interpolate _ = B . bsToTextBuilder+instance {-# OVERLAPS #-} Interpolatable 'True LB.ByteString LT.Builder where+ interpolate _ = B . lbsToTextBuilder+instance {-# OVERLAPS #-} Interpolatable 'True LB.Builder LT.Builder where+ interpolate _ = B . lbsToTextBuilder . LB.toLazyByteString+ instance {-# OVERLAPPABLE #-} Show src => Interpolatable 'True src B.ByteString where interpolate _ = B . LB.byteString . unUTF8 . convertText . show+instance {-# OVERLAPS #-} Interpolatable 'True Char B.ByteString where+ interpolate _ = B . encodeCharUTF8 instance {-# OVERLAPS #-} Interpolatable 'True String B.ByteString where interpolate _ = B . LB.byteString . unUTF8 . convertText instance {-# OVERLAPS #-} Interpolatable 'True T.Text B.ByteString where interpolate _ = B . LB.byteString . unUTF8 . convertText instance {-# OVERLAPS #-} Interpolatable 'True LT.Text B.ByteString where interpolate _ = B . LB.byteString . unUTF8 . convertText+instance {-# OVERLAPS #-} Interpolatable 'True LT.Builder B.ByteString where+ interpolate _ = B . LB.byteString . unUTF8 . convertText . LT.toLazyText instance {-# OVERLAPS #-} Interpolatable 'True B.ByteString B.ByteString where interpolate _ = B . LB.byteString instance {-# OVERLAPS #-} Interpolatable 'True LB.ByteString B.ByteString where interpolate _ = B . LB.lazyByteString+instance {-# OVERLAPS #-} Interpolatable 'True LB.Builder B.ByteString where+ interpolate _ = B instance {-# OVERLAPPABLE #-} Show src => Interpolatable 'True src LB.ByteString where interpolate _ = B . LB.lazyByteString . unUTF8 . convertText . show+instance {-# OVERLAPS #-} Interpolatable 'True Char LB.ByteString where+ interpolate _ = B . encodeCharUTF8 instance {-# OVERLAPS #-} Interpolatable 'True String LB.ByteString where interpolate _ = B . LB.lazyByteString . unUTF8 . convertText instance {-# OVERLAPS #-} Interpolatable 'True T.Text LB.ByteString where interpolate _ = B . LB.lazyByteString . unUTF8 . convertText instance {-# OVERLAPS #-} Interpolatable 'True LT.Text LB.ByteString where interpolate _ = B . LB.lazyByteString . unUTF8 . convertText+instance {-# OVERLAPS #-} Interpolatable 'True LT.Builder LB.ByteString where+ interpolate _ = B . LB.lazyByteString . unUTF8 . convertText . LT.toLazyText instance {-# OVERLAPS #-} Interpolatable 'True B.ByteString LB.ByteString where interpolate _ = B . LB.byteString instance {-# OVERLAPS #-} Interpolatable 'True LB.ByteString LB.ByteString where interpolate _ = B . LB.lazyByteString+instance {-# OVERLAPS #-} Interpolatable 'True LB.Builder LB.ByteString where+ interpolate _ = B +instance {-# OVERLAPPABLE #-} Show src => Interpolatable 'True src LB.Builder where+ interpolate _ = B . LB.lazyByteString . unUTF8 . convertText . show+instance {-# OVERLAPS #-} Interpolatable 'True Char LB.Builder where+ interpolate _ = B . encodeCharUTF8+instance {-# OVERLAPS #-} Interpolatable 'True String LB.Builder where+ interpolate _ = B . LB.lazyByteString . unUTF8 . convertText+instance {-# OVERLAPS #-} Interpolatable 'True T.Text LB.Builder where+ interpolate _ = B . LB.lazyByteString . unUTF8 . convertText+instance {-# OVERLAPS #-} Interpolatable 'True LT.Text LB.Builder where+ interpolate _ = B . LB.lazyByteString . unUTF8 . convertText+instance {-# OVERLAPS #-} Interpolatable 'True LT.Builder LB.Builder where+ interpolate _ = B . LB.lazyByteString . unUTF8 . convertText . LT.toLazyText+instance {-# OVERLAPS #-} Interpolatable 'True B.ByteString LB.Builder where+ interpolate _ = B . LB.byteString+instance {-# OVERLAPS #-} Interpolatable 'True LB.ByteString LB.Builder where+ interpolate _ = B . LB.lazyByteString+instance {-# OVERLAPS #-} Interpolatable 'True LB.Builder LB.Builder where+ interpolate _ = B+ -- | -- Convert a strict ByteString into a Text `LT.Builder', converting any invalid -- characters into the Unicode replacement character � (U+FFFD).@@ -170,3 +254,14 @@ -- characters into the Unicode replacement character � (U+FFFD). lbsToTextBuilder :: LB.ByteString -> LT.Builder lbsToTextBuilder = LUTF8.foldr (\char bldr -> LT.singleton char <> bldr) mempty++-- |+-- "Data.ByteString.Builder" provides `charUtf8' to do this, but it doesn't+-- correctly handle invalid characters.+encodeCharUTF8 :: Char -> LB.Builder+encodeCharUTF8 c =+ let normalized = case c of+ '\xFFFE' -> '\xFFFD'+ '\xFFFF' -> '\xFFFD'+ _ -> c+ in LB.charUtf8 normalized
string-interpolate.cabal view
@@ -4,11 +4,11 @@ -- -- see: https://github.com/sol/hpack ----- hash: e19a345342449e9beb74add05e63adf5a16f1e6899912ae637a1b6f8a4bca51e+-- hash: e5e0c8e769850dc5f6451b53b84d5b2e9afce9ef6393e668d971ef3bfe810401 name: string-interpolate-version: 0.0.1.0-synopsis: Haskell string interpolation that just works+version: 0.1.0.0+synopsis: Haskell string/text/bytestring interpolation that just works description: Unicode-aware string interpolation that handles all textual types. . See the README at <https://gitlab.com/williamyaoh/string-interpolate.git#string-interpolate> for more info.@@ -69,11 +69,14 @@ hs-source-dirs: bench build-depends:- base ==4.*+ Interpolation+ , QuickCheck+ , base ==4.* , bytestring , criterion , formatting , interpolate+ , interpolatedstring-perl6 , string-interpolate , text default-language: Haskell2010
test/Spec.hs view
@@ -11,9 +11,8 @@ import qualified Data.ByteString.Builder as LB import Data.Foldable ( foldMap ) -import "quickcheck-text" Data.Text.Arbitrary- import "hspec" Test.Hspec+import "hspec" Test.Hspec.QuickCheck import "QuickCheck" Test.QuickCheck import "quickcheck-instances" Test.QuickCheck.Instances.ByteString @@ -21,162 +20,211 @@ main :: IO () main = hspec $ parallel $ do- describe "i" $ do+ describe "i" $ modifyMaxSuccess (const 10000) $ modifyMaxSize (const 500) $ do context "when using String as a parameter" $ do- it "just interpolating should be id" $- property $ \(str :: String) -> [i|#{str}|] == str+ prop "just interpolating should be id" $+ \(UTF8S str) -> [i|#{str}|] == str - it "should passthrough a conversion to strict Text and back unchanged" $- property $ \(str :: String) ->+ prop "should passthrough a conversion to strict Text and back unchanged" $+ \(UTF8S str) -> let t = [i|#{str}|] :: T.Text str' = [i|#{t}|] :: String in str' == str - it "should passthrough a conversion to lazy Text and back unchanged" $- property $ \(str :: String) ->+ prop "should passthrough a conversion to lazy Text and back unchanged" $+ \(UTF8S str) -> let lt = [i|#{str}|] :: LT.Text str' = [i|#{lt}|] :: String in str' == str - it "should passthrough a conversion to strict ByteString and back unchanged" $- property $ \(str :: String) ->+ prop "should passthrough a conversion to strict ByteString and back unchanged" $+ \(UTF8S str) -> let b = [i|#{str}|] :: B.ByteString str' = [i|#{b}|] :: String in str' == str - it "should passthrough a conversion to lazy ByteString and back unchanged" $- property $ \(str :: String) ->+ prop "should passthrough a conversion to lazy ByteString and back unchanged" $+ \(UTF8S str) -> let lb = [i|#{str}|] :: LB.ByteString str' = [i|#{lb}|] :: String in str' == str context "when using strict Text as a parameter" $ do- it "just interpolating should be id" $- property $ \(t :: T.Text) -> [i|#{t}|] == t+ prop "just interpolating should be id" $+ \(t :: T.Text) -> [i|#{t}|] == t - it "should passthrough a conversion to String and back unchanged" $- property $ \(t :: T.Text) ->+ prop "should passthrough a conversion to String and back unchanged" $+ \(t :: T.Text) -> let str = [i|#{t}|] :: String t' = [i|#{str}|] :: T.Text in t' == t - it "should passthrough a conversion to lazy Text and back unchanged" $- property $ \(t :: T.Text) ->+ prop "should passthrough a conversion to lazy Text and back unchanged" $+ \(t :: T.Text) -> let lt = [i|#{t}|] :: LT.Text t' = [i|#{lt}|] :: T.Text in t' == t - it "should passthrough a conversion to strict ByteString and back unchanged" $- property $ \(t :: T.Text) ->+ prop "should passthrough a conversion to strict ByteString and back unchanged" $+ \(t :: T.Text) -> let b = [i|#{t}|] :: B.ByteString t' = [i|#{b}|] :: T.Text in t' == t - it "should passthrough a conversion to lazy ByteString and back unchanged" $- property $ \(t :: T.Text) ->+ prop "should passthrough a conversion to lazy ByteString and back unchanged" $+ \(t :: T.Text) -> let lb = [i|#{t}|] :: LB.ByteString t' = [i|#{lb}|] :: T.Text in t' == t context "when using lazy Text as a parameter" $ do- it "just interpolating should be id" $- property $ \(lt :: LT.Text) -> [i|#{lt}|] == lt+ prop "just interpolating should be id" $+ \(lt :: LT.Text) -> [i|#{lt}|] == lt - it "should passthrough a conversion to String and back unchanged" $- property $ \(lt :: LT.Text) ->+ prop "should passthrough a conversion to String and back unchanged" $+ \(lt :: LT.Text) -> let str = [i|#{lt}|] :: String lt' = [i|#{str}|] :: LT.Text in lt' == lt - it "should passthrough a conversion to strict Text and back unchanged" $- property $ \(lt :: LT.Text) ->+ prop "should passthrough a conversion to strict Text and back unchanged" $+ \(lt :: LT.Text) -> let t = [i|#{lt}|] :: T.Text lt' = [i|#{t}|] :: LT.Text in lt' == lt - it "should passthrough a conversion to strict ByteString and back unchanged" $- property $ \(lt :: LT.Text) ->+ prop "should passthrough a conversion to strict ByteString and back unchanged" $+ \(lt :: LT.Text) -> let b = [i|#{lt}|] :: B.ByteString lt' = [i|#{b}|] :: LT.Text in lt' == lt - it "should passthrough a conversion to lazy ByteString and back unchanged" $- property $ \(lt :: LT.Text) ->+ prop "should passthrough a conversion to lazy ByteString and back unchanged" $+ \(lt :: LT.Text) -> let lb = [i|#{lt}|] :: LB.ByteString lt' = [i|#{lb}|] :: LT.Text in lt' == lt context "when using strict ByteString as a parameter" $ do- it "just interpolating should be id" $- property $ \(b :: B.ByteString) -> [i|#{b}|] == b+ prop "just interpolating should be id" $+ \(b :: B.ByteString) -> [i|#{b}|] == b - it "should passthrough a conversion to lazy ByteString and back unchanged" $- property $ \(b :: B.ByteString) ->+ prop "should passthrough a conversion to lazy ByteString and back unchanged" $+ \(b :: B.ByteString) -> let lb = [i|#{b}|] :: LB.ByteString b' = [i|#{lb}|] :: B.ByteString in b' == b context "and the ByteString is valid UTF8" $ do- it "should passthrough a conversion to String and back unchanged" $ do- property $ \(UTF8 b) ->+ prop "should passthrough a conversion to String and back unchanged" $ do+ \(UTF8BS b) -> let str = [i|#{b}|] :: String b' = [i|#{str}|] in b' == b - it "should passthrough a conversion to strict Text and back unchanged" $ do- property $ \(UTF8 b) ->+ prop "should passthrough a conversion to strict Text and back unchanged" $ do+ \(UTF8BS b) -> let t = [i|#{b}|] :: T.Text b' = [i|#{t}|] in b' == b - it "should passthrough a conversion to lazy Text and back unchanged" $ do- property $ \(UTF8 b) ->+ prop "should passthrough a conversion to lazy Text and back unchanged" $ do+ \(UTF8BS b) -> let lt = [i|#{b}|] :: LT.Text b' = [i|#{lt}|] in b' == b context "when using lazy ByteString as a parameter" $ do- it "just interpolating should be id" $- property $ \(lb :: LB.ByteString) -> [i|#{lb}|] == lb+ prop "just interpolating should be id" $+ \(lb :: LB.ByteString) -> [i|#{lb}|] == lb - it "should passthrough a conversion to strict ByteString and back unchanged" $- property $ \(lb :: LB.ByteString) ->+ prop "should passthrough a conversion to strict ByteString and back unchanged" $+ \(lb :: LB.ByteString) -> let b = [i|#{lb}|] :: B.ByteString lb' = [i|#{b}|] :: LB.ByteString in lb' == lb context "and the ByteString is valid UTF8" $ do- it "should passthrough a conversion to String and back unchanged" $- property $ \(LUTF8 lb) ->+ prop "should passthrough a conversion to String and back unchanged" $+ \(UTF8LBS lb) -> let str = [i|#{lb}|] :: String lb' = [i|#{str}|] in lb' == lb - it "should passthrough a conversion to strict Text and back unchanged" $- property $ \(LUTF8 lb) ->+ prop "should passthrough a conversion to strict Text and back unchanged" $+ \(UTF8LBS lb) -> let t = [i|#{lb}|] :: T.Text lb' = [i|#{t}|] in lb' == lb - it "should passthrough a conversion to lazy Text and back unchanged" $- property $ \(LUTF8 lb) ->+ prop "should passthrough a conversion to lazy Text and back unchanged" $+ \(UTF8LBS lb) -> let lt = [i|#{lb}|] :: LT.Text lb' = [i|#{lt}|] in lb' == lb -newtype UTF8ByteString = UTF8 { unUTF8 :: B.ByteString }+ context "when using Char as a parameter" $ do+ prop "interpolating into a String shouldn't have quotes" $+ \(UTF8C c) -> [i|#{c}|] == [c]++ prop "interpolating into strict Text shouldn't have quotes" $+ \(UTF8C c) -> [i|#{c}|] == T.singleton c++ prop "interpolating into lazy Text shouldn't have quotes" $+ \(UTF8C c) -> [i|#{c}|] == LT.singleton c++ prop "interpolating into strict ByteString shouldn't have quotes" $+ \(UTF8C c) -> [i|#{c}|] == (LB.toStrict $ LB.toLazyByteString $ LB.charUtf8 c)++ prop "interpolating into lazy ByteString shouldn't have quotes" $+ \(UTF8C c) -> [i|#{c}|] == (LB.toLazyByteString $ LB.charUtf8 c)++-- |+-- The default Arbitrary for Char generates U+FFFF and U+FFFE, which aren't+-- valid Unicode. Sigh...+newtype UTF8Char = UTF8C { unUTF8C :: Char } deriving newtype (Eq, Show)-newtype LUTF8ByteString = LUTF8 { unLUTF8 :: LB.ByteString }++newtype UTF8String = UTF8S { unUTF8S :: String } deriving newtype (Eq, Show) -instance Arbitrary LUTF8ByteString where- arbitrary = LUTF8 . LB.toLazyByteString . foldMap LB.charUtf8 <$> arbitrary @[Char]-instance Arbitrary UTF8ByteString where- arbitrary = UTF8 . LB.toStrict . LB.toLazyByteString . foldMap LB.charUtf8 <$>- arbitrary @[Char]+newtype UTF8ByteString = UTF8BS { unUTF8BS :: B.ByteString }+ deriving newtype (Eq, Show)+newtype UTF8LazyByteString = UTF8LBS { unUTF8LBS :: LB.ByteString }+ deriving newtype (Eq, Show) --- Why the hell doesn't `quickcheck-text' not provide this--- instance already? I don't know. Don't ask me.+instance Arbitrary UTF8Char where+ arbitrary = UTF8C <$> arbitrary `suchThat` (\c -> c /= '\xFFFE' && c /= '\xFFFF')+ shrink (UTF8C c) = UTF8C <$> shrink c++instance Arbitrary UTF8String where+ arbitrary = do+ chars <- listOf arbitrary+ pure $ UTF8S (unUTF8C <$> chars)+ shrink (UTF8S str) = case str of+ [] -> []+ (_:[]) -> []+ _ -> let mid = length str `div` 2+ in [UTF8S $ take mid str, UTF8S $ drop mid str]++instance Arbitrary T.Text where+ arbitrary = T.pack . unUTF8S <$> arbitrary+ shrink t = if T.null t || T.length t == 1+ then []+ else let mid = T.length t `div` 2+ in [T.take mid t, T.drop mid t]+ instance Arbitrary LT.Text where- arbitrary = LT.fromStrict <$> arbitrary- shrink t = LT.fromStrict <$> shrink (LT.toStrict t)+ arbitrary = LT.pack . unUTF8S <$> arbitrary+ shrink lt = if LT.null lt || LT.length lt == 1+ then []+ else let mid = LT.length lt `div` 2+ in [LT.take mid lt, LT.drop mid lt]++instance Arbitrary UTF8ByteString where+ arbitrary = UTF8BS . LB.toStrict . LB.toLazyByteString . foldMap LB.charUtf8 . unUTF8S+ <$> arbitrary++instance Arbitrary UTF8LazyByteString where+ arbitrary = UTF8LBS . LB.toLazyByteString . foldMap LB.charUtf8 . unUTF8S+ <$> arbitrary