5

edit: I have followed up with a more specific question. Thank you answerers here, and I think the followup question does a better job of explaining some confusion I introduced here.


TL;DR I'm struggling to get proofs of constraints into expressions, while using GADTs with existential constraints on the constructors. (that's a serious mouthful, sorry!)


I've distilled a problem down to the following. I have a simple GADT that represents points called X and function applications called F. The points X are constrained to be Objects.

data GADT ix a where
  X :: Object ix a => a -> GADT ix a
  F :: (a -> b) -> GADT ix a -> GADT ix b

Constrained refers to containers whose objects are constrained by something and Object is that something. (edit: my real problem involves Category and Cartesian classes from constrained-categories)

-- | I can constrain the values within containers of kind `* -> *`
class Constrained (ix :: * -> *) where
  type Object ix a :: Constraint

-- | Here's a trivial constraint. A more interesting one might include `Typeable a`, for ex
instance Constrained (GADT ix) where
  type Object (GADT ix) a = (Constrained ix, Object ix a)

I would like to write an expression:

-- error: Could not deduce: Object ix Int arising from a use of ‘X’
ex0 :: GADT ix String
ex0 = F show (X (3 :: Int))

And while the obvious solution works, it quickly becomes verbose when building larger expressions:

-- Typechecks, but eventually verbose
ex1 :: Object ix Int => GADT ix String
ex1 = F show (X (3 :: Int))

I think the correct solution should look something like this:

-- error: Could not deduce: Object ix Int arising from a use of ‘X’
ex2 :: Constrained ix => GADT ix String
ex2 = F show (X (3 :: Int))

But I still can't get that proof of Object ix Int.

I'm sure it's simpler than I'm thinking. I have tried adding constraints to the Object constraint family in the GADT class instance. I have tried offering constraints in the expression's signature. I have tried QuantifiedConstraints, although, I'm not sure I fully grasp it yet. Please help me wise ones!


Runnable:

{-# LANGUAGE GADTs #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE QuantifiedConstraints #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeFamilyDependencies #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE InstanceSigs #-}

module Test where

import Data.Kind
import Data.Functor.Identity
import Data.Functor.Const

-- | I can constrain the values within containers of kind `* -> *`
class Constrained (ix :: * -> *) where
  type Object ix a :: Constraint

-- | Here's a trivial constraint. A more interesting one might include `Typeable a`, for instance
instance Constrained (GADT ix) where
  type Object (GADT ix) a = (Constrained ix, Object ix a)

-- | A demo GADT that has function application ('F'), and points ('X'). The
-- points are constrained.
data GADT ix a where
  X :: Object ix a => a -> GADT ix a
  F :: (a -> b) -> GADT ix a -> GADT ix b

-- -- Broken
-- -- error: Could not deduce: Object ix Int arising from a use of ‘X’
-- ex0 :: GADT ix String
-- ex0 = F show (X (3 :: Int))

-- Typechecks
-- but for larger programs becomes verbose, requiring many explicit constraints
ex1 :: Object ix Int => GADT ix String
ex1 = F show (X (3 :: Int))

-- -- What I want, but, it's broken
-- ex2 :: Constrained ix => GADT ix String
-- ex2 = F show (X (3 :: Int))
Josh.F
  • 3,666
  • 2
  • 27
  • 37
  • 1
    Surely, `ex2` still needs to include at least the `Int` annotation? Or mayble you should use an example other than `show`, a function that's not polymorphic in its input. – leftaroundabout Nov 23 '21 at 09:45
  • Why don't you parameterise `GADT` on the constraints: `GADT :: (Type -> Constraint) -> (Type -> Type)` – Iceland_jack Nov 23 '21 at 12:48
  • @leftaroundabout agreed, the `Int` annotation would be needed in `ex2` to unify `Show a` with `Num a` – Josh.F Nov 23 '21 at 17:13
  • @Iceland_jack I've tried your approach but couldn't make it work either, though, it seems promising. I've asked a followup question [here](https://stackoverflow.com/questions/70088443/how-can-i-use-a-constraint-family-thats-in-scope-to-prove-instances-within-the) where I hope to clarify why this problem exists in the first place for me. – Josh.F Nov 23 '21 at 22:10

2 Answers2

1

I think the correct solution should look something like this:

-- error: Could not deduce: Object ix Int >arising from a use of ‘X’
ex2 :: Constrained ix => GADT ix String
ex2 = F show (X 3)

Unfortunately, this solution doesn't make any sense. The compiler is justified in pointing out that it doesn't know that Object ix Int is satisfied at this point, since all it knows is that Constrained ix may impose some constraint through Object ix Int.

A solution through quantification

So perhaps what you want is a constraint which says: "at this point, all Object ix a constraints I use are satisfied" - which we can try and do through quantification:

{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ConstraintKinds #-}

type Satisfied ix = forall a. Object ix a

ex2 :: Satisfied ix => GADT ix String
ex2 = F show (X 3)

Unfortunately, that gives us a GHC error:

• Quantified predicate must have a class or type variable head:
        forall a. Object ix a
• In the quantified constraint ‘forall a. Object ix a’
  In the type synonym declaration for ‘Satisfied’

Since Object is a type family and not a class or type variable.

Re-architecture

But... why is Object a type family? In fact, why does Constrained exist at all as a lawless class with no methods? If we want to exert constraints on combinations of containers and types, Haskell already gives us the means to do that - just use instance constraints!

{-# LANGUAGE MultiParamTypeClasses #-}

class Object ix a

type Constrained ix = forall a. Object ix a

Because if we have

instance (...<some stuff>...) => Constrained Foo where
  type Object ix a = (...<some def>...)

we could translate that to

instance (...<some stuff>..., ...<some def>...) 
  => Object ix a

Which makes this example compile.

ex2 :: Constrained ix => GADT ix String
ex2 :: F show (X 3)
Isaac van Bakel
  • 1,772
  • 10
  • 22
  • This makes sense. Unfortunately, what I've simplified to `Constrained`, in my real problem, is actually `Category` and `Cartesian` from `cartesian-categories`, which are lawful with methods. I don't know a way, other than TypeFamilies (ie a constraint family, here) to express the idea of a class whose objects are arbitrarily constrained subtypes of Hask, so, I don't think the rearchitecture will work for this particular problem. – Josh.F Nov 23 '21 at 17:08
  • Do you mean the `categories` library? I think you need to provide a more motivating example as to why treating `Object` as a class doesn't work as an approach, because it's not obvious to me from looking at those classes. – Isaac van Bakel Nov 23 '21 at 18:17
  • shoot, so sorry, it's this one! https://hackage.haskell.org/package/constrained-categories-0.4.1.0 – Josh.F Nov 23 '21 at 18:48
  • actually, here's a direct link to the class: https://hackage.haskell.org/package/constrained-categories-0.4.1.0/docs/Control-Category-Constrained.html#t:Category – Josh.F Nov 23 '21 at 18:57
  • Your examples very much go down the same paths I walked. Wrt your idea to rearchitecture it, I've asked a [follow up](https://stackoverflow.com/questions/70088443/how-can-i-use-a-constraint-family-thats-in-scope-to-prove-instances-within-the) question that shows why I can't, namely, the `Object` is a typefamily that constrains the co/domain and is a requirement from another library. – Josh.F Nov 23 '21 at 22:15
1

Without more context it's hard to say what's the best solution, but here are a couple of possibilities:

Avoid constraining at all

As it stands, your GADT doesn't seem to have much reason for restricting X to Object. Maybe this is just not needed?

data GADT ix a where
  X :: a -> GADT ix a
  F :: (a -> b) -> GADT ix a -> GADT ix b

Instead, the constraint could come from the outside when it's needed.

Bite the bullet of constraint lists, but make them nicer

If you have many different types in your expression that all need to fulfill the same constraint, you can use a helper like All

ex2' :: All (Object ix) '[Int] => GADT ix String
ex2' = F show (X (3 :: Int))

where there can be more types in the list in addition to Int; and/or you can make synonym constraints such as

type StdObjs ix = (Object ix Int, Object x Bool, ...)

ex2'' :: StdObjs ix => GADT ix String
ex2'' = F show (X (3 :: Int))

Propagate the constraints backwards through the data structure itself

If you do need the constraint on the X values, it may nevertheless be possible to express this in another way in the GADT. For example, if the function is not a general function but something that is already constrained to only accept Objects, you could have it like this:

data YourFunc ix a b where
  YourFunc :: Object ix a => (a->b) -> YourFunc ix a b

show' :: Object ix Int => YourFunc ix Int String
show' = YourFunc show

This doesn't directly help with the problem you were asking about, but maybe the function is shared or something. You could even have something like

class Object ix a => InferrenceChain ix a where
  type PreElem ix a :: Type
  propInferrence :: (InferrenceChain ix (PreElem a) => r) -> r

and then

data YourFunc ix a b where
  YourFunc :: InferrenceChain ix a
                 => (PreElem a -> a) -> YourFunc (PreElem a) a

Then in the end you could proof the X constraint from just putting in Object ix String on the outside and recursing over propInferrence. But this would probably get quite fiddly.

leftaroundabout
  • 117,950
  • 5
  • 174
  • 319
  • I've asked a [follow up question](https://stackoverflow.com/questions/70088443/how-can-i-use-a-constraint-family-thats-in-scope-to-prove-instances-within-the). Wrt eliding constraints, the follow up shows why I need them. Wrt a constraint list, I still think the boilerplate would become insufferably large. Wrt `YourFunc`, that'd introduce a ton of up front boiler plate (a new prelude), though, probably eliminate future boilerplate. Wrt `InferrenceChain`, I'm struggling to map it onto my problem, but maybe the followup helps explain better? Thank you btw! – Josh.F Nov 23 '21 at 22:13
  • woh, I just realized you're the author of the library I'm messing around with, `constrained-categories`, thanks for the library, it's amazing! – Josh.F Nov 23 '21 at 22:27
  • 1
    Well, I'm glad to hear you find it useful! – leftaroundabout Nov 23 '21 at 23:49