6

Is there anyway to apply a constraint within another constraint such that this

{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE KindSignatures #-}

module Test where

type Con a = (Num a, Show a)
type App c a b = (c a, c b)

program :: App Con a b => a -> b -> String
program a b = show a ++ " " ++ show (b+1)

will work?

Currently GHC is giving me the following errors:

[1 of 1] Compiling Test             ( Test.hs, interpreted )

Test.hs:9:12: error:
    • Expected a constraint, but ‘App Con a b’ has kind ‘*’
    • In the type signature: program :: App Con a b => a -> b -> String
  |
9 | program :: App Con a b => a -> b -> String
  |            ^^^^^^^^^^^

Test.hs:9:16: error:
    • Expected kind ‘* -> *’, but ‘Con’ has kind ‘* -> Constraint’
    • In the first argument of ‘App’, namely ‘Con’
      In the type signature: program :: App Con a b => a -> b -> String
  |
9 | program :: App Con a b => a -> b -> String
  |                ^^^
Failed, no modules loaded.

Thanks!

thoughtpolice
  • 479
  • 4
  • 13
  • 1
    Related: https://stackoverflow.com/questions/4922560/why-doesnt-typesynonyminstances-allow-partially-applied-type-synonyms-to-be-use – danidiaz Dec 21 '20 at 13:37

2 Answers2

5

An easy way to fix this is to use the LiberalTypeSynonyms extension. This extension allows GHC to first treat the type synonyms as substitutions and only afterwards check that the synonyms are fully applied. Note that GHC can be a little silly at kind inference, so you'll need to be very clear with it (i.e., an explicit signature). Try this:

{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE LiberalTypeSynonyms #-}

module Test where

import Data.Kind (Constraint)

type Con a = (Num a, Show a)
type App c a b = (c a, c b) :: Constraint

program :: App Con a b => a -> b -> String
program a b = show a ++ " " ++ show (b+1)

Before I understood that this could be solved with LiberalTypeSynonyms, I had a different solution, which I'll keep here in case anyone's interested.


Although the error message you're getting is a bit misleading, the fundamental problem with your code comes down to the fact that GHC does not support partial application of type synonyms, which you have in App Con a b. There are a few ways to fix this, but I find the simplest is to convert the type synonym constraint into a class constraint following this pattern:

{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE UndecidableInstances #-}

type Con' a = (Num a, Show a)

class    Con' a => Con a
instance Con' a => Con a

You can use this definition of Con anywhere you were intending to use your old one.

If you're interested in how/why this works, it's basically a trick to get around GHC's lack of support for partial type synonym/family application for the particular cases where those type synonyms/families define simple constraints.

What we're doing is defining a class, and every class comes with a constraint of the same name. Now, notice that the class has no body, but critically, the class itself has a constraint (in the above case Con' a), which means that every instance of the class must have that same constraint.

Next, we make an incredibly generic instance of Con, one that covers any type so long as the constraint Con' holds on that type. In essence, this assures that any type that is an instance of Con' is also an instance of Con, and the Con' constraint on the Con class instance assures that GHC knows that anything that's an instance of Con also satisfies Con'. In total, the Con constraint is functionally equivalent to Con', but it can be partially applied. Success!


As another side note, the GHC proposal for unsaturated type families was recently accepted, so there may be a not-too-far-off future where these tricks are unnecessary because partial application of type families becomes allowed.

DDub
  • 3,884
  • 1
  • 5
  • 12
  • If the problem is partial application, is `LiberalTypeSynonyms` enough? (I mean, I know it's not, because I checked, but why not?) – Daniel Wagner Dec 21 '20 at 15:34
  • If you look at [the docs for `LiberalTypeSynonyms`](https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/glasgow_exts.html#extension-LiberalTypeSynonyms), it says, "With the LiberalTypeSynonyms extension, GHC does validity checking on types only after expanding type synonyms." The problem is that if the type synonym is only partially applied, it doesn't get expanded. – DDub Dec 21 '20 at 16:39
  • In that same documentation, a few paragraphs later: "You can apply a type synonym to a partially applied type synonym:". – Daniel Wagner Dec 21 '20 at 17:15
  • 1
    @DanielWagner Huh! I did not know that `LiberalTypeSynonyms` worked that way. Thanks for pointing that out! Actually, now I can get your example to type check without my silly trick, and I'll update my answer to reflect that. – DDub Dec 21 '20 at 18:36
  • 2
    Your "silly" trick is not at all silly. In a number of situations, it's really the only way to go. – dfeuer Dec 21 '20 at 20:23
3

Haskell does not support type-level lambdas, nor partial application of type families / type synonyms. Your Con must always be fully applied, it can not passed unapplied to another type synonym.

At best, we can try to use "defunctionalization" as follows, effectively giving names to the type-level lambdas we need.

{-# LANGUAGE ConstraintKinds, KindSignatures, TypeFamilies #-}

import Data.Kind

-- Generic application operator
type family Apply f x :: Constraint

-- A name for the type-level lambda we need
data Con
-- How it can be applied
type instance Apply Con x = (Show x, Num x)

-- The wanted type-level function
type App c a b = (Apply c a, Apply c b)

-- Con can now be passed since it's a name, not a function
program :: App Con a b => a -> b -> String
program a b = show a ++ " " ++ show (b+1)

To call App with a different first argument, one would need to repeat this technique: define a custom dummy type name (like Con) and describe how to apply it (using type instance Apply ... = ...).

chi
  • 111,837
  • 3
  • 133
  • 218
  • 1
    Defunctionalization seems very heavy for this. The wrapper class pattern is generally sufficient for `Constraint` stuff. Defunctionalization seems most important for dealing with promoted kinds, and I imagine it can help avoid `newtype` noise in `Type`. – dfeuer Dec 21 '20 at 23:54
  • @dfeuer I agree. I somehow expected the OP needed something more general than what they asked. – chi Dec 22 '20 at 10:25