The Error Message#

I’ve written before about just how befuddling Haskell error messages can be, especially for beginners. And now, even though I have some professional Haskell development under my belt, I ran across a Haskell error message that confused me for a bit, where I had to get help. It’s clear to me now when I look at the error message what it’s trying to say, but I legitimately was stumped by it, and so, even though it’s embarrassing for me now, I feel the need to write about how this error message could have been easier to understand:

frontend/src/Frontend/WordTiles.hs:87:25-45: error:
    • Could not deduce (HasDomEvent t () 'ClickTag)
        arising from a use of ‘domEvent’
      from the context: (DomBuilder t m, PostBuild t m, MonadHold t m,
                         MonadFix m)
        bound by the type signature for:
                   app :: forall t (m :: * -> *).
                          (DomBuilder t m, PostBuild t m, MonadHold t m, MonadFix m) =>
                          m ()
        at frontend/src/Frontend/WordTiles.hs:(70,1)-(76,9)
    • In the expression: domEvent Click submit
      In an equation for ‘click’: click = domEvent Click submit
      In the second argument of ‘($)’, namely
        ‘do inputText <- fmap value $ inputElement $ def
            submit <- el "button" $ text "Submit"
            let click = domEvent Click submit
            pure $ current inputText <@ click’
   |
87 |             let click = domEvent Click submit
   |                         ^^^^^^^^^^^^^^^^^^^^^

The code in question was in the Reflex FRP’s “widget” monad, defined as usual by a number of monad typeclasses:

app
  :: ( DomBuilder t m
     , PostBuild t m
     , MonadHold t m
     , MonadFix m
     )
  => m ()
app = do
    let
        start = Game [] wordSet "PIETY"
        moveAll word (gm, _) = move word gm
    rec
        game <- foldDyn moveAll (start, []) newWord
        gameDisplay game
        newWord <- fmap (fmap T.unpack) $ el "div" $ do
            inputText <- fmap value $ inputElement $ def
            submit <- el "button" $ text "Submit"
            let click = domEvent Click submit
            pure $ current inputText <@ click
    pure ()

My Confusion#

Some of you might already see the problem, especially those who know Reflex. But I didn’t see it. My brain saw (HasDomEvent t () 'ClickTag) and completely misread it. I assumed it meant something like “with t as the tag, we can get the DOM event as 'ClickTag.” I assumed that the () was irrelevant to understanding the type, indicating some sort of optional type was not necessary to be provided.

I then tried to address this by adding (HasDomEvent t () 'ClickTag) to the context of app:

app
  :: ( DomBuilder t m
     , PostBuild t m
     , MonadHold t m
     , MonadFix m
     , HasDomEvent t () 'ClickTag
     )
  => m ()

It wasn’t the issue.

I had hoped this wasn’t the issue, but I thought it might be, and I had no idea what the issue actually was. Maybe we just needed to list all the DOM events t can handle, I had thought. I should’ve noticed it was t and not m, and I would expect m to be involved in such a context. I should have read the thing out loud in my head, and realized that it wasn’t t that didn’t have the DOM event of 'ClickTag, but (). But I didn’t. My eyes kind of glazed over at the complicated typeclass expression. I just didn’t think.

The Solution#

The problem, a friend had to tell me, was nothing to do with t and everything to do with (). submit was not, as I had thought, a representation of the DOM element I had created with a button. To do that, you need to call el':

(submit, _) <- el' "button" $ text "Submit"
let click = domEvent Click submit
pure $ current inputText <@ click

submit, gotten from el, was actually of type (). And, of course, you can’t get any DOM event out of (), let alone a Click.

Better Error Messages#

But while I left this situation with take-aways for myself, to better read Haskell error messages in the future, I was also frustrated at the Haskell compiler, especially in comparison to the Rust compiler I have gotten used to recently through my job.

List Involved Types#

How on earth did it not indicate at all that (HasDomEvent t () 'ClickTag) was a problem with the type of submit? Sure, the constraint “arose” from the type of domEvent, but submit is clearly an important value involved in making the type not work.

This is easier to implement than a Haskell person might think. I understand that it’s unclear which type “caused” the problem from a human perspective. So why not list them all? Just a laundry list of inferred types would’ve been helpful: I would have seen that submit was of type (), and that would’ve helped me through the situation. Is that too much to ask? Something like this:

Related types:
domEvent :: HasDomEvent t a => EventName en -> a -> Event t (EventResultType en)
Click :: EventName ClickTag
submit :: ()

Any two of those types would have given me the hint I needed. Really, either domEvent or submit would have enabled me to figure it out.

Warn About () Bindings#

Similarly, how on earth was I allowed to write this line without a warning:

submit <- el "button" $ text "Submit"

submit is invariately (). Shouldn’t binding a () value be at least a warning? In what possible situation would you want to do that? I know that situations exist, especially situations where a type is sometimes (), but this type is invariably (), and I have -Wall turned on in this project. I want warnings for things that there are occasionally legitimate use cases for. Binding a name to (), especially when it’s from a function call and not literally let unit = (), has got to be a mistake 99 times out of 100.

This is apparently not a warning in Rust either, and I am confused by that, because Rust is normally better about its warnings:

fn foo() {
}

fn main() {
    let x = foo(); // Compiles without warning!
    drop(x);
}

I think it would be a reasonable and useful warning in both programming languages. The opposite situation already provokes a warning in Haskell, where you have an action in a do-block that returns a value and you implicitly ignore it:

[jim@palatinate:~/Writing/TheCodedMessage/content/posts]$ ghci -Wall
GHCi, version 8.8.4: https://www.haskell.org/ghc/  :? for help
Prelude> do { pure 'x'; pure () }

<interactive>:1:6: warning: [-Wunused-do-bind]
    A do-notation statement discarded a result of type ‘Char’
    Suppress this warning by saying ‘_ <- pure 'x'’
Prelude>

It only makes sense that the converse mistake, which is even more likely to be a mistake, also have a warning.

Conclusion#

Error messages are an extremely important part of a programming language, both for adoption and for programmer efficiency. Part of the point in working in a strongly-typed language with a sophisticated type system, like Rust or Haskell, is supposed to be that we discover most of our problems through compiler error messages, rather than through runtime bugs. So most of our troubleshooting will happen at compile time, grappling with these error message. This makes error messages in Haskell more important than in the average programming language, and makes the standard for good error messages even higher. We can do better than the status quo, and we should.