2

I'm using the Reflex.Dom library, which defines a set of functions for creating HTML DOM elements

  • el creates an element
  • el' creates and returns an element
  • elAttr creates an element with the given attributes
  • elAttr' creates and returns an element with the given attributes
  • etc

I'm making my own widget library and I don't want to define all those variations for every widget. So I wrote a typeclass that uses the same names, but defines all the functions in terms of one another, leaving only one of them to be defined in each instance:

{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}

module ElMaker where

import Data.Map (Map)
import qualified Data.Map as Map
import qualified Reflex.Dom as D

-- el: type of element to create
-- input: input parameter
-- output: return value
class (D.MonadWidget t m) => ElMaker t m el input output where
  el :: el -> input -> m output
  el e = elAttr e Map.empty

  elAttr :: el -> Map Text Text -> input -> m output
  elAttr e attrs input = snd <$> elAttr' e attrs input

  el' :: el -> input -> m (D.El t, output)
  el' e = elAttr' e Map.empty

  -- This is the only one to implement, yay!
  elAttr' :: el -> Map Text Text -> input -> m (D.El t, output)

I created an instance that uses the original elAttr' to test it out. It worked:

import Data.Text (Text)
import qualified Reflex.Dom as D

instance (D.MonadWidget t m) => ElMaker t m Text (m output) output where
  elAttr' = D.elAttr'

And then I created a Button widget instance that returns an event for when the button is clicked. It worked:

data Button = Button
instance (MonadWidget t m) => ElMaker t m Button (m input) (Event t ()) where
  elAttr' _ attrs contents = do
    (e, _) <- D.el' "button" contents
    return $ (e, D.domEvent D.Click e)

I'd like to be able to compose widgets, so I tried rewriting the Button instance to use the Text instance of ElMaker to create the element. But it fails to compile:

data Button = Button
instance (MonadWidget t m) => ElMaker t m Button (m input) (Event t ()) where
  elAttr' _ attrs contents = do
    (e, _) <- el' ("button" :: Text) contents
    return $ (e, D.domEvent D.Click e)

Compiler output:

MDL.hs:119:15: error:
    • Could not deduce (ElMaker t m Text (m input) output0)
        arising from a use of ‘el'’
      from the context: MonadWidget t m
        bound by the instance declaration at MDL.hs:116:10-71
      The type variable ‘output0’ is ambiguous
      Relevant bindings include
        contents :: m input (bound at MDL.hs:117:19)
        elAttr' :: Button
                   -> Map.Map Text Text -> m input -> m (D.El t, Event t ())
          (bound at MDL.hs:117:3)
      These potential instance exist:
        instance MonadWidget t m => ElMaker t m Text (m output) output
          -- Defined in ‘ElMaker’
    • In a stmt of a 'do' block:
        (e, _) <- el' ("button" :: Text) contents
      In the expression:
        do { (e, _) <- el' ("button" :: Text) contents;
             return $ (e, D.domEvent D.Click e) }
      In an equation for ‘elAttr'’:
          elAttr' _ attrs contents
            = do { (e, _) <- el' ("button" :: Text) contents;
                   return $ (e, D.domEvent D.Click e) }

I think this is because the function doesn't do anything with the value that would constrain its type, and the compiler really wants it to have a concrete type. But this typeclass doesn't care what the value of that type parameter is. Is there any way to compile this anyway?

alltom
  • 3,162
  • 4
  • 31
  • 47
  • Maybe I missing something, but what is your class for? Why not just use `elAttr'` if that's what you want? – dfeuer Oct 08 '16 at 19:58
  • dfeuer: I'd like to be able to compose widgets. I'll update the question to make that clear. – alltom Oct 08 '16 at 19:59
  • dfeuer: Also, I'd like my new functions to work the same as the original functions, if possible. If the original `el'` can be called this way, why can't my new `el'`? – alltom Oct 08 '16 at 20:03
  • I still don't understand what your class is for. What does it give you that `MonadWidget` does not? Why does it have so many parameters? – dfeuer Oct 08 '16 at 20:06
  • dfeuer: The typeclass lets me define `elAttr'` for a new kind of widget, and have `el`, `el'`, `elAttr`, etc defined for it automatically. `Button` is just a ` – alltom Oct 08 '16 at 20:10
  • Oh, I think I see what you mean. The names being borrowed confused me. – dfeuer Oct 08 '16 at 20:19
  • Sorry about that. Updated the question again to call out that the same names are used. I'd change the names, but I'm afraid I'd break the code in the process. – alltom Oct 08 '16 at 20:22
  • 1
    In all the functions you've mentioned, the implementation in `Reflex` leaves the parameters `t`, `m`, and `a` are free - it doesn't seem like there's any reason for `t` or `m` to be bound in your class. Furthermore, you use `el` as a sort of 'label' to pick what you are constructing, but then you would have to write `el Button ...` or `el SomethingElse ...` anyways - why not just have `el_Button`, `el_SomethingElse`, etc, and assign each the correct type? The other function defaults can just be functions themselves - taking a function of appropriate type (type of `elAttr'`) as input. – user2407038 Oct 08 '16 at 21:06
  • 1
    ... then you could write simply `el_Something = elDefault elAttr'_Something` - this would of course cost you 3 extra lines of boilerplate per 'instance' but then you are not fighting upstream against the type system to acheieve some almost arbitrary name overloading. – user2407038 Oct 08 '16 at 21:08

1 Answers1

5

What you probably want to do (and this is something you very often want to do; it's becoming something of a FAQ) is replace a constructor on the right side of => with an equality constraint on the left side.

{-# LANGUAGE GADTs #-}

instance (D.MonadWidget t m, input ~ m output)
   => ElMaker t m Text input output where ...

instance (D.MonadWidget t m, input' ~ m input, output ~ Event t ())
   => ElMaker t m Button input' output where ...

Once you know you're building Text or Button, you want to commit to a particular instance, and then to certain class parameters having particular shapes. Putting those in the instance constraints lets you do that.

For the particular case here, once you know that you're dealing with Text, you know which instance you want to use, and that you can calculate output by matching on input. You want GHC to know that, rather than wondering if some other Text instance will have a different input/output relationship.

Note: it's generally best for the critical class parameter that determines others to go last. So I'd make el the last parameter of ElMaker. This is good for newtype deriving, and is also conventional.

dfeuer
  • 48,079
  • 5
  • 63
  • 167