2

I'm currently building a server in haskell and as a newbie to the language, I'd like to try a new approach zu Monad composition. The idea is that we can write library methods like

    isGetRequest :: (SupportsRequests m r) => m Bool
    isGetRequest = do
        method <- liftRequests $ requestMethod
        return $ method == GET

    class (Monad m, RequestSupport r) => SupportsRequests m r | m -> r where
        liftRequests :: r a -> m a

    class (Monad r) => RequestSupport r where
        requestMethod :: r Method

which work without knowing the underlying monad. Of course in this example, it would have been sufficient to make isGetRequest operate directly on the (RequestSupport r) monad but the idea is that my library might also have more than one constraint on the monad. Yet, I do not want to implement all of those different concerns in the same module nor spread them across different modules (orphan instances!). That's why the m monad only implements the Supports* classes, delegating the real concerns to other monads.

The above code should work perfectly (with some language extensions to GHC). Unfortunately, I got some problems with the CRUD (Create Read Update Delete) concern:

class (Monad m, CRUDSupport c a) => SupportsCRUD m c a | m a -> c where
    liftCRUD :: c x -> m x

class (Monad c) => CRUDSupport c a | c -> a where
    list :: c [a] -- List all entities of type a

No I get an error:

Could not deduce (SupportsCRUD m c a0) from the context [...]
The type variable 'a0' is ambiguous [...]
To defer the ambiguity check to use sites, enable AllowAmbiguousTypes
When checking the class method: liftCRUD [...]

Seems like the type checker doesn't like that the a parameter does not directly arise in the signature of liftCRUD. That's understandable because a cannot be derived from the functional dependencies.

The type checker in my brain tells me that it should not be a problem to infer the type a later on, using AllowAmbiguousTypes, when some method regarding CRUD is executed in a library method. Unfortunately, GHC seems unable to do this inference step, for example

bookAvailable :: (SupportsCRUD m c Book) => m Bool
bookAvailable = do
    books <- liftCRUD (list :: c [Book]) -- I use ScopedTypeVariables
    case books of
        [] -> return False
        _ -> return True

yields

Could not deduce (SupportsCRUD m c0 a1) arising from a use of 'liftCRUD' [...]
The type variables c0, a1 are ambiguous [...]

It seems that I am still unable to reason about the compiler. I there a way to resolve this problem? Or at least a way to understand what the compiler is able to infer?

Best Regards, bloxx

luqui
  • 59,485
  • 12
  • 145
  • 204
bloxx
  • 123
  • 7
  • 3
    The `SupportsCRUD` class declaration involves three type variables, `m`, `c`, and `a`. Can you say what values you expect the compiler to infer for these three variables in your example, and why? – Daniel Wagner Oct 10 '17 at 17:18
  • 1
    What did you intend by your fundep `m a -> c`? What that means is "the choice of `c` is uniquely determined by the specific types `m` and `a`", which has the effect that the type checker can choose an instance based on only `m` and `a` in context, and the `c` follows from whatever you've put in the instance head. – jberryman Oct 10 '17 at 17:21
  • @jberryman It actually should tell the compiler that for the monad m and the "CRUD entity" a, the type checker can choose a unique monad c (this is the one supporting the CRUD operations). – bloxx Oct 10 '17 at 17:40
  • @DanielWagner The idea is that m is not specified here at all. m is the monad we want to get back from bookAvailable. a should just be Book because this it the entity we run CRUD queries about. Because m implements SupportsCRUD, the compiler should infer the monad c to lift into. c is the monad supporting the CRUD operations. I hope this helps; I wasn't able to condense my problem into a simpler case. – bloxx Oct 10 '17 at 17:45
  • 1
    I've made a few obvious fixes to your code in order to reproduce the error you quote. Noting it here so you can review, in case it's something you missed and not just a question-typo. – luqui Oct 10 '17 at 17:51
  • Yes, thank you! I was just also about to fix it. But it seems that your (deleted) comment to use ScopedTypeVariables indeed fixed my problem... – bloxx Oct 10 '17 at 17:53

1 Answers1

3

To use ScopedTypeVariables you also need to bind the variables you want to be in scope with forall. So it should be

bookAvailable :: forall m c. (SupportsCRUD m c Book) => m Bool
...

That was all that was necessary (after some trivial fixes I made which I assume were typos from entering your question) for me to get the code to compile.

cdk
  • 6,698
  • 24
  • 51
luqui
  • 59,485
  • 12
  • 145
  • 204
  • Wow! Well, that surprises me so completely that now I have a question: why was GHC willing to choose `a ~ Book` when inferring the type of `liftCRUD`? Clearly with that choice there is an appropriate instance available; but just as clearly one could come along later and add another instance that would also be a fine choice, which to me screams "ambiguous". – Daniel Wagner Oct 10 '17 at 17:56
  • 2
    Aha, and after trying to make a witness for my claim that "clearly one could come along later and add another instance", I think perhaps I see the reason: there is a key `c -> a` fundep on `CRUDSupport`, and all `SupportsCRUD` instances imply a corresponding `CRUDSupport` instance. – Daniel Wagner Oct 10 '17 at 17:58
  • @DanielWagner yeah it tripped me up too. I missed that `CRUDSupport` was a superclass of `SupportsCRUD` because it was declared afterward, and I'm used to superclasses coming first. – luqui Oct 10 '17 at 17:59
  • Good to know, I'm still not familiar to Haskell best practice conventions.. :) – bloxx Oct 10 '17 at 18:16
  • @bloxx Yeah I'm not sure either. I see two styles: "first here's the thing I want to define, then here is what I needed to define it", and "here's some definitions, and after all that we can get to what we want". Different styles of thought. – luqui Oct 10 '17 at 20:08