4

I have some Haskell code that won't compile (with GHC 8.0.2). I think I understand the basic problem, but I would like to understand it better so I can avoid this in the future.

My library looks similar to this:

{-# language TypeFamilyDependencies #-}
{-# language GADTs #-}
{-# language RankNTypes #-}

module Lib where

type Key = Int

class Handle m where
    type Connection m = c | c -> m
    withConnection :: Connection m -> m a -> IO a

class (Handle m) => Data m where
    getKeyVal :: Key -> m String

data SomeConn where
    SomeConn :: (Data m) => Connection m -> SomeConn

useConnection :: SomeConn -> (forall m. Data m => m String) -> IO String
useConnection (SomeConn c) action = withConnection c action

The idea is that Data m represents a class of monads similar to ReaderT (Connection m) IO. I am hoping to write generic functions with the methods of this typeclass, and have the exact method instance be determined by the connection type wrapped with SomeConn (which is chosen at run-time).

Now the following code

getKeyValWith :: SomeConn -> Key -> IO String
getKeyValWith c = (useConnection c). getKeyVal

gives me the following error from GHC 8.0.2:

• Couldn't match type ‘m0 String’
                 with ‘forall (m :: * -> *). Data m => m String’
  Expected type: m0 String -> IO String
    Actual type: (forall (m :: * -> *). Data m => m String)
                 -> IO String
• In the first argument of ‘(.)’, namely ‘useConnection c’
  In the expression: useConnection c . getKeyVal
  In an equation for ‘getKeyValWith’:
      getKeyValWith c = useConnection c . getKeyVal

Strangely enough, the following works just fine:

getKeyValWith c k = useConnection c (getKeyVal k)

Less surprisingly, so does this:

getKeyValWith (SomeConn c) = withConnection c . getKeyVal

Is there a simple rule to understand why GHC doesn't like the first example, but the other examples are okay? Is there a way I can ask GHC for more information about what it's doing when it tries to compile the first definition? I understand this is probably not idiomatic Haskell (what some call the "Existential/typeclass anti-pattern").

Edit:

I should add that I run into the same problem even if I explicitly add the type getKeyVal :: Key -> (Data m => m String) in the first example. I can even give this function its own name with my chosen type signature (which typechecks), but I get the same error. But I see now that even when I explicitly add the type, running :t in GHCI (with -XRankNTypes) on it gives me back the original type with Data m => floated to the left. So I think I understand why GHC is balking at me. Can I force GHC to use my chosen type?

dunnl
  • 55
  • 6
  • I have read [this](https://stackoverflow.com/questions/9468963/runst-and-function-composition/9469942#9469942) and [this](https://stackoverflow.com/questions/8343239/type-error-with-rank-2-types-and-function-composition/8343964#8343964) but the content seems obscured by the discussion of `($)`. I am trying to understand what the problem is in general, and how I can either help GHC understand my code, or ask GHC to help me understand the problem. – dunnl Aug 31 '17 at 00:45
  • 1
    The type of `(. getKeyVal)` is `Data m => (m String -> c) -> Key -> c`; the type of `useConnection c` is `(forall m. Data m => m String) -> IO String`. Do you see why the types `m String -> IO String` and `(forall m . Data m => m String) -> IO String` cannot unify? Neither of the latter two definitions require performing this unification. – user2407038 Aug 31 '17 at 03:25
  • Might be helpful: https://stackoverflow.com/questions/43943793/function-composition-and-foralled-types – shree.pat18 Aug 31 '17 at 03:26
  • This might be a near-duplicate of https://stackoverflow.com/questions/45964162/could-not-match-polymorphic-type-in-haskell – chi Aug 31 '17 at 07:26
  • @user2407038, I do see that, and I edited my question to ask a new one: Can I force GHC to accept a different type signature on `getKeyVal`? – dunnl Aug 31 '17 at 12:48
  • @chi, the answer there mentions instantiating `(.)` as an issue. But in my edit, I think I'm running into an issue even before `(.)` comes into play. – dunnl Aug 31 '17 at 12:50
  • @dunnl Aren't you still using `(.)` in your edit? That can't work since its type variables can not be instantiated to the polymorphic type, I think. – chi Aug 31 '17 at 14:55
  • @chi My concern was that in GHCI, `:t getKeyVal :: Key -> (Data m => m String)` returns `getKeyVal :: Data m => Key -> m String`, without yet talking about `(.)`. My belief now is that the type is somewhat flexible (w.r.t. the exact location of the `forall`), and indeed the issue is `(.)` as you say. – dunnl Aug 31 '17 at 15:06
  • Ah, yes. `forall a . ...` and `Context => ...` are automatically hoisted -- moved to the top level if possible. This is usually not an issue, though. I.e. if I write `f :: Int -> forall a . a` it gets changed to `f :: forall a. Int -> a`, but after I apply `f 5` I get again `forall a. a`, so everything is mostly OK. Even composition usually works fine with that hoisting. A major problem, instead, is if composition requires to involve a polymorphic type, requiring impredicativity. – chi Aug 31 '17 at 15:18

1 Answers1

4

This is all about .. It's unable to pass a polymorphic argument between the functions, hence f . g doesn't work if f is rank-2 polymorphic. Notice the following works:

(~.) :: ((∀ m. Data m => m String) -> z) -> (x -> (∀ m. Data m => m String))
          -> x -> z
(~.) f g x = f (g x)

getKeyValWith :: SomeConn -> Key -> IO String
getKeyValWith c = useConnection c ~. getKeyVal

Ideally, . would have a type like

(.) :: ∀ c . ((∀ y . c y => y) -> z) -> x -> ((∀ y . c y => y) -> x)
               -> x -> z

and thus cover all special cases like ~. above. But this is not possible – it would require inferring the weakest possible constraint c to pick in any given situation – in the traditional case, c y = y~y₀ – and I'm pretty sure that is uncomputable in general.

(An interesting question is how far we could get if the compiler just inlined . as much as possible before type-checking, as it right now already does with $. If it did automatic eta-expansion, it could certainly get useConnection c . getKeyVal to work, but automatic eta-expansion is in general not a good idea...)

Hiding the Rank-2 polymorphism by wrapping the polymorphic argument in a GADT, as you did with SomeConn, is the usual workaround.

leftaroundabout
  • 117,950
  • 5
  • 174
  • 319
  • In light of the edit to my question, it appeared to me that GHC simply wouldn't allow me to say `getKeyVal :: Key -> (Data m => m String)` (or rather it would, but GHCI refuses to give me back a rank 2 type with `:t`). Would you say that my observation is just a red herring? It seems now that GHC is flexible with exact type of `getKeyVal` until it appears in a specific context. – dunnl Aug 31 '17 at 14:56
  • You can always move a universal quantifier out of the result of a function, that's why querying GHCi for `:t _ :: Key -> (∀ m . Data m => m String)` gives `∀ m . Data m => Key->m String`. But in case the type variable doesn't appear in the arguments, this can also be inverted and GHC will in fact happily do that, so there's no problem there. – leftaroundabout Aug 31 '17 at 15:09