Skip to content

Commit 9a48ced

Browse files
authored
Merge pull request #1045 from haskell/nothunks
Produce Value in normal form in D.A.Decoding.
2 parents f2073e7 + fbdcc9b commit 9a48ced

File tree

9 files changed

+162
-23
lines changed

9 files changed

+162
-23
lines changed

aeson.cabal

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,11 +183,13 @@ test-suite aeson-tests
183183
UnitTests.OptionalFields.Manual
184184
UnitTests.OptionalFields.TH
185185
UnitTests.UTCTime
186+
UnitTests.NoThunks
186187

187188
build-depends:
188189
aeson
189190
, base
190191
, base-compat
192+
, deepseq
191193
, base-orphans >=0.5.3 && <0.10
192194
, base16-bytestring
193195
, bytestring
@@ -225,6 +227,9 @@ test-suite aeson-tests
225227
, uuid-types
226228
, vector
227229

230+
if impl(ghc >=9.2 && <9.7)
231+
build-depends: nothunks >=0.1.4 && <0.2
232+
228233
source-repository head
229234
type: git
230235
location: git://github.com/haskell/aeson.git

benchmarks/aeson-benchmarks.cabal

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ library
2626
default-language: Haskell2010
2727
build-depends: base
2828

29-
executable aeson-benchmark-suite
29+
executable aeson-bench
3030
default-language: Haskell2010
3131
main-is: aeson-benchmark-suite.hs
3232
hs-source-dirs: bench examples/src

benchmarks/bench/CompareWithJSON.hs

Lines changed: 51 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,27 @@ decode' :: BL.ByteString -> A.Value
4747
decode' s = fromMaybe (error "fail to parse via Aeson") $ A.decode' s
4848

4949
decodeS :: BS.ByteString -> A.Value
50-
decodeS s = fromMaybe (error "fail to parse via Aeson") $ A.decodeStrict' s
50+
decodeS s = fromMaybe (error "fail to parse via Aeson") $ A.decodeStrict s
51+
52+
decodeS' :: BS.ByteString -> A.Value
53+
decodeS' s = fromMaybe (error "fail to parse via Aeson") $ A.decodeStrict' s
5154

5255
decodeAtto :: BL.ByteString -> A.Value
5356
decodeAtto s = fromMaybe (error "fail to parse via Parser.decodeWith") $
5457
I.decodeWith I.jsonEOF A.fromJSON s
5558

59+
decodeAtto' :: BL.ByteString -> A.Value
60+
decodeAtto' s = fromMaybe (error "fail to parse via Parser.decodeWith") $
61+
I.decodeWith I.jsonEOF' A.fromJSON s
62+
63+
decodeAttoS :: BS.ByteString -> A.Value
64+
decodeAttoS s = fromMaybe (error "fail to parse via Parser.decodeWith") $
65+
I.decodeStrictWith I.jsonEOF A.fromJSON s
66+
67+
decodeAttoS' :: BS.ByteString -> A.Value
68+
decodeAttoS' s = fromMaybe (error "fail to parse via Parser.decodeWith") $
69+
I.decodeStrictWith I.jsonEOF' A.fromJSON s
70+
5671
encodeJ :: J.JSValue -> BL.ByteString
5772
encodeJ = toLazyByteString . fromString . J.encode
5873

@@ -72,17 +87,43 @@ benchmark =
7287
env (readStr jpFile) $ \jpJ ->
7388
bgroup "compare-json" [
7489
bgroup "decode" [
75-
bgroup "en" [
76-
bench "aeson/lazy" $ nf decode enA
77-
, bench "aeson/strict" $ nf decode' enA
78-
, bench "aeson/stricter" $ nf decodeS enS
79-
, bench "aeson/attoparsec" $ nf decodeAtto enA
80-
, bench "json" $ nf decodeJ enJ
90+
bgroup "whnf" [
91+
-- Note: we use whnf to only force the outer constructor,
92+
-- which may force different amount of value substructure.
93+
bench "aeson/normal" $ whnf decode enA
94+
, bench "aeson/normal'" $ whnf decode' enA
95+
, bench "aeson/strict" $ whnf decodeS enS
96+
, bench "aeson/strict'" $ whnf decodeS' enS
97+
98+
-- attoparsec-aeson package
99+
, bench "aeson/atto" $ whnf decodeAtto enA
100+
, bench "aeson/atto'" $ whnf decodeAtto' enA
101+
, bench "aeson/attoS" $ whnf decodeAttoS enS
102+
, bench "aeson/attoS'" $ whnf decodeAttoS' enS
103+
104+
-- json package
105+
, bench "json" $ whnf decodeJ enJ
106+
]
107+
108+
, bgroup "nf" [
109+
bench "aeson/normal" $ nf decode enA
110+
, bench "aeson/normal" $ nf decode' enA
111+
, bench "aeson/strict" $ nf decodeS enS
112+
, bench "aeson/strict'" $ nf decodeS' enS
113+
114+
-- attoparsec-aeson package
115+
, bench "aeson/atto" $ nf decodeAtto enA
116+
, bench "aeson/atto'" $ nf decodeAtto' enA
117+
, bench "aeson/attoS" $ nf decodeAttoS enS
118+
, bench "aeson/attoS'" $ nf decodeAttoS' enS
119+
120+
-- json package
121+
, bench "json" $ nf decodeJ enJ
81122
]
82123
, bgroup "jp" [
83-
bench "aeson" $ nf decode jpA
84-
, bench "aeson/stricter" $ nf decodeS jpS
85-
, bench "json" $ nf decodeJ jpJ
124+
bench "aeson/normal" $ whnf decode jpA
125+
, bench "aeson/strict" $ whnf decodeS jpS
126+
, bench "json" $ whnf decodeJ jpJ
86127
]
87128
]
88129
, bgroup "encode" [

changelog.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ For the latest version of this document, please see [https://github.com/haskell/
2626
[#792](https://github.com/haskell/aeson/issues/792).
2727

2828
* Use `Data.Aeson.Decoding` parsing functions (introduced in version 2.1.2.0) as default in `Data.Aeson`.
29+
As one side-effect, `decode` and `decode'` etc pair functions are operationally the same.
30+
All variants use an intermediate `Value` in normal form.
31+
32+
The lazier variant could had `Value` thunks inside `Array` (i.e. `Vector`), but the record had been value strict since version `0.4.0.0` (before that the lazy `Data.Map` was used as `Object`).
33+
2934
* Move `Data.Aeson.Parser` module into separate [`attoparsec-aeson`](https://hackage.haskell.org/package/attoparsec-aeson) package, as these parsers are not used by `aeson` itself anymore.
3035
* Use [`text-iso8601`](https://hackage.haskell.org/package/text-iso8601) package for parsing `time` types. These are slightly faster than previously used (copy of) `attoparsec-iso8601`.
3136
* Remove `cffi` flag. Toggling the flag made `aeson` use a C implementation for string unescaping (used for `text <2` versions).

src/Data/Aeson.hs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,8 @@ decodeStrict' = decodeStrict
221221
-- If this fails due to incomplete or invalid input, 'Nothing' is
222222
-- returned.
223223
--
224+
-- Since @2.2.0.0@ an alias for 'decodeFileStrict'.
225+
--
224226
decodeFileStrict' :: (FromJSON a) => FilePath -> IO (Maybe a)
225227
decodeFileStrict' = decodeFileStrict
226228

@@ -231,22 +233,30 @@ eitherDecodeFileStrict =
231233
{-# INLINE eitherDecodeFileStrict #-}
232234

233235
-- | Like 'decode'' but returns an error message when decoding fails.
236+
--
237+
-- Since @2.2.0.0@ an alias for 'eitherDecode'.
234238
eitherDecode' :: (FromJSON a) => L.ByteString -> Either String a
235239
eitherDecode' = eitherDecode
236240
{-# INLINE eitherDecode' #-}
237241

238242
-- | Like 'decodeStrict'' but returns an error message when decoding fails.
243+
--
244+
-- Since @2.2.0.0@ an alias for 'eitherDecodeStrict'.
239245
eitherDecodeStrict' :: (FromJSON a) => B.ByteString -> Either String a
240246
eitherDecodeStrict' = eitherDecodeStrict
241247
{-# INLINE eitherDecodeStrict' #-}
242248

243249
-- | Like 'decodeFileStrict'' but returns an error message when decoding fails.
250+
--
251+
-- Since @2.2.0.0@ an alias for 'eitherDecodeFileStrict''.
244252
eitherDecodeFileStrict' :: (FromJSON a) => FilePath -> IO (Either String a)
245253
eitherDecodeFileStrict' = eitherDecodeFileStrict
246254
{-# INLINE eitherDecodeFileStrict' #-}
247255

248256
-- | Like 'decode'' but throws an 'AesonException' when decoding fails.
249257
--
258+
-- Since @2.2.0.0@ an alias for 'throwDecode'.
259+
--
250260
-- @since 2.1.2.0
251261
--
252262
throwDecode' :: forall a m. (FromJSON a, MonadThrow m) => L.ByteString -> m a
@@ -255,6 +265,8 @@ throwDecode' = throwDecode
255265

256266
-- | Like 'decodeStrict'' but throws an 'AesonException' when decoding fails.
257267
--
268+
-- Since @2.2.0.0@ an alias for 'throwDecodeStrict'.
269+
--
258270
-- @since 2.1.2.0
259271
--
260272
throwDecodeStrict' :: forall a m. (FromJSON a, MonadThrow m) => B.ByteString -> m a

src/Data/Aeson/Decoding.hs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{-# LANGUAGE RankNTypes #-}
22
{-# LANGUAGE ScopedTypeVariables #-}
33
-- | Convertion to and from @aeson@ 'A.Value'.
4+
--
45
module Data.Aeson.Decoding (
56
decode,
67
eitherDecode,

src/Data/Aeson/Decoding/Conversion.hs

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ lbsSpace :: L.ByteString -> Bool
2626
lbsSpace = L.all (\w -> w == 0x20 || w == 0x0a || w == 0x0d || w == 0x09)
2727

2828
-- | Convert 'Tokens' to 'A.Value'.
29+
--
30+
-- The resulting value will be in normal form if its forced.
31+
-- In other words, there shouldn't be thunks inside.
32+
--
2933
toEitherValue
3034
:: Tokens k e -- ^ tokens
3135
-> Either e (A.Value, k) -- ^ either token error or value and leftover.
@@ -34,43 +38,48 @@ toEitherValue t = unResult (toResultValue t) Left $ \v k -> Right (v, k)
3438
toResultValue
3539
:: Tokens k e -- ^ tokens
3640
-> Result e k A.Value -- ^ either token error or value and leftover.
37-
toResultValue t0 = Result (go t0) where
38-
go :: Tokens k e -> (e -> r) -> (A.Value -> k -> r) -> r
39-
go (TkLit l k) _ f = f (lit l) k
40-
go (TkText t k) _ f = f (A.String t) k
41-
go (TkNumber n k) _ f = f (A.Number (num n)) k
42-
go (TkArrayOpen arr) g f = goA 0 id arr g $ \n xs k -> f (A.Array (V.fromListN n xs)) k
43-
go (TkRecordOpen rec) g f = goR [] rec g $ \xs k -> f (A.Object (KM.fromList xs)) k
44-
go (TkErr e) g _ = g e
41+
toResultValue t0 = Result (convert t0)
4542

43+
convert :: Tokens k e -> (e -> r) -> (A.Value -> k -> r) -> r
44+
convert (TkLit l k) _ f = f (lit l) k where
4645
lit :: Lit -> A.Value
4746
lit LitNull = A.Null
4847
lit LitTrue = A.Bool True
4948
lit LitFalse = A.Bool False
50-
49+
convert (TkText t k) _ f = f (A.String t) k
50+
convert (TkNumber n k) _ f = f (A.Number (num n)) k where
5151
num :: Number -> Scientific
52-
num (NumInteger n) = fromInteger n
52+
num (NumInteger m) = fromInteger m
5353
num (NumDecimal s) = s
5454
num (NumScientific s) = s
55+
convert (TkArrayOpen arr) g f = convertA arr g $ \xs k -> f (A.Array xs) k
56+
convert (TkRecordOpen rec) g f = convertR rec g $ \xs k -> f (A.Object xs) k
57+
convert (TkErr e) g _ = g e
5558

59+
convertA :: TkArray k e -> (e -> r) -> (A.Array -> k -> r) -> r
60+
convertA tka err kont = goA 0 id tka err $ \n xs -> kont (V.fromListN n xs) where
5661
goA :: Int -- size accumulator
5762
-> ([A.Value] -> [A.Value]) -- dlist accumulator
5863
-> TkArray k e -- array tokens
5964
-> (e -> r) -- error continuation
6065
-> (Int -> [A.Value] -> k -> r) -- success continuation
6166
-> r
62-
goA !n !acc (TkItem toks) g f = go toks g $ \v k -> goA (succ n) (acc . (v :)) k g f
67+
goA !n !acc (TkItem toks) g f = convert toks g $ \ !v k -> goA (succ n) (acc . (v :)) k g f
6368
goA !n !acc (TkArrayEnd k) _ f = f n (acc []) k
6469
goA !_ !_ (TkArrayErr e) g _ = g e
6570

71+
convertR :: TkRecord k e -> (e -> r) -> (A.Object -> k -> r) -> r
72+
convertR tkr err kont = goR [] tkr err $ kont . KM.fromList where
6673
-- we accumulate keys in reverse order
6774
-- then the first duplicate key in objects wins (as KM.fromList picks last).
6875
goR :: [(Key, A.Value)]
6976
-> TkRecord k e
7077
-> (e -> r)
7178
-> ([(Key, A.Value)] -> k -> r)
7279
-> r
73-
goR !acc (TkPair t toks) g f = go toks g $ \v k -> goR ((t , v) : acc) k g f
80+
-- here we don't stricly need bang on !v as KM is a Strict (in values) map.
81+
-- but we force the value sooner.
82+
goR !acc (TkPair t toks) g f = convert toks g $ \ !v k -> goR ((t , v) : acc) k g f
7483
goR !acc (TkRecordEnd k) _ f = f acc k
7584
goR !_ (TkRecordErr e) g _ = g e
7685

tests/UnitTests.hs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ import UnitTests.FromJSONKey
6969
import UnitTests.Hashable
7070
import UnitTests.KeyMapInsertWith
7171
import UnitTests.MonadFix
72+
import UnitTests.NoThunks
7273
import UnitTests.NullaryConstructors (nullaryConstructors)
7374
import UnitTests.OptionalFields (optionalFields)
7475
import UnitTests.UTCTime
@@ -569,4 +570,5 @@ tests = testGroup "unit" [
569570
, issue967
570571
, keyMapInsertWithTests
571572
, omitNothingFieldsNoteTests
573+
, noThunksTests
572574
]

tests/UnitTests/NoThunks.hs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
{-# LANGUAGE BangPatterns #-}
2+
{-# LANGUAGE CPP #-}
3+
{-# LANGUAGE OverloadedStrings #-}
4+
{-# OPTIONS_GHC -Wno-orphans #-}
5+
module UnitTests.NoThunks where
6+
7+
import Test.Tasty (TestTree, testGroup)
8+
9+
#if __GLASGOW_HASKELL__ >=902 && __GLASGOW_HASKELL__ <907
10+
11+
import Data.Maybe (isNothing)
12+
import NoThunks.Class (NoThunks (..), allNoThunks, noThunksInKeysAndValues)
13+
import Test.QuickCheck (ioProperty)
14+
import Test.Tasty.HUnit (assertFailure, testCase)
15+
import Test.Tasty.QuickCheck (testProperty)
16+
17+
import qualified Data.Aeson.Key as K
18+
import qualified Data.Aeson.KeyMap as KM
19+
import qualified Data.Scientific as Sci
20+
21+
import Data.Aeson
22+
23+
noThunksTests :: TestTree
24+
noThunksTests = testGroup "nothunks"
25+
[ testNoThunks "example1" "null"
26+
, testNoThunks "example2" "[ 1, 2, 3, true ]"
27+
, testNoThunks "example3" "{ \"1\": 1, \"2\": 2 }"
28+
, testProperty "property" $ \input -> ioProperty $ do
29+
let lbs = encode (input :: Value)
30+
!value <- either fail return $ eitherDecode lbs
31+
isNothing <$> noThunks [] (value :: Value)
32+
]
33+
where
34+
testNoThunks name bs = testCase name $ do
35+
!value <- either fail return $ eitherDecode bs
36+
x <- noThunks [] (value :: Value)
37+
case x of
38+
Nothing -> return ()
39+
Just ti -> assertFailure $ show ti
40+
41+
instance NoThunks Value
42+
43+
instance NoThunks v => NoThunks (KM.KeyMap v) where
44+
wNoThunks ctx m = noThunksInKeysAndValues ctx (KM.toList m)
45+
showTypeOf _ = "KeyMap"
46+
47+
instance NoThunks K.Key where
48+
wNoThunks _ _ = return Nothing
49+
showTypeOf _ = "Key"
50+
51+
instance NoThunks Sci.Scientific where
52+
wNoThunks ctx s = do
53+
let !c = Sci.coefficient s
54+
let !e = Sci.base10Exponent s
55+
allNoThunks [ wNoThunks ctx c, wNoThunks ctx e ]
56+
showTypeOf _ = "Scientific"
57+
58+
#else
59+
60+
-- for other GHCs the test group is empty
61+
noThunksTests :: TestTree
62+
noThunksTests = testGroup "nothunks" []
63+
64+
#endif

0 commit comments

Comments
 (0)