Type errors trying to draw on a canvas with Halogen Hooks

I’m trying to see if I can use Halogen Hooks to draw on a canvas but I’m having trouble getting the types to line up in the useTickEffect block.

I’ve primarily been looking at Example.Hooks.UsePreviousValue (v0.4.2) to write the hook itself and the myComponent code block in Introducing Halogen Hooks to call the hook.

Here’s my code so far:

module Main where

import Prelude

import Data.Newtype (class Newtype)
import Data.Traversable (sequence)
import Effect (Effect)
import Effect.Aff.Class (class MonadAff)
import Effect.Class (liftEffect)
import Graphics.Canvas (Context2D, fillRect, getCanvasElementById, getContext2D, setFillStyle)
import Halogen as H
import Halogen.Aff as HA
import Halogen.HTML as HH
import Halogen.HTML.Properties as HP
import Halogen.Hooks (Hook, UseEffect)
import Halogen.Hooks as Hooks
import Halogen.VDom.Driver (runUI)

main :: Effect Unit
main =
  HA.runHalogenAff do
    body <- HA.awaitBody
    runUI canvasComponent unit body

canvasComponent ::
  forall query input output m.
  MonadAff m =>
  H.Component HH.HTML query input output m
canvasComponent =
  Hooks.component \_ _ -> Hooks.do
    drawOnCanvas
    Hooks.pure $ HH.canvas [ HP.id_ "canvas", HP.width 300, HP.height 300 ]

newtype DrawOnCanvas hooks
  = DrawOnCanvas (UseEffect hooks)

derive instance newtypeDrawOnCanvas :: Newtype (DrawOnCanvas hooks) _

drawOnCanvas :: forall m. MonadAff m => Hook m DrawOnCanvas Unit
drawOnCanvas =
  Hooks.wrap Hooks.do
    Hooks.captures {} Hooks.useTickEffect do
      -- Error on next line: Could not match type Maybe with type HookM t0
      mcanvas <- liftEffect $ getCanvasElementById "canvas"
      mcontext <- liftEffect $ sequence $ getContext2D <$> mcanvas
      drawRect <$> mcontext
      pure unit
    Hooks.pure unit
  where
  drawRect context = do
    liftEffect $ setFillStyle context "red"
    liftEffect
      $ fillRect context { x: 100.0, y: 50.0, width: 200.0, height: 25.0 }

The full error message is:

[1/1 TypesDoNotUnify] src/Main.purs:42:7

42        mcanvas <- liftEffect $ getCanvasElementById "canvas"
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Could not match type

    Maybe

with type

    HookM t0

while trying to match type t1 t2
    with type HookM t0 (Maybe (HookM t0 Unit))
while checking that expression
    (bind ((apply liftEffect) (getCanvasElementById "canvas"))) (\mcanvas ->
        (bind (...)) (\mcontext ->
            ...
        )
    )
    has type HookM t0 (Maybe (HookM t0 Unit))
in value declaration drawOnCanvas

where t0 is an unknown type
        t1 is an unknown type
        t2 is an unknown type

I have a lot of Elm and a little Haskell experience but PureScript and Halogen are brand new to me. I also have a jumble of questions:

  1. Am I on the right track? Am I going about this the right way or am I completely misunderstanding how Halogen & Hooks work?

  2. Which part of the expression is not unifying correctly?

  3. The types I’m expecting everything to have are below. Are these correct?

-- mcanvas <- liftEffect $ getCanvasElementById "canvas"

getCanvasElementById "canvas" :: Effect (Maybe CanvasElement)

liftEffect $ getCanvasElementById "canvas"
  :: forall m. MonadAff m => m (Maybe CanvasElement)

mcanvas :: Maybe CanvasElement

-- mcontext <- liftEffect $ sequence $ getContext2D <$> mcanvas

getContext2D <$> mcanvas :: Maybe (Effect Context2D)

sequence $ getContext2D <$> mcanvas :: Effect (Maybe Context2D)

liftEffect $ sequence $ getContext2D <$> mcanvas
  :: forall m. MonadAff m => m (Maybe Context2D)

mcontext :: Maybe Context2D

-- drawRect <$> mcontext

drawRect :: forall m. MonadAff m => Context2D -> m Unit

drawRect <$> mcontext :: forall m. MonadAff m => Maybe (m Unit)

pure unit :: forall m. MonadAff m => m Unit
  1. It seems like the UseEffect type is only for useTickEffect and useLifecycleEffect - is that correct, or is it for any kind of effect? Example.Hooks.UseWindowWidth doesn’t seem to do anything special (as far as types) to call Window.innerWidth, which returns an Effect Int.

  2. Even if this worked, will it never show anything because the call to drawOnCanvas comes before the canvas element? Or because it’s a tick effect, is the order irrelevant?

I think my next step will be to unsugar the whole Hooks.do block and see if I see anything weird there.

Thanks for reading this far! Any help at all or any answers - full or partial - would be greatly appreciated!

Hey @smilack!

The use*Effect functions take an argument of type HookM m (Maybe (HookM m Unit)). In other words, they use an effect written in HookM, which returns an optional finalizer to run to clean up any resources used in your effect when the component unmounts. What immediately stands out to me in your example is that you’re returning pure unit, which would have the type HookM m Unit.

What happens if you instead return pure Nothing?

   Hooks.captures {} Hooks.useTickEffect do
     mcanvas <- liftEffect $ getCanvasElementById "canvas"
     mcontext <- liftEffect $ sequence $ getContext2D <$> mcanvas
     drawRect <$> mcontext
-    pure unit
+    pure Nothing
   Hooks.pure unit

I’d love to give a more thorough answer later, but I’m a bit pressed for time at the moment :slight_smile:

2 Likes

I got it working!

The change to pure Nothing didn’t change the error, but it was still necessary in the end.

I was commenting out lines to see if I could get different errors, and I found that without the call to drawRect, it type checked. I inlined the drawing logic with a case statement and it worked! (Both compiling successfully and drawing the red rectangle):

    Hooks.captures {} Hooks.useTickEffect do
      mcanvas <- liftEffect $ getCanvasElementById "canvas"
      mcontext <- liftEffect $ sequence $ getContext2D <$> mcanvas
      --drawRect <$> mcontext

      case mcontext of
        Just context -> do
          liftEffect $ setFillStyle context "red"
          liftEffect
            $ fillRect context { x: 100.0, y: 50.0, width: 200.0, height: 25.0 }
        Nothing -> do
          pure unit

      pure Nothing

I’m still curious why the function call didn’t work, though.

The drawRect <$> mcontext should have clued me that something was odd there, too. What’s happening is that you’ve got:

mcontext :: Maybe Context2D

drawRect :: forall m. MonadAff m => Context2D -> m Unit

and then you’re using map:

map :: forall a b f. Functor f => (a -> b) -> f a -> f b

to apply drawRect to the Context2D from within mcontext:

result :: forall m. MonadAff m => Maybe (m Unit)
result = drawRect <$> mcontext

This represents a possible effect (m Unit) that can be run, but it doesn’t actually run the effect. In your case you actually do want to draw the rectangle right now, so you are looking for something which executes effects – which Functor won’t give you.

But that’s what Applicative (and Monad, by extension) is for! Judging from your case statement, you really didn’t want map, you wanted traverse_.

traverse_ :: forall a b f m. Applicative m => Foldable f => (a -> m b) -> f a -> m Unit

-- compared to `map`
map :: forall a b f. Functor f => (a -> b) -> f a -> f b

-- specialized to your types:
traverse_ :: forall m. MonadAff m => (Contex2D -> m Unit) -> Maybe Context2D -> m Unit
                                     -- drawRect             -- mcontext

To summarize, you’re in a do block where every bind is meant to have the result type of MonadAff m => m a. Then, in the middle of this block, you have this expression drawRect <$> mcontext which has the type Maybe (m Unit). Because it’s not being assigned to anything with let, it’s being desugared to a discard:

_ <- drawRect <$> mcontext

which would only be acceptable to the compiler if this had the result type MonadAff m => m Unit. But it doesn’t – it’s actually a MonadAff m => Maybe (m Unit). So you need a function that will produce this m Unit result type (like traverse_, or for_, or your manual case statement).

Edit: Last thing: if you’d like, try copy/pasting your initial example (the entire module) into try.purescript.org. Then, once you fix the code:

-   drawRect <$> mcontext
- pure unit
+   traverse_ drawRect mcontext
+ pure Nothing

…you should see the canvas rendering properly. If you get stuck again in the future, you can always share a gist and link to it from Try PureScript to help folks immediately start troubleshooting :slight_smile:

This looks fine to me, for such a small example. But if drawOnCanvas doesn’t get much bigger or it doesn’t need to be re-used I might just put the useTickEffect directly in the where clause:

canvasComponent = Hooks.component \_ _ -> Hooks.do
  drawOnCanvas
  Hooks.pure $ HH.canvas ...
  where
  drawOnCanvas = Hooks.captures {} Hooks.useTickEffect do
    ...

I believe the drawRect <$> mcontext is leading the compiler to believe we’re in the Maybe monad, and then it sees the liftEffect $ getCanvasElementById "canvas" as an error because that’s running in HookM. I’m not exactly sure why that’s the error instead of the error being reported at drawContext <$> mcontext – after all, useTickEffect requires HookM – and perhaps someone else knows.

Errors are not so great within do blocks. Others can perhaps explain more as to why that is, but this error does seem to be reported in an unexpected place. You did the right thing by writing out all the types explicitly. I don’t know if you did this, but if debugging this I would have done this:

    Hooks.captures {} Hooks.useTickEffect do
      (mcanvas :: Maybe CanvasElement) <- liftEffect $ getCanvasElementById "canvas"
      (mcontext :: Maybe Context2D) <- liftEffect $ sequence $ getContext2D <$> mcanvas
      (drawRect <$> mcontext) :: Maybe (m Unit)
      (pure unit) :: HookM m (Maybe (HookM m Unit))

In the vast majority of cases doing this will pinpoint the error more effectively. In this case, oddly enough, the compiler seems really convinced that we’re in the Maybe monad. Even so, writing these types out explicitly perhaps helps it be clear where there are mismatches. For example, you can tell that pure unit couldn’t possibly have the right type.

Look right to me!

UseEffect is the Hook type, but that’s opaque – it doesn’t tell you anything about what code you can write. It’s just there to ensure you always use the same hooks in the same order.

The important thing is that you’re providing the use*Effect hook with a function of type HookM m Unit. In this monad you can do Halogen things like fork threads, start and stop subscriptions, raise output messages, and so on, and you can also do anything that your chosen monad m can do. For example, if m is Effect then you can use any Effect functions you want; if it’s Aff then you can use any Aff functions you want (and any Effect functions, via liftEffect), and so on.

HookM is used for use*Effect, but it’s also the type used for actions in your HTML. For example, if you write an onClick handler, that’ll also be in HookM.

Effects run after every render (unless their memo values haven’t changed, or it’s a lifecycle effect). So when your component initializes, it will queue up the drawOnCanvas effect (but not execute it), then it will render, then it will execute the drawOnCanvas effect.

Thank you for the detailed (and clear!) responses!

I put the code up on https://try.purescript.org/?gist=11c2fbb48fd85d811999880388e4fa9e for posterity.

I also saw the updated syntax for defining hooks in the docs and it looks like it’ll be very nice to use.

Unfortunately, @smilack, TryPureScript org doesn’t save the code in a shareable way based on the session ID. The current workflow is to instead save the code as a GitHub Gist and pair its ID with the ?gist= query.

e.g. take the Gist ID from your GitHub Gist URL:
https://gist.github.com/and-pete/a129c66339baabe7d083357e09a3ffdc

(which in this example is “a129c66339baabe7d083357e09a3ffdc”)

…to create your shareable TryPureScript link:
https://try.purescript.org/?gist=a129c66339baabe7d083357e09a3ffdc

:slight_smile:

Thanks! I edited the correct link into my previous post.

1 Like