2

I've got a situation in a Haskell web project where I'm getting the error Ambiguous type variable.

The relevant code is

--- Other import statements
import qualified Model as Model

---------- HTTP Handlers
needItem db user itemName = do
  Model.changeItemStatus db user itemName Model.Need
  listItems db user

gotItem db user itemName = do
  Model.changeItemStatus db user itemName Model.Got
  listItems db user

newItem db user itemName comment count = do
  Model.insertItem db user itemName comment count
  listItems db user

listItems db user = do
  lst <- Model.listItems db user
  resOk lst

--- rest of the program

The errors are specifically complaining about insertItem and changeItemStatus, both of which are acid-state query functions

insertItem db name itemName comment count = withAccount db name newItem
  where item = Item { itemName = itemName, itemComment = comment, itemCount = count, itemStatus = Need } 
        newItem account = update' db $ NewItem account item

-- other stuff

changeItemStatus db name itemName status = withAccount db name cItem
  where cItem account = withItem account itemName
                        (\i -> update' db $ ChangeItem account $ i { itemStatus = status})

withAccount and withItem are helper functions to help me deal with Maybe return values from the database. They're defined as

withAccount :: MonadIO m => AcidState GoGetDB -> String -> (Account -> a) -> m (Maybe a)
withAccount db name fn = do
  maybeAccount <- query' db $ AccountByName name
  return $ do acct <- maybeAccount
              return $ fn acct

withItem :: Account -> String -> (Item -> a) -> Maybe a
withItem account itemName fn = do
  item <- getOne $ (accountItems account) @= itemName
  return $ fn item

Ok, now then.

I've read over the AcidState documentation in the Happstack crash-course several times, but it didn't help much; they use queries directly in their response generating functions which

  • I've tried and got the same Ambiguous type variable error, except pointing at the query' call iteslf,
  • I don't really want to do since that would force me to mix my model/controller code and
  • doesn't help my particular situation, since it doesn't show me what the concrete return type of a function that calls query' or update' would be (their functions are all AcidState DBName -> ServerPart Response, since they generate the response directly).

I've tried to put together type signatures for insertItem and changeItemStatus by using :t on pieces of the expression, but every attempt has given me what I assume is the worse error of No instance for (MonadIO m1) instead.

I'm not a particularly adept Haskeller yet, so I feel like the only other thing I could try is sprinkling random return $s around the place, but that doesn't sound like it would have a good chance of actually fixing the problem, or teaching me anything.

The general concept I'm trying to implement is: "Make this change to the DB, and then return the (potentially altered) list of relevant elements for the current user".

Any hints on what I should try next, or where I'm specifically going wrong? Am I thinking about this in entirely the wrong way? Is there other documentation I could consult on the subject?

PS. I've included what I think is all relevant code above, but if you want to see the complete source, it's here and here.

EDIT: Full type errors

/home/inaimathi/projects/goget/goget.hs:31:3:
    Ambiguous type variable `m2' in the constraint:
      (Control.Monad.IO.Class.MonadIO m2)
        arising from a use of `Model.changeItemStatus'
    Probable fix: add a type signature that fixes these type variable(s)
    In a stmt of a 'do' block:
      Model.changeItemStatus db user itemName Model.Need
    In the expression:
      do { Model.changeItemStatus db user itemName Model.Need;
           listItems db user }
    In an equation for `needItem':
        needItem db user itemName
          = do { Model.changeItemStatus db user itemName Model.Need;
                 listItems db user }

/home/inaimathi/projects/goget/goget.hs:35:3:
    Ambiguous type variable `m1' in the constraint:
      (Control.Monad.IO.Class.MonadIO m1)
        arising from a use of `Model.changeItemStatus'
    Probable fix: add a type signature that fixes these type variable(s)
    In a stmt of a 'do' block:
      Model.changeItemStatus db user itemName Model.Got
    In the expression:
      do { Model.changeItemStatus db user itemName Model.Got;
           listItems db user }
    In an equation for `gotItem':
        gotItem db user itemName
          = do { Model.changeItemStatus db user itemName Model.Got;
                 listItems db user }

/home/inaimathi/projects/goget/goget.hs:39:3:
    Ambiguous type variable `m0' in the constraint:
      (Control.Monad.IO.Class.MonadIO m0)
        arising from a use of `Model.insertItem'
    Probable fix: add a type signature that fixes these type variable(s)
    In a stmt of a 'do' block:
      Model.insertItem db user itemName comment count
    In the expression:
      do { Model.insertItem db user itemName comment count;
           listItems db user }
    In an equation for `newItem':
        newItem db user itemName comment count
          = do { Model.insertItem db user itemName comment count;
                 listItems db user }
Inaimathi
  • 13,853
  • 9
  • 49
  • 93
  • It would help if you could include the actual error in full and if you could add type signatures to every function. – Dag Feb 06 '13 at 17:43
  • @Dag - Type error in full added. If I could add type signatures (more precise than what `:t` gives me in GHCi) to everything, I wouldn't be having a problem here. Incidentally, GHCi says `insertItem :: AcidState GoGetDB -> String -> String -> String -> Integer -> m1 (Maybe (m (acid-state-0.8.2:Data.Acid.Common.EventResult NewItem)))` and `changeItemStatus :: (MonadIO m, MonadIO m1) => AcidState GoGetDB -> String -> String -> ItemStatus -> m1 (Maybe (Maybe (m (acid-state-0.8.2:Data.Acid.Common.EventResult ChangeItem))))` respectively. – Inaimathi Feb 06 '13 at 20:18

1 Answers1

2

Write type signatures for your functions to capture what they should do. Write them before you write the implementation, then you can use the type signatures, and the better error messages you get from the compiler if it knows what type you want to have, to check that the implementation does [or at least has a chance to do] what you want.

Let us look at the problem children, and what they actually do:

From acid-state we use

update' :: (UpdateEvent event, MonadIO m) => AcidState (EventState event) -> event -> m (EventResult event)

for

insertItem db name itemName comment count = withAccount db name newItem
  where item = Item { itemName = itemName, itemComment = comment, itemCount = count, itemStatus = Need } 
        newItem account = update' db $ NewItem account item

Now let's see what sort of type that would produce. From the use of

withAccount :: MonadIO m => AcidState GoGetDB -> String -> (Account -> a) -> m (Maybe a)

we see that the result has type

m (Maybe a)

for some MonadIO m, where a is the result type of newItem. Now,

newItem account = update' db something

so

newItem :: MonadIO mio => Account -> mio (EventResult type_of_something)

and thus the result type of insertItem would be

m (Maybe (mio (EventResult type_of_something)))

and in

newItem db user itemName comment count = do
  Model.insertItem db user itemName comment count
  listItems db user

the compiler doesn't know which MonadIO mio it should use in the second line. Thus the type variable mio is ambiguous.

Note that specifying the type variable there would still not do what you presumably want. I expect that you actually want to execute the update', and not only compute the action that would update the database when executed.

For insertItem, if it should indeed update the database, withAccount as it stands is not useful. You could maybe use something like

withAccountM :: MonadIO m => AcidState GoGetDB -> String -> (Account -> m a) -> m (Maybe a)
withAccountM db name fn = do
  maybeAccount <- query' db $ AccountByName name
  case maybeAccount of
    Nothing -> return Nothing
    Just acct -> do
         result <- fn acct
         return $ Just result

(not tested, could still be entirely wrong) to actually execute the update'.

The problems and probable fixes are similar for changItemStatus.

Daniel Fischer
  • 181,706
  • 17
  • 308
  • 431
  • Trying to add an `EventResult` constructor to the type signature gives me `Not in scope: type constructor or class 'EventResult'`. Trying to import the module that supposedly implements it (`Data.Acid.Common`) gives me `Could not find module 'Data.Acid.Common' it is a hidden module in the package 'acid-state-0.8.2'`. I'm also not entirely clear on why `update' db something` wouldn't update the database state, but `do { res <- update' db something; return res }` would. Aren't those equivalent? – Inaimathi Feb 06 '13 at 20:57
  • `Data.Acid.Common`? I don't see such a module in `acid-state`. `EventResult` is a type alias defined in [Data.Acid.Memory.Pure](http://hackage.haskell.org/packages/archive/acid-state/0.8.2/doc/html/Data-Acid-Memory-Pure.html#t:EventResult). What package are you actually using? – Daniel Fischer Feb 06 '13 at 21:09
  • 1
    The important change is that in `withAccountM`, I execute the action `fn acct` [sorry, forgot the argument `acct` there initially] in the `MonadIO m`, while in `withAccount`, `fn` was treated as a pure function (that happened to return an action) that was `fmap`ped over a `Maybe`. – Daniel Fischer Feb 06 '13 at 21:13
  • Hm. Doing `import Data.Acid.Memory.Pure (EventResult)` gives me access to `EventResult`, but if I don't import that package, GHCi reports the inferred return type of `insertItem` as `mio (Maybe (m (acid-state-0.8.2:Data.Acid.Common.EventResult NewItem)))` (which is why I assumed that I would get access to `EventResult` by `import`ing `Data.Acid.Common`). – Inaimathi Feb 06 '13 at 21:16
  • Ah, right. Hidden module where stuff is defined, exported via another one. If something is not in scope, GHC tells you where it's defined in this case, not where you can get it from. – Daniel Fischer Feb 06 '13 at 21:20
  • @your-second-comment - So in order to execute the query, I need to bind it to a result and return that. That probably means `withItem` *also* isn't doing what I meant for it to. I'll need to sit down later and take a much closer look at the whole `Model.hs`. – Inaimathi Feb 06 '13 at 21:23
  • Not quite. `update' db something` is indeed equivalent to `do { res <- update' db something; return res; }`. But that's not the change. The change is that you had a `Maybe` do-block under the `return`. That was `return $ case maybeAccount of { Nothing -> Nothing; Just acct -> Just (fn acct); }`, so you basically had `return $ Just (update' db something)` there, and that doesn't execute the `update'`. – Daniel Fischer Feb 06 '13 at 21:30
  • Wait, no. I did actually pass the account to that function before returning it (`return $ fn acct`). This is why I'm confused; if it was just `return $ fn`, ok, yeah, that's just returning a partial function, but shouldn't the last chunk in my original definition of `withAccount` be equivalent to `return $ Just $ (update' do something) acct`? Line 2 queries for an account (`maybeAccount` is `Maybe Account`), the next line binds that (`acct` is `Account`), what I'm intending with the last line is to apply `fn` to `acct` and return the result. Ctd. – Inaimathi Feb 06 '13 at 21:47
  • In `insertItem`, `fn` is `(\a -> update' db $ NewItem a item)` (albeit expressed slightly differently), so I'd expect that to actually (potentially) run as part of the `withAccount` call. – Inaimathi Feb 06 '13 at 21:48
  • No, that is the point. In the `return $ fn acct`, the `return` is that of `Maybe`, i.e. `Just`. You have `(return :: a -> mio a) $ maybe_do_block`. In the `maybe_do_block`, you compute an action of type `mio2 whatever`, but of course that is not run. You end up with `return $ Just $ update' db $ NewItem acct item`. – Daniel Fischer Feb 06 '13 at 22:04
  • Ok. This answer didn't directly solve my problem, but it (and the accompanying discussion) has revealed a few places where I didn't think things through sufficiently. I think an accept is fair enough at this point. – Inaimathi Feb 07 '13 at 14:58