haskellimpredicativetypes

Simple example for ImpredicativeTypes


The GHC user's guide describes the impredicative polymorphism extension with reference to the following example:

f :: Maybe (forall a. [a] -> [a]) -> Maybe ([Int], [Char])
f (Just g) = Just (g [3], g "hello")
f Nothing  = Nothing

However, when I define this example in a file and try to call it, I get a type error:

ghci> f (Just reverse)

<interactive>:8:9:
    Couldn't match expected type `forall a. [a] -> [a]'
                with actual type `[a0] -> [a0]'
    In the first argument of `Just', namely `reverse'
    In the first argument of `f', namely `(Just reverse)'
    In the expression: f (Just reverse)
ghci> f (Just id)

<interactive>:9:9:
    Couldn't match expected type `forall a. [a] -> [a]'
                with actual type `a0 -> a0'
    In the first argument of `Just', namely `id'
    In the first argument of `f', namely `(Just id)'
    In the expression: f (Just id)

Seemingly only undefined, Nothing, or Just undefined satisfies the type-checker.

I have two questions, therefore:

The latter is particularly with the HaskellWiki page on Impredicative Polymorphism in mind, which currently makes a decidedly unconvincing case for the existence of the extension.


Solution

  • Here's an example of how one project, const-math-ghc-plugin, uses ImpredicativeTypes to specify a list of matching rules.

    The idea is that when we have an expression of the form App (PrimOp nameStr) (Lit litVal), we want to look up the appropriate rule based upon the primop name. A litVal will be either a MachFloat d or MachDouble d (d is a Rational). If we find a rule, we want to apply the function for that rule to d converted to the correct type.

    The function mkUnaryCollapseIEEE does this for unary functions.

    mkUnaryCollapseIEEE :: (forall a. RealFloat a => (a -> a))
                        -> Opts
                        -> CoreExpr
                        -> CoreM CoreExpr
    mkUnaryCollapseIEEE fnE opts expr@(App f1 (App f2 (Lit lit)))
        | isDHash f2, MachDouble d <- lit = e d mkDoubleLitDouble
        | isFHash f2, MachFloat d  <- lit = e d mkFloatLitFloat
        where
            e d = evalUnaryIEEE opts fnE f1 f2 d expr
    

    The first argument needs to have a Rank-2 type, because it will be instantiated at either Float or Double depending on the literal constructor. The list of rules looks like this:

    unarySubIEEE :: String -> (forall a. RealFloat a => a -> a) -> CMSub
    unarySubIEEE nm fn = CMSub nm (mkUnaryCollapseIEEE fn)
    
    subs =
        [ unarySubIEEE "GHC.Float.exp"    exp
        , unarySubIEEE "GHC.Float.log"    log
        , unarySubIEEE "GHC.Float.sqrt"   sqrt
        -- lines omitted
        , unarySubIEEE "GHC.Float.atanh"  atanh
        ]
    

    This is ok, if a bit too much boilerplate for my taste.

    However, there's a similar function mkUnaryCollapsePrimIEEE. In this case, the rules are different for different GHC versions. If we want to support multiple GHCs, it gets a bit tricky. If we took the same approach, the subs definition would require a lot of CPP, which can be unmaintainable. Instead, we defined the rules in a separate file for each GHC version. However, mkUnaryCollapsePrimIEEE isn't available in those modules due to circular import issues. We could probably re-structure the modules to make it work, but instead we defined the rulesets as:

    unaryPrimRules :: [(String, (forall a. RealFloat a => a -> a))]
    unaryPrimRules =
        [ ("GHC.Prim.expDouble#"    , exp)
        , ("GHC.Prim.logDouble#"    , log)
        -- lines omitted
        , ("GHC.Prim.expFloat#"     , exp)
        , ("GHC.Prim.logFloat#"     , log)
        ]
    

    By using ImpredicativeTypes, we can keep a list of Rank-2 functions, ready to use for the first argument to mkUnaryCollapsePrimIEEE. The alternatives would be much more CPP/boilerplate, changing the module structure (or circular imports), or a lot of code duplication. None of which I would like.

    I do seem to recall GHC HQ indicating that they would like to drop support for the extension, but perhaps they've reconsidered. It is quite useful at times.