You need some run-time witness of the types to compare. The class Typeable provides features for this purpose:
sameFoos :: (Typeable a, Typeable b) => Foo a -> Foo b -> Maybe SameFoos
You can build a restricted variant of Typeable with GADTs, if you are okay with a closed universe of types (whereas Typeable is “open” because it automatically extends to newly declared types).
data MyTypeable a where
MyInt :: MyTypeable Int
MyBool :: MyTypeable Bool
MyList :: MyTypeable a -> MyTypeable [a]
MyPair :: MyTypeable a -> MyTypeable b -> MyTypeable (a, b)
sameFoos :: MyTypeable a -> MyTypeable b -> Foo a -> Foo b -> Maybe SameFoos
newtype Foo a = Foo a
data SameFoos = forall a. Show a => SameFoos (Foo a) (Foo a)
deriving instance Show a => Show (Foo a)
deriving instance Show SameFoos
class SameFoosClass a b where
sameFoos :: Foo a -> Foo b -> Maybe SameFoos
instance Show a => SameFoosClass a a where
sameFoos a b = Just (SameFoos a b)
instance {-# OVERLAPPABLE #-} SameFoosClass a b where
sameFoos _ _ = Nothing
main = do
print $ sameFoos (Foo True) (Foo "hello")
print $ sameFoos (Foo True) (Foo False)
If Foo :: Type -> Type then Typeable is the answer, as suggested by others. If Foo :: k -> Type, for some other kind k, then SingI is the answer (and Typeable is basically the SingI of Type, so it’s a strict generalization).
sameFoos :: (Typeable a, Typeable b) => Foo a -> Foo b -> Maybe SameFoos
sameFoos bar baz = do
Refl <- testEquality (typeRep @bar) (typeRep @baz)
Just $ SameFoos bar baz
If you wanted to be even more of a charlatan:
sameFoos :: (Typeable a, Typeable b) => a -> b -> Maybe SameFoos
sameFoos bar baz = case typeRep @bar of
App t1 t2 -> do
HRefl <- eqTypeRep t1 (typeRep @Foo)
Refl <- testEquality (typeRep @bar) (typeRep @baz)
Just $ SameFoos bar baz
_ -> Nothing
The second dynamically checks if both are Foo AND if the are the same kind of Foo.
I’d imagine dependent typing has a better solution than this or even the overlapping instances solution.