JSaddle - ArrayBuffers and NotificationsPosted on December 14, 2017
During some experiments with reflex-dom-canvas, a library for interacting with the Canvas API, I started having runtime exceptions regarding a vertex index being out of bounds. Initially I thought this was due to an incorrect memory plan:
let size = 2 -- 2 components per iteration dataType = Gl.FLOAT -- the data is 32bit floats normalise = False -- don't normalize the data stride = 0 -- 0 for tightly packed array, or (size * sizeof(type)) to get the next position/per iteration offset = 0 -- start at the beginning of the buffer -- Tell the attribute how to get data out of positionBuffer (ARRAY_BUFFER) Gl.vertexAttribPointerF (fromIntegral _rPosAttrLoc) size dataType normalise stride offset
After trying a few iterations of different
positions :: [Double] positions = [ 0.0, 0.0 , 0.0, 0.5 , 0.7, 0.0 ]
toJSVal function from
jsaddle to take our
[Double] and turn it into a
JSVal for the API to consume:
The problem was that the list of vertices were being inlined as separate arguments to the
bufferData function, instead of the list itself being treated as a single argument.
What I expected:
bufferData(ARRAY_BUFFER, vertexList, STATIC_DRAW);
bufferData(ARRAY_BUFFER, 0.0, 0.0, 0.0, 0.5, 0.7, 0.0, STATIC_DRAW);
This is because
jsaddle provides the
MakeArgs typeclass for turning a list into the arguments of a function. So my list of vertices was being inlined as individual function inputs, and I didn’t understand it well enough to be able to solve my problem - yet.
From [Double] to ArrayBuffer
In the first attempt, the
ArrayBuffer was handled directly. Creating a
Float32Array that was backed by that buffer, populating it with data, before accessing the underlying buffer again to use in the WebGL code. Fuelled by a need to make something appear on the screen, and my general ignorance of the workings of the
jsaddle library, this attempt ended up being very low level and manual.
Step 1) Instantiate a fixed size
ArrayBuffer object and create a
Float32Array using that buffer.
-- Haskell -- The 'length' function returns an 'Int', but we need to give jsaddle a 'Double', so we use -- 'fromIntegral' to handle that transformation for us. This needed a type annotation to -- help Haskell along as these types end up very general. You can't pick a typeclass when -- all you know about a type is that it is an instance of 'Num'. let buffSize :: Double buffSize = fromIntegral (length positions * 4) buff <- new (jsg "ArrayBuffer") buffSize f32Arr <- new (jsg "Float32Array") buff
new function from
window object may be accessed using
jsg. You can read more about these functions here (
new), and here (
Step 2) Populate the
Float32Array with the position information.
-- Haskell itraverse_ (\ix pos -> (f32Arr <## ix) pos ) positions
itraverse_ function is an indexed traversal, providing the index of the current element as input to the traversal function:
itraverse_ :: (Applicative f, FoldableWithIndex i t) => (i -> a -> f b) -> t a -> f ()
We can use that with a
(<##) ,documented here. This function sets a property on an object at the given index. For a bit more flavour, we’re able to take advantage of the fact that Haskell functions only take one argument , to simplify our traversal function:
-- Starting here with our original function, let's call it 'f' for now... f :: Int -> Double -> JSM () f ix pos = (f32Arr <## ix) pos -- We don't need to explicitly include the 'pos' argument f :: Int -> Double -> JSM () f ix pos = (f32Arr <## ix) pos -- is the same as f :: Int -> Double -> JSM () f ix = f32Arr <## ix -- because... We can also drop the 'ix' argument, because this function: (<##) :: (MakeObject this, ToJSVal val) => this -> Int -> val -> JSM () -- Specialised to our Float32Array (<##) :: Float32Array -> Int -> Double -> JSM () -- Then partially applied to our Float32Array: f :: Int -> Double -> JSM () f = (f32Arr <##) -- Consequently our thoroughly "code golf'd" 'itraverse_' becomes itraverse_ (f32Arr <##) positions
Step 3) Retrieve the underlying buffer to be used in the rendering process.
The underlying buffer of a
Float32Array is accessed as a property of the array object itself.
Haskell needs a bit more information as the property access functions return a
JSVal. Conversions are up to you, because this lets you choose where you want to be on the runtime safety scale:
-- Haskell -- Access the "buffer" property on our f32Arr object f32Buff <- f32Arr ! "buffer" -- Convert our JSVal to the ArrayBuffer type we require by giving -- the constructor to the ``castTo`` function from 'jsaddle' castTo ArrayBuffer f32Buff
castTo function from
jsaddle will return a
Maybe a of your desired cast, to avoid runtime exceptions wherever possible. There is another version
unsafeCastTo that will simply crash if the cast was not possible.
First attempt results:
All in all, quite a bit of heavy lifting going on…
-- Haskell buildBuffer :: [Double] -> JSM (Maybe ArrayBuffer) buildBuffer positions = do let buffSize :: Double buffSize = fromIntegral (length positions * 4) buff <- new (jsg "ArrayBuffer") buffSize f32Arr <- new (jsg "Float32Array") buff itraverse_ (f32Arr <##) positions buffVal <- f32Arr ! "buffer" castTo ArrayBuffer buffVal
The different steps translate quite easily to Haskell, and we have the added benefit of the types and all the various plumbing functions that Haskell provides.
Many WebGL tutorials demonstrate the following technique for creating the
Float32Array. Also, given that the array is backed by an
ArrayBuffer by design, we can simply pull that off the newly minted array:
Where I was coming unstuck with my usage of the
jsaddle functions was my understanding of the
MakeArgs TypeClass. Its purpose is to allow you to construct the list of arguments for a function. I needed to pass a list of inputs as a single argument to the function. Perhaps obviously, the solution was to simply place my list in a list. Thus…
buildBuffer :: [Double] -> JSM (Maybe ArrayBuffer) buildBuffer positions = do f32Arr <- new (jsg "Float32Array") [positions] buffVal <- f32Arr ! "buffer" castTo ArrayBuffer buffVal
Or if you don’t like intermediate variables:
buildBuffer :: [Double] -> JSM (Maybe ArrayBuffer) buildBuffer positions = new (jsg "Float32Array") [positions] >>= ( ! "buffer" ) >>= castTo ArrayBuffer
This is a much more concise way of building an
ArrayBuffer, and knowing more about the
GHCJS code, pass arguments to the constructors, and access properties on the objects themselves. We can do all these things, we will try something a bit more adventurous…
Now, let’s annoy, I mean communicate with, some users using desktop notifications! We’ll use some of the functions we introduced in the first section:
jsg. To access the
Notification object on the window, check our permissions, and try to send a simple notification to the user. We’ll work through the example from the MDN API documentation page.
Accessing the API Object
Earlier we used the
Float32Array. We’ll use that function again, but this time we need access to the object because we need to run some of its functions. You can think of the
jsg function as a roughly equivalent to a property accessor for the
notify <- jsg "Notification"
Now we can check what our permissions are with respect to the Notifications API and decide what to do. To do that we need to access a property on the
permStr <- notify ^. js "permission"
This will give us a
JSVal that is the current permission setting for the
Notification API. But we have a
JSVal and it could be anything! According to the documentation though, this property should be a stringly value from a list of three options:
We could use the
valToText function from
jsaddle to try to change this to a
JSString, change that to a
Text value with
strToText, and finally
unpack this to a
String value and decide what to do:
permStr <- valToText =<< notify ^. js "permission" case fromJSString permStr where "denied" -> ... "granted" -> ... "default" -> ... _ -> ...
But we would need to do that every time we checked the permissions, that’s not very nice! We’re using Haskell after all, so we will build a data type to represent our permission levels, then tell
jsaddle how to translate from a
JSVal. First, the new type:
data NotifyPerm = Default | Denied | Granted deriving (Show, Eq)
Then we need to tell
jsaddle how to translate a
JSVal into our type. We do this by creating an instance of the
instance FromJSVal NotifyPerm where fromJSVal :: JSVal -> JSM (Maybe NotifyPerm) fromJSVal v = do -- Will give us the JSString value of whatever this 'JSVal' is. permStr <- valToStr v -- Unpack the 'JSString' to a boring Haskell 'String' so we can use a 'case': pure $ case unpack $ strToText permStr of -- Pattern match on the string values "default" -> Just Default "denied" -> Just Denied "granted" -> Just Granted -- Ignore everything that doesn't meet our requirements. _ -> Nothing
We are able to use
Generic to derive a
FromJSVal instance automatically, using the
DeriveAnyClass extensions. But this technique won’t work the way you want. By just looking at the
NotifyPerm type, can you see why?
Now that we’re able to use a more robust type, we can decide what to do when we know what permissions we have for creating notifications:
permVal <- notify ^. js "permission" notifyPerm <- fromJSVal permVal -- Using 'traverse' here lets us write the 'handleNotify' function without -- worrying about the 'Nothing' case, which makes our life easier. traverse (handleNotify notify message) notifyPerm
Now we can write our
handleNotify function in a
where binding to keep things neat and tidy. Or not, referential transparency is lovely like that. We’ll need the reference to the
Notification object, and the message, assumed to be a
Text value. Because we don’t have to worry about the
Maybe, we can pattern match on our permission, making everything even easier, again.
where -- No permission, just return '()' and do nothing. handleNotify _ _ Denied = pure () -- Permission already granted, create our Notification handleNotify nObj msg Granted = -- We use 'void' here because we don't need the result of this call to 'new' void $ new nObj (ValString msg) -- The gnarly case, the API will ask the user for permission to show notifications -- We must provide a callback function to act on the answer handleNotify nObj msg Default = -- Call the 'requestPermission' function on the Notification object void $ nObj ^. jsf "requestPermission" -- This is our callback function, we will go over this in more detail next [ fun $ \_ _ [newPerm] -> do pV <- fromJSVal newPerm traverse_ (\p -> when ( p == Granted ) $ newNotify nObj ) pV ]
type JSCallAsFunction = JSVal -- The function object -> JSVal -- "this" object -> [JSVal] -- The function arguments -> JSM () -- Must return unit as the function may run on a different thread -- We can discard the first two arguments, using '_', we don't use them. Then we pattern match on the list of arguments [ fun $ \_ _ [newPerm] -> do -- Take the 'JSVal' and convert it to a 'NotifyPerm' pV <- fromJSVal newPerm -- Use a traverse to run our function on the 'NotifyPerm' value, if we have one. traverse_ (\p -> when ( p == Granted ) $ newNotify nObj ) pV ]
The purpose of placing the entire call to
fun inside a list is that we want the
MakeArgs typeclass to pass in this function as a single input to the
requestPermission function. Now we have all the moving parts in place to start using the Notification API in the Browser via GHCJS. Yay! A version of this integration is being used here. It won’t work on Android at the moment. Chrome version 62 or higher will require a
https website before it will enable notifications, but Firefox should work.
Phew, that was a lot to get through. But by now you should know how to:
- Access properties on objects and top level references.
All from the nice type safe world of Haskell & GHCJS.
jsaddle-dom packages are invaluable when working with GHCJS. However they cover an enormous surface area simply because of the size of the APIs they represent, which may be a bit daunting. Hopefully I’ve provided enough information that you’ve more confidence to delve into creating your own GHCJS/Reflex applications, and leverage existing APIs without feeling like you need to reinvent the wheel. Integrate the parts you need, or make some APIs more functional and easier for the next person to use.