Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to not apply instance constraints on a restricted function which uses a more generic function?

Let's say I have a function:

logResult :: (MonadIO m, ToJSON a) => Either MyError (Response a) -> m ()
logResult = ...

In this function, if I get:

  1. Right (Response a) - I call toJSON to log the result.
  2. Left MyError - I log it as well. MyError already has a ToJSON instance defined.

Now I want to write a helper function:

logError :: (MonadIO m) :: MyError -> m ()
logError err = logResult (Left err)

But GHC complains something along the following lines:

    • Could not deduce (ToJSON a0) arising from a use of ‘logResult’                                                                    
      from the context: MonadIO m                                                                                                       
        bound by the type signature for:                                                                                                
                   logError :: forall (m :: * -> *).                                                                                    
                               MonadIO m =>                                                                                             
                               L.Logger                                                                                                 
                               -> Wai.Request
                               -> MyError
                               -> m ()

...
...
The type variable ‘a0’ is ambiguous 

I understand the error is because logResult needs to guarantee that the a in Response a must have a ToJSON instance defined. But in logError I am explicitly passing Left MyError. Shouldn't this disambiguate ?

Is there any way I can write the logError helper function ?

PS: I have simplified the type signatures in the example. The error message has the gory details.

like image 898
ecthiender Avatar asked May 23 '19 21:05

ecthiender


1 Answers

Why is this one function? If the behavior of this function splits so cleanly into two, then it should be two functions. That is, you've written one monolithic function and are trying to define a simpler function as a utility using it. Instead, write a simple function and write the monolithic function as a composition of it with another. The type is pretty much asking for it: Either a b -> c is isomorphic to (a -> c, b -> c).

-- you may need to factor out some common utility stuff, too
logError :: (MonadIO m) :: MyError -> m ()
logResponse :: (MonadIO m, ToJSON a) => Response a -> m ()

logResult :: (MonadIO m, ToJSON a) => Either MyError (Response a) -> m ()
logResult = either logError logResponse

logResult still has its uses; if you get an Either MyError (Response a) from some library, then logResult can deal with it without much fuss. But, otherwise, you shouldn't be writing logResult (Left _) or logResult (Right _) very often; that essentially treats logResult . Left and logResult . Right as their own functions, which leads you back to actually writing them as separate functions.

But in logError I am explicitly passing Left MyError. Shouldn't this disambiguate?

No, it shouldn't. The end and beginning of the issue is that logResult looks like this:

logResult :: (MonadIO m, ToJSON a) => Either MyError (Response a) -> m ()

When you call it, the implementation doesn't matter one lick. The type says you need ToJSON a—you need to provide ToJSON a. That's it. If you know that you don't need ToJSON a for Left values, then you possess useful information that is not reflected in the type. You should add that information to the type, which, in this case, means splitting it into two. It would (IMO) actually be bad language design to allow what you were thinking of, because the halting problem should make it impossible to do correctly.

like image 187
HTNW Avatar answered Nov 19 '22 20:11

HTNW