git.iko.soy/rowtype-yoga/purescript-arraybuffer-builder
on master help sign in
Bytes type for appending DataView
commit19d52fb7c031e6c7ffe61b82eb4e3b87d7408a2a authorJames Brock <jamesbrock@gmail.com> dateWed 28 Apr 2021, 23:36 (5 years ago) parente2ff47a
4 files changed+119−24
CHANGELOG.md+4−2 README.md+39−8 src/Data/ArrayBuffer/Builder.purs+63−14 test/Main.purs+13
CHANGELOG.md+4−2
@@ -1,6 +1,8 @@
1−## 2021-04-28
1+## v2.0.0 2021-04-29
22
3−Build for PureScript v0.14.1
3+Added the `Bytes` type.
4+
5+Spago build for PureScript v0.14.1.
46
57## v1.1.0 2020-09-10
68
README.md+39−8
@@ -30,8 +30,10 @@ All `ArrayBuffer` building must occur in `Effect`.
3030
3131Create a two-byte `arraybuffer :: ArrayBuffer` which contains the number *-10* encoded as big-endian 16-bit two’s-complement.
3232```purescript
33+import Data.ArrayBuffer.Builder (execPut, putInt16be)
34+
3335do
34− arraybuffer <- execPut $ putInt16be (-10)
36+ arraybuffer :: ArrayBuffer <- execPut $ putInt16be (-10)
3537```
3638
3739### Serialize three floats
@@ -40,8 +42,10 @@ Create a 24-byte `arraybuffer :: ArrayBuffer` which contains three big-endian
4042IEEE-754 double-precision floats.
4143
4244```purescript
45+import Data.ArrayBuffer.Builder (execPut, putFloat64be)
46+
4347do
44− arraybuffer <- execPut $ do
48+ arraybuffer :: ArrayBuffer <- execPut $ do
4549 putFloat64be 1.0
4650 putFloat64be 2.0
4751 putFloat64be 3.0
@@ -49,22 +53,27 @@ do
4953
5054### Serialize a `String` as UTF8
5155
52−Encode a `String` as UTF8 with a length prefix.
56+Encode a `String` as UTF8 with a length prefix into our `Builder`.
5357
5458We give this as an example, rather than supporting it in the library, because
5559it depends on
5660[`Data.TextEncoding.encodeUtf8`](https://pursuit.purescript.org/packages/purescript-text-encoding/docs/Data.TextEncoding#v:encodeUtf8).
5761
5862```purescript
63+import Data.ArrayBuffer.Builder (PutM, putArrayBuffer, execPut)
64+import Data.ArrayBuffer.Typed (buffer)
65+import Data.TextEncoding (encodeUtf8)
66+import Data.ArrayBuffer.ArrayBuffer (byteLength)
67+
5968putStringUtf8 :: forall m. (MonadEffect m) => String -> PutM m Unit
6069putStringUtf8 s = do
61− let stringbuf = Data.ArrayBuffer.Typed.buffer $ Data.TextEncoding.encodeUtf8 s
70+ let stringbuf = buffer $ encodeUtf8 s
6271 -- Put a 32-bit big-endian length prefix for the length of the utf8 string, in bytes.
63− putUint32be $ Data.Uint.fromInt $ Data.ArrayBuffer.byteLength stringbuf
72+ putInt32be $ byteLength stringbuf
6473 putArrayBuffer stringbuf
6574
6675do
67− arraybuffer <- execPut $ putStringUtf8 "BLM"
76+ arraybuffer :: ArrayBuffer <- execPut $ putStringUtf8 "BLM"
6877```
6978
7079### Serialize an `Array Int`
@@ -77,17 +86,38 @@ from the Haskell
7786library.
7887
7988```purescript
89+import Data.ArrayBuffer.Builder (execPut, putInt32be)
90+import Data.Foldable (traverse_)
91+import Data.Array (length)
92+
8093putArrayInt32 :: forall m. (MonadEffect m) => Array Int -> PutM m Unit
8194putArrayInt32 xs = do
8295 -- Put a 64-bit big-endian length prefix for the length of the array.
83− putUint32be 0
84− putUint32be $ Data.Uint.fromInt $ Data.Array.length xs
96+ putInt32be 0
97+ putInt32be $ length xs
8598 traverse_ putInt32be xs
8699
87100do
88101 arraybuffer <- execPut $ putArrayInt32 [1,2,3]
89102```
90103
104+
105+## `unsafePerformEffect execPut`
106+
107+Dear computer programmer, you can call `execPut` inside `unsafePerformEffect`.
108+
109+The reason why this whole library is `Effect`ful is that `ArrayBuffer`s are mutable, so the `ArrayBuffer` library functions do everything in `Effect` to to confirm that any mutations are properly sequenced.
110+
111+However, all of the `put` functions in this library are actually side-effect free, so we can use `unsafePerformEffect` to tell the compiler to trust us that we don't perform any side-effects while we're building an `ArrayBuffer`.
112+
113+```purescript
114+let serialized :: ArrayBuffer
115+ serialized = unsafePerformEffect $ execPut do
116+ putInt32be 3 -- ✅ Good, this has no side-effects
117+ putInt32be =<< getCurrentPosixTime -- ❌ No don't do this
118+ putArrayBuffer =<< readFileFromMyFilesystem -- ❌ No don’t do this either
119+```
120+
91121## Deserialization
92122
93123This package is only for writing `ArrayBuffer`s, not reading them.
@@ -104,3 +134,4 @@ for a way to deserialize from `ArrayBuffer` back to Purescript data.
104134* [__dynamic-buffers__](https://pursuit.purescript.org/packages/purescript-dynamic-buffers)
105135* [__node-buffer__](https://pursuit.purescript.org/packages/purescript-node-buffer)
106136* [__arraybuffer-class__](https://pursuit.purescript.org/packages/purescript-arraybuffer-class)
137+* [__bytestrings__](https://pursuit.purescript.org/packages/purescript-bytestrings/)
src/Data/ArrayBuffer/Builder.purs+63−14
@@ -16,6 +16,7 @@ module Data.ArrayBuffer.Builder
1616, execPut
1717, subBuilder
1818, putArrayBuffer
19+, putDataView
1920, putUint8
2021, putInt8
2122, putUint16be
@@ -32,11 +33,16 @@ module Data.ArrayBuffer.Builder
3233, putFloat64le
3334, Builder
3435, (<>>)
36+, Bytes
37+, toView
3538, execBuilder
3639, length
3740, singleton
41+, singleton_
3842, cons
43+, cons_
3944, snoc
45+, snoc_
4046, encodeUint8
4147, encodeInt8
4248, encodeUint16be
@@ -60,7 +66,7 @@ import Control.Monad.Writer.Trans (WriterT, execWriterT, tell, lift)
6066import Data.ArrayBuffer.ArrayBuffer as AB
6167import Data.ArrayBuffer.DataView as DV
6268import Data.ArrayBuffer.Typed as AT
63−import Data.ArrayBuffer.Types (ArrayBuffer, Uint8Array)
69+import Data.ArrayBuffer.Types (ArrayBuffer, Uint8Array, DataView, ByteLength)
6470import Data.Float32 (Float32)
6571import Data.Maybe (Maybe(..))
6672import Data.UInt (UInt)
@@ -80,6 +86,15 @@ type Put = PutM Effect
8086
8187-- | Monoidal builder for `ArrayBuffer`s.
8288-- |
89+-- | We can add two types of things to the `Builder`: `ArrayBuffer`s and
90+-- | `DataView`s.
91+-- | We might prefer
92+-- | to add a `DataView` to a `Builder` when we’re adding a large slice of data
93+-- | from some other `ArrayBuffer`, so that we don’t
94+-- | need an extra intermediate copy of the slice.
95+-- | Our convention is that functions which add `DataView`s to
96+-- | the `Builder` are suffixed with underscore (`_`).
97+-- |
8398-- | ### Left-associative `<>>` append operator
8499-- |
85100-- | __TL;DR__ You probably don't want to use the `Builder` monoid directly
@@ -114,7 +129,7 @@ type Put = PutM Effect
114129-- | `snoc` case.
115130-- |
116131data Builder
117− = Node Builder ArrayBuffer Builder
132+ = Node Builder Bytes Builder
118133 | Null
119134
120135instance semigroupBuilder :: Semigroup Builder where
@@ -134,36 +149,60 @@ infixl 5 append as <>>
134149instance monoidBuilder :: Monoid Builder where
135150 mempty = Null
136151
152+-- | For distinguishing between `ArrayBuffer` and `DataView`.
153+data Bytes
154+ = ByteBuff ArrayBuffer
155+ | ByteView DataView
156+
157+toView :: Bytes -> DataView
158+toView (ByteBuff ab) = DV.whole ab
159+toView (ByteView dv) = dv
160+
137161-- | `Builder` is not an instance of `Foldable` because `Builder` is monomorphic.
138−foldl :: forall b. (b -> ArrayBuffer -> b) -> b -> Builder -> b
139−foldl f a Null = a
162+foldl :: forall b. (b -> Bytes -> b) -> b -> Builder -> b
163+foldl _ a Null = a
140164foldl f a (Node l x r) = foldl f (f (foldl f a l) x) r
141165
142166-- | Monomorphic foldM copied from
143167-- | [`Data.Foldable.foldM`](https://pursuit.purescript.org/packages/purescript-foldable-traversable/4.1.1/docs/Data.Foldable#v:foldM)
144−foldM :: forall b m. (Monad m) => (b -> ArrayBuffer -> m b) -> b -> Builder -> m b
168+foldM :: forall b m. (Monad m) => (b -> Bytes -> m b) -> b -> Builder -> m b
145169foldM f a0 = foldl (\ma b -> ma >>= flip f b) (pure a0)
146170
147171-- | Construct a `Builder` with a single `ArrayBuffer`. *O(1)*
148172singleton :: ArrayBuffer -> Builder
149−singleton buf = Node Null buf Null
173+singleton buf = Node Null (ByteBuff buf) Null
174+
175+-- | Construct a `Builder` with a single `DataView`. *O(1)*
176+singleton_ :: DataView -> Builder
177+singleton_ buf = Node Null (ByteView buf) Null
150178
151179-- | Prepend an `ArrayBuffer` to the beginning of the `Builder`. *O(1)*
152180cons :: ArrayBuffer -> Builder -> Builder
153−cons x bs = Node Null x bs
181+cons x bs = Node Null (ByteBuff x) bs
182+
183+-- | Prepend a `DataView` to the beginning of the `Builder`. *O(1)*
184+cons_ :: DataView -> Builder -> Builder
185+cons_ x bs = Node Null (ByteView x) bs
154186
155187-- | Append an `ArrayBuffer` to the end of the `Builder`. *O(1)*
156188snoc :: Builder -> ArrayBuffer -> Builder
157−snoc bs x = Node bs x Null
189+snoc bs x = Node bs (ByteBuff x) Null
190+
191+-- | Append a `DataView` to the end of the `Builder`. *O(1)*
192+snoc_ :: Builder -> DataView -> Builder
193+snoc_ bs x = Node bs (ByteView x) Null
158194
159195-- | Calculate the total byte length of the `Builder`. *O(n)*
160−length :: Builder -> Int
161−length bldr = foldl (\b a -> b + AB.byteLength a) 0 bldr
196+length :: Builder -> ByteLength
197+length bldr = foldl (\b a -> b + len a) 0 bldr
198+ where
199+ len (ByteBuff ab) = AB.byteLength ab
200+ len (ByteView dv) = DV.byteLength dv
162201
163202-- | Build a single `ArrayBuffer` from a `Builder`. *O(n)*
164203execBuilder :: forall m. (MonadEffect m) => Builder -> m ArrayBuffer
165204execBuilder bldr = do
166− let buflen = foldl (\b a -> b + AB.byteLength a) 0 bldr
205+ let buflen = length bldr
167206 -- Allocate the final ArrayBuffer
168207 buf <- liftEffect $ AB.empty buflen
169208 -- Then copy each ArrayBuffer into the final ArrayBuffer.
@@ -172,11 +211,16 @@ execBuilder bldr = do
172211 newview <- liftEffect (AT.whole buf :: Effect Uint8Array)
173212 _ <- foldM
174213 ( \offset a -> do
175− aview <- liftEffect (AT.whole a :: Effect Uint8Array)
214+ aview <- liftEffect $ toUint8Array a
176215 _ <- liftEffect $ AT.setTyped newview (Just offset) aview
177− pure $ offset + AB.byteLength a
216+ pure $ offset + AT.byteLength aview
178217 ) 0 bldr
179218 pure buf
219+ where
220+ toUint8Array :: Bytes -> Effect Uint8Array
221+ toUint8Array (ByteBuff ab) = AT.whole ab
222+ toUint8Array (ByteView dv) =
223+ AT.part (DV.buffer dv) (DV.byteOffset dv) (DV.byteLength dv)
180224
181225-- | Build an `ArrayBuffer` with do-notation in any `MonadEffect`. *O(n)*
182226execPutM :: forall m. (MonadEffect m) => PutM m Unit -> m ArrayBuffer
@@ -212,6 +256,10 @@ subBuilder = lift <<< execWriterT
212256putArrayBuffer :: forall m. (MonadEffect m) => ArrayBuffer -> PutM m Unit
213257putArrayBuffer = tell <<< singleton
214258
259+-- | Append a `DataView` to the builder.
260+putDataView :: forall m. (MonadEffect m) => DataView -> PutM m Unit
261+putDataView = tell <<< singleton_
262+
215263-- | Serialize an 8-bit unsigned integer (byte) into a new `ArrayBuffer`.
216264encodeUint8 :: forall m. (MonadEffect m) => UInt -> m ArrayBuffer
217265encodeUint8 x = do
@@ -379,7 +427,7 @@ putFloat64le = putArrayBuffer <=< encodeFloat64le
379427-- left or right tree is a singleton. If that's not true, then in the
380428-- unlikely worst case `append` might be *O(n)*.
381429--
382−-- `Builder` is optimized what we consider to be normal usage, that is,
430+-- `Builder` is optimized for what we consider to be normal usage, that is,
383431-- `snoc`ing singleton elements to the end of the `Builder`.
384432--
385433-- If a Builder is built entirely by `snoc`ing, it will look like a
@@ -427,6 +475,7 @@ putFloat64le = putArrayBuffer <=< encodeFloat64le
427475--
428476-- One relatively cheap and simple performance improvement for this library would be to
429477-- remove the Null constructor of `Builder` and instead use Javascript nulls.
478+-- UPDATE Actually no, we need the Null constructor for the Monoid instance.
430479--
431480-- In the longer term, it might make sense to try to change the Builder so
432481-- that it works like the
test/Main.purs+13
@@ -3,6 +3,7 @@ module Test.Main where
33import Prelude (Unit, bind, pure, discard, map, negate, ($), (<$>), (=<<), (<>))
44import Data.ArrayBuffer.Types (ArrayBuffer,Uint8Array)
55import Data.ArrayBuffer.Typed as AT
6+import Data.ArrayBuffer.DataView as DV
67import Data.UInt as UInt
78import Effect (Effect)
89import Test.Assert (assertEqual')
@@ -98,3 +99,15 @@ main = do
9899 putInt8 $ length x
99100 tell x
100101
102+ buildTest "Test 10" [1,2,3,4,5,6,7,8] $ do
103+ b1 <- DV.whole <$> encodeInt8 1
104+ b2 <- DV.whole <$> encodeInt8 2
105+ b3 <- DV.whole <$> encodeInt8 3
106+ b4 <- DV.whole <$> encodeInt8 4
107+ b5 <- DV.whole <$> encodeInt8 5
108+ b6 <- DV.whole <$> encodeInt8 6
109+ b7 <- DV.whole <$> encodeInt8 7
110+ b8 <- DV.whole <$> encodeInt8 8
111+ pure $ ((singleton_ b1 <> singleton_ b2) <> (singleton_ b3 <> singleton_ b4))
112+ <> ((singleton_ b5 <> singleton_ b6) <> (singleton_ b7 <> singleton_ b8))
113+