summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorknupfer <>2017-09-13 16:33:00 (GMT)
committerhdiff <hdiff@hdiff.luite.com>2017-09-13 16:33:00 (GMT)
commit69c527fb1062047fcc4047cf791298861536a65f (patch)
treeab94a1e4af07ed6295de530027b6d62ac1b3328f
parent171a39e3921545b080709170689c912950510fdd (diff)
version 0.5.1.00.5.1.0
-rw-r--r--ChangeLog.md5
-rw-r--r--Readme.md51
-rw-r--r--src/Html/Convert.hs79
-rw-r--r--src/Html/Type.hs46
-rw-r--r--type-of-html.cabal3
5 files changed, 144 insertions, 40 deletions
diff --git a/ChangeLog.md b/ChangeLog.md
index 7952eb7..b360cd4 100644
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -1,5 +1,10 @@
# Revision history for type-of-html
+## 0.5.1.0 -- 2017-09-13
+
+* perf increase
+* better compile times
+
## 0.5.0.0 -- 2017-09-12
* type attributes
diff --git a/Readme.md b/Readme.md
index 4434b1a..ddc0902 100644
--- a/Readme.md
+++ b/Readme.md
@@ -1,6 +1,6 @@
# Type of html
-`Type of html` is a library for generating html in a highly
+`type-of-html` is a library for generating html in a highly
performant, modular and type safe manner.
Please look at the documentation of the module for an overview of the api:
@@ -110,10 +110,10 @@ page tds =
)
```
-Please note that the type of 'page' is inferable, so ask ghc-mod or
+Please note that the type of `page` is inferable, so ask ghc-mod or
whatever you use to write it for you. If you choose not to write the
types, you don't need the language extensions. I strongly suggest
-that you don't write signatures for bigger documents.
+that you don't write type signatures for `type-of-html`.
All text will be automatically html escaped:
@@ -137,17 +137,17 @@ into type-of-html.
## Performance
-`Type of html` is a lot faster than `blaze html` or than `lucid`.
+`type-of-html` is a lot faster than `blaze-html` or than `lucid`.
Look at the following benchmarks:
-Remember this benchmark from blaze?
+Remember this benchmark from `blaze-html`?
![blaze](https://jaspervdj.be/blaze/images/benchmarks-bigtable.png)
-This is comparing blaze with type of html:
+This is comparing blaze with `type-of-html`:
-![bench-324712b](https://user-images.githubusercontent.com/5609565/30344227-b4547230-9800-11e7-8c9d-6a8b8b5ab64d.png)
+![type-of-html](https://user-images.githubusercontent.com/5609565/30388159-2281182c-98af-11e7-8b29-aac26b7fbb57.png)
To look at the exact code of this benchmark look [here](bench/Main.hs)
in the repo. The big table benchmark here is only a 4x4 table. Using
@@ -157,9 +157,9 @@ unreadable.
How is this possible? We supercompile lots of parts of the generation
process. This is possible thanks to the new features of GHC 8.2:
-AppendSymbol. We represent tags as kinds and map these tags to (a ::
-[Symbol]) and then fold all neighbouring Proxies with
-AppendSymbol. Afterwards we retrieve the Proxies with symbolVal which
+AppendSymbol. We represent tags and attributes as kinds and map these
+to (a :: [Symbol]) and then fold all neighbouring Symbols with
+AppendSymbol. Afterwards we reify the Symbols with symbolVal which
will be embedded in the executable as Addr#. All this happens at
compile time. At runtime we do only generate the content and append
Builders.
@@ -176,7 +176,26 @@ doesn't exist for Builder, so it's slightly more complicated):
```haskell
decodeUtf8 $ toLazyByteString
( Data.ByteString.Builder.unpackCString# "<tr><td>"#
- <> escape (Data.Text.unpackCString# "test"#)
+ <> builderCString# "test"#
+ <> Data.ByteString.Builder.unpackCString# "</tr>"#
+ )
+```
+
+Note that the compiler automatically sees that your string literal
+doesn't need utf8 and converts directly the `"test"# :: Addr#` to an
+escaped Builder without any intermediate structure, not even an
+allocated bytestring.
+
+```haskell
+renderByteString $ tr_ (td_ "teſt")
+```
+
+Results in
+
+```haskell
+toLazyByteString
+ ( Data.ByteString.Builder.unpackCString# "<tr><td>"#
+ <> encodeUtf8BuilderEscaped prim (Data.Text.unpackCString# "te\\197\\191t"#)
<> Data.ByteString.Builder.unpackCString# "</tr>"#
)
```
@@ -198,20 +217,20 @@ compilation times.
## Comparision to lucid and blaze-html
-Advantages of 'type-of-html':
-- more or less 5 times faster
-- a lot higher type safety: a lot of invalid documents are not inhabited
+Advantages of `type-of-html`:
+- more or less 7 times faster
+- a lot higher type safety: nearly no invalid document is inhabited
- fewer dependencies
Disadvantages of 'type-of-html':
- a bit noisy syntax (don't write types!)
- sometimes unusual type error messages
-- compile times (1 min for a medium sized page, with -O0 only ~4sec)
+- compile times (30sec for a medium sized page, with `-O0` only ~3sec)
- needs at least ghc 8.2
I'd generally recommend that you put your documents into an extra
module to avoid frequent recompilations. Additionally you can use
-type-of-html within an blaze-html document and vice versa. This
+`type-of-html` within an `blaze-html` document and vice versa. This
allows you to gradually migrate, or only write the hotpath in a more
efficient representation.
diff --git a/src/Html/Convert.hs b/src/Html/Convert.hs
index 69757a7..8c19270 100644
--- a/src/Html/Convert.hs
+++ b/src/Html/Convert.hs
@@ -3,6 +3,8 @@
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE TypeOperators #-}
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE MagicHash #-}
module Html.Convert where
@@ -10,10 +12,14 @@ import Data.Word
import Data.Proxy
import Data.String
import GHC.TypeLits
+
import Html.Type
+import GHC.Prim (Addr#, ord#, indexCharOffAddr#)
+import GHC.Types
import Data.Char (ord)
+import qualified GHC.CString as GHC
import qualified Data.Monoid as M
import qualified Data.Semigroup as S
@@ -27,26 +33,41 @@ import qualified Data.Text.Encoding as T
import qualified Data.Text.Lazy as TL
import qualified Data.Text.Lazy.Encoding as TL
+{-# INLINE escapeUtf8 #-}
+escapeUtf8 :: BP.BoundedPrim Char
+escapeUtf8 =
+ BP.condB (> '>' ) BP.charUtf8 $
+ BP.condB (== '<' ) (fixed4 ('&',('l',('t',';')))) $
+ BP.condB (== '>' ) (fixed4 ('&',('g',('t',';')))) $
+ BP.condB (== '&' ) (fixed5 ('&',('a',('m',('p',';'))))) $
+ BP.condB (== '"' ) (fixed5 ('&',('#',('3',('4',';'))))) $
+ BP.condB (== '\'') (fixed5 ('&',('#',('3',('9',';'))))) $
+ BP.liftFixedToBounded BP.char7
+ where
+ {-# INLINE fixed4 #-}
+ fixed4 x = BP.liftFixedToBounded $ const x BP.>$<
+ BP.char7 BP.>*< BP.char7 BP.>*< BP.char7 BP.>*< BP.char7
+
+ {-# INLINE fixed5 #-}
+ fixed5 x = BP.liftFixedToBounded $ const x BP.>$<
+ BP.char7 BP.>*< BP.char7 BP.>*< BP.char7 BP.>*< BP.char7 BP.>*< BP.char7
+
{-# INLINE escape #-}
escape :: BP.BoundedPrim Word8
escape =
BP.condB (> c2w '>' ) (BP.liftFixedToBounded BP.word8) $
- BP.condB (== c2w '<' ) (fixed4 (c2w '&',(c2w 'l',(c2w 't',c2w ';')))) $ -- &lt;
- BP.condB (== c2w '>' ) (fixed4 (c2w '&',(c2w 'g',(c2w 't',c2w ';')))) $ -- &gt;
- BP.condB (== c2w '&' ) (fixed5 (c2w '&',(c2w 'a',(c2w 'm',(c2w 'p',c2w ';'))))) $ -- &amp;
- BP.condB (== c2w '"' ) (fixed5 (c2w '&',(c2w '#',(c2w '3',(c2w '4',c2w ';'))))) $ -- &#34;
- BP.condB (== c2w '\'') (fixed5 (c2w '&',(c2w '#',(c2w '3',(c2w '9',c2w ';'))))) $ -- &#39;
- BP.liftFixedToBounded BP.word8 -- fallback for Chars smaller than '>'
+ BP.condB (== c2w '<' ) (fixed4 (c2w '&',(c2w 'l',(c2w 't',c2w ';')))) $
+ BP.condB (== c2w '>' ) (fixed4 (c2w '&',(c2w 'g',(c2w 't',c2w ';')))) $
+ BP.condB (== c2w '&' ) (fixed5 (c2w '&',(c2w 'a',(c2w 'm',(c2w 'p',c2w ';'))))) $
+ BP.condB (== c2w '"' ) (fixed5 (c2w '&',(c2w '#',(c2w '3',(c2w '4',c2w ';'))))) $
+ BP.condB (== c2w '\'') (fixed5 (c2w '&',(c2w '#',(c2w '3',(c2w '9',c2w ';'))))) $
+ BP.liftFixedToBounded BP.word8
where
-
- {-# INLINE c2w #-}
c2w = fromIntegral . ord
- {-# INLINE fixed4 #-}
fixed4 x = BP.liftFixedToBounded $ const x BP.>$<
BP.word8 BP.>*< BP.word8 BP.>*< BP.word8 BP.>*< BP.word8
- {-# INLINE fixed5 #-}
fixed5 x = BP.liftFixedToBounded $ const x BP.>$<
BP.word8 BP.>*< BP.word8 BP.>*< BP.word8 BP.>*< BP.word8 BP.>*< BP.word8
@@ -101,7 +122,7 @@ instance Convert b => Convert (a := b) where
convert (AT x) = convert x
instance Convert (Raw String) where
{-# INLINE convert #-}
- convert (Raw x) = Converted (fromString x)
+ convert (Raw x) = stringConvRaw x
instance Convert (Raw T.Text) where
{-# INLINE convert #-}
convert (Raw x) = Converted (T.encodeUtf8Builder x)
@@ -110,7 +131,7 @@ instance Convert (Raw TL.Text) where
convert (Raw x) = Converted (TL.encodeUtf8Builder x)
instance Convert String where
{-# INLINE convert #-}
- convert = convert . T.pack
+ convert = stringConv
instance Convert T.Text where
{-# INLINE convert #-}
convert = Converted . T.encodeUtf8BuilderEscaped escape
@@ -135,3 +156,37 @@ instance Convert Word where
instance KnownSymbol a => Convert (Proxy a) where
{-# INLINE convert #-}
convert = Converted . U.byteStringCopy . fromString . symbolVal
+
+{-# INLINE builderCString# #-}
+builderCString# :: Addr# -> Converted
+builderCString# addr = Converted $ BP.primUnfoldrBounded escape go 0
+ where
+ go !i | b /= 0 = Just (fromIntegral b, i+1)
+ | otherwise = Nothing
+ where
+ !b = I# (ord# (at# i))
+ at# (I# i#) = indexCharOffAddr# addr i#
+
+{-# INLINE [0] stringConv #-}
+stringConv :: String -> Converted
+stringConv = Converted . BP.primMapListBounded escapeUtf8
+
+{-# INLINE [0] stringConvRaw #-}
+stringConvRaw :: String -> Converted
+stringConvRaw = Converted . B.stringUtf8
+
+{-# RULES "CONVERTED literal" forall a.
+ stringConv (GHC.unpackCString# a)
+ = builderCString# a #-}
+
+{-# RULES "CONVERTED literal raw" forall a.
+ stringConvRaw (GHC.unpackCString# a)
+ = Converted (U.byteStringCopy (fromString (GHC.unpackCString# a))) #-}
+
+{-# RULES "CONVERTED literal utf8" forall a.
+ stringConv (GHC.unpackCStringUtf8# a)
+ = convert (T.pack (GHC.unpackCStringUtf8# a)) #-}
+
+{-# RULES "CONVERTED literal utf8 raw" forall a.
+ stringConvRaw (GHC.unpackCStringUtf8# a)
+ = convert (Raw (T.pack (GHC.unpackCStringUtf8# a))) #-}
diff --git a/src/Html/Type.hs b/src/Html/Type.hs
index 2114707..47ca882 100644
--- a/src/Html/Type.hs
+++ b/src/Html/Type.hs
@@ -331,7 +331,7 @@ type family (a :: Element) ?> b :: Constraint where
a ?> f (b # c) = a ?> (b # c)
a ?> () = ()
a ?> (b -> c) = TypeError (Text "Html elements can't contain functions")
- a ?> b = CheckString a
+ a ?> b = CheckString a b
type family Null xs where
Null '[] = True
@@ -680,8 +680,13 @@ type family ToTypeList a where
ToTypeList x = '[Nothing]
-- | Append two type lists.
+--
+-- Note that this definition is that ugly to reduce compiletimes.
+-- Please check whether the context reduction stack or compiletimes of
+-- a big html page get bigger if you try to refactor.
type family Append xs ys :: [k] where
- Append xs '[] = xs
+ Append xs '[]
+ = xs
Append (x1 ': x2 ': x3 ': x4 ': x5 ': x6 ': x7 ': x8 ': x9 ': x10 ': x11 ': x12 ': x13 ': x14 ': x15 ': x16 ': x17 ': x18 ': x19 ': x20 ': x21 ': x22 ': x23 ': x24 ': x25 ': x26 ': x27 ': x28 ': x29 ': x30 ': x31 ': x32 ': x33 ': x34 ': x35 ': x36 ': x37 ': x38 ': x39 ': x40 ': x41 ': x42 ': x43 ': x44 ': x45 ': x46 ': x47 ': x48 ': x49 ': x50 ': x51 ': x52 ': x53 ': x54 ': x55 ': x56 ': x57 ': x58 ': x59 ': x60 ': x61 ': x62 ': x63 ': x64 ': xs) ys
= x1 ': x2 ': x3 ': x4 ': x5 ': x6 ': x7 ': x8 ': x9 ': x10 ': x11 ': x12 ': x13 ': x14 ': x15 ': x16 ': x17 ': x18 ': x19 ': x20 ': x21 ': x22 ': x23 ': x24 ': x25 ': x26 ': x27 ': x28 ': x29 ': x30 ': x31 ': x32 ': x33 ': x34 ': x35 ': x36 ': x37 ': x38 ': x39 ': x40 ': x41 ': x42 ': x43 ': x44 ': x45 ': x46 ': x47 ': x48 ': x49 ': x50 ': x51 ': x52 ': x53 ': x54 ': x55 ': x56 ': x57 ': x58 ': x59 ': x60 ': x61 ': x62 ': x63 ': x64 ': Append xs ys
@@ -704,8 +709,8 @@ type family Append xs ys :: [k] where
Append (x1 ': xs) ys
= x1 ': Append xs ys
- Append '[] ys = ys
-
+ Append '[] ys
+ = ys
-- | Check whether an element may have content.
type family HasContent a where
@@ -713,8 +718,12 @@ type family HasContent a where
HasContent _ = True
-- | Fuse neighbouring non empty type level strings.
+--
+-- Note that this definition is that ugly to reduce compiletimes.
+-- Please check whether the context reduction stack or compiletimes of
+-- a big html page get bigger if you try to refactor.
type family Fuse a where
- Fuse '[Just a, Nothing] = '[a, ""]
+ Fuse '[Just a, Nothing] = '[a, ""]
Fuse (Just x1 ': Nothing ': Just x2 ': Nothing ': x ': xs) = x1 ': x2 ': Fuse (x ': xs)
Fuse (Just x1 ': Just x2 ': Nothing ': Just x3 ': Nothing ': x ': xs) = AppendSymbol x1 x2 ': x3 ': Fuse (x ': xs)
Fuse (Just x1 ': Nothing ': Just x2 ': Just x3 ': Nothing ': x ': xs) = x1 ': AppendSymbol x2 x3 ': Fuse (x ': xs)
@@ -733,10 +742,15 @@ type family Fuse a where
Fuse (Just x1 ': Just x2 ': Just x3 ': Just x4 ': Just x5 ': Nothing ': x ': xs) = AppendSymbol x1 (AppendSymbol x2 (AppendSymbol x3 (AppendSymbol x4 x5))) ': Fuse (x ': xs)
Fuse (Just x1 ': Just x2 ': xs) = Fuse (Just (AppendSymbol x1 x2) ': xs)
- Fuse (Just a ': xs) = '[a]
+ Fuse (Just a ': xs) = '[a]
Fuse (Nothing ': xs) = "" ': Fuse xs
- Fuse '[] = '[]
+ Fuse '[] = '[]
+-- | Type level drop.
+--
+-- Note that this definition is that ugly to reduce compiletimes.
+-- Please check whether the context reduction stack or compiletimes of
+-- a big html page get bigger if you try to refactor.
type family Drop n xs :: [Symbol] where
Drop 0 xs = xs
Drop 1 (_ ': xs) = xs
@@ -745,6 +759,11 @@ type family Drop n xs :: [Symbol] where
Drop 4 (_ ': _ ': _ ': _ ': xs) = xs
Drop n (_ ': _ ': _ ': _ ': _ ': xs) = Drop (n-5) xs
+-- | Type level take.
+--
+-- Note that this definition is that ugly to reduce compiletimes.
+-- Please check whether the context reduction stack or compiletimes of
+-- a big html page get bigger if you try to refactor.
type family Take n xs :: [Symbol] where
Take 0 _ = '[]
Take 1 (x1 ': _) = '[x1]
@@ -754,6 +773,10 @@ type family Take n xs :: [Symbol] where
Take n (x1 ': x2 ': x3 ': x4 ': x5 ': xs) = x1 ': x2 ': x3 ': x4 ': x5 ': Take (n-5) xs
-- | Last for type level lists.
+--
+-- Note that this definition is that ugly to reduce compiletimes.
+-- Please check whether the context reduction stack or compiletimes of
+-- a big html page get bigger if you try to refactor.
type family Last (xs :: [Symbol]) where
Last (_ ': _ ': _ ': _ ': _ ': _ ': _ ': _ ': x ': xs) = Last (x ': xs)
Last (_ ': _ ': _ ': _ ': x ': xs) = Last (x ': xs)
@@ -777,10 +800,10 @@ type family CheckContentCategory (a :: ContentCategory) (b :: [ContentCategory])
CheckContentCategory a c = Elem a c
-- | Check whether a given element may contain a string.
-type family CheckString (a :: Element) where
- CheckString a = If (TestPaternity OnlyText (GetInfo a) (ElementInfo '[FlowContent, PhrasingContent] NoContent))
- (() :: Constraint)
- (TypeError (ShowType a :<>: Text " can't contain a string"))
+type family CheckString (a :: Element) b where
+ CheckString a b = If (TestPaternity OnlyText (GetInfo a) (ElementInfo '[FlowContent, PhrasingContent] NoContent))
+ (() :: Constraint)
+ (TypeError (ShowType a :<>: Text " can't contain a " :<>: ShowType b))
-- | Content categories according to the html spec.
data ContentCategory
@@ -818,6 +841,7 @@ newtype Tagged (proxies :: [Symbol]) target = Tagged target
type Symbols a = Fuse (ToTypeList a)
+-- | Get type list of valid elements for a given attribute. An empty list signifies global attribute.
type family GetAttributeInfo a where
GetAttributeInfo AcceptA = '[Form, Input]
GetAttributeInfo AcceptCharsetA = '[Form]
diff --git a/type-of-html.cabal b/type-of-html.cabal
index 126cae3..f36031e 100644
--- a/type-of-html.cabal
+++ b/type-of-html.cabal
@@ -1,5 +1,5 @@
name: type-of-html
-version: 0.5.0.0
+version: 0.5.1.0
synopsis: High performance type driven html generation.
description: This library makes most invalid html documents compile time errors and uses advanced type level features to realise compile time computations.
license: BSD3
@@ -31,6 +31,7 @@ library
build-depends: base >= 4.10 && < 4.11
, text
, bytestring
+ , ghc-prim
test-suite test
type: exitcode-stdio-1.0