2
votes

I will briefly explain my chain of thought before the example so that if any of it does not make sense we are able to fix that as well.

Suppose that for each type constructor of my data declaration (a sum of product types) there is one parameter that has an instance to a given type class. In my head, that means that I would be able to explain to GHC/Haskell how to get to that specific type so that my type would end up behaving like an instance of that type class.

For example:

data Vector

-- The class type I talked about
class Transformable a where
    setPosition' :: Vector -> a -> IO ()
    setOrigin'   :: Vector -> a -> IO ()
    setAngle'     :: Float  -> a -> IO ()
    -- ... this goes a long way


data TCircleShape
data TSquareShape
data TTriangleShape
data TConvexShape
-- Large sum type that defines different types of Shape
data Shape  = Circle Float TCircleShape
            | Square Float String TSquareShape
            | Triangle Float TTriangleShape
            | Convex [Vector] Float TConvexShape
            -- ...

-- Almost all of the the Shape constructors have at least one
-- parameter that has an instance of the Transformable typeclass:
instance Transformable TCircleShape
instance Transformable TSquareShape
instance Transformable TTriangleShape
instance Transformable TConvexShape

-- What I would like to write then is:
runOnTransformable :: Transformable a => (a -> IO ()) -> Shape -> IO ()
runOnTransformable = undefined -- (???)

-- What I am doing right now is simply expanding Shape manually:
setShapePosition :: Vector -> Shape -> IO ()
setShapePosition v (Circle _ ptr)   = setPosition' v ptr
setShapePosition v (Square _ _ ptr) = setPosition' v ptr
-- and so on...

setShapeAngle' :: Float -> Shape -> IO ()
setShapeAngle' f (Circle _ ptr)   = setAngle' f ptr
setShapeAngle' f (Convex _ _ ptr) = setAngle' f ptr
-- and so on...

There is a clear pattern in my eyes and I would like to have some way of abstracting this unwrapping somehow.

One might try to have an instance for the data type itself:

instance Transformable Shape where
    setPosition' v (Circle _ ptr) = setPosition' v ptr
    -- [...]

    setAngle' f (Convex _ _ ptr) = setAngle' f ptr
    -- [...]

The downside is that I would have to 'reimplement' all the methods by manually unwrapping the type classes again, except it is in an instance declaration. Right?

Coming back to my question: Is there a way to inform Haskell how to unwrap and act upon a type class from a sum type?

I have a really thin familiarity with Lens and none with TemplateHaskell, however, if using said features would be a probable solution, by all means, go for it.

2

2 Answers

6
votes

Your runOnTransformable function is not possible to write as specified, because its type signature is wrong.

runOnTransformable :: Transformable a => (a -> IO ()) -> Shape -> IO ()

means that for any a, which the caller of runOnTransformable chooses, they can provide you a function taking that specific a, and you will call that function with an a of the right type, which you will produce somehow from the Shape object you have. Now, that is clearly not possible, because they may pass you a function of type TSquareShape -> IO () but a Shape which has no TSquareShape in it. Worse, GHC will worry that someone may define instance Transformable Integer where {...}, and you need to be able to handle that case too even though your Shape type doesn't have any way to guess what Integer to give to this function.

You don't want to say that your function works for any Transformable a => a, but rather that the caller's function must work for any Transformable a => a, so that it will be willing to accept whatever value happens to live in your Shape type. You will need the RankNTypes extension to enable you to write the correct signature:

runOnTransformable :: (forall a. Transformable a => a -> IO ()) -> Shape -> IO ()

Sadly, after you've done this I still don't know an automated way to implement this function for all of the various constructors of Shape. I think something ought to be possible with Generic or Data, or Template Haskell or something, but that's beyond my knowledge. Hopefully what I've written here is enough to get you moving in the right direction.

2
votes

Warning: Speculative answer, proceed with care.

Here is an alternative approach that uses data families. A data family is, in essence, a type-level function which introduces brand new types for their results. In this case, the data families ShapeData and TShape are used to produce the types of the Shape fields.

{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE EmptyDataDecls #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE UndecidableInstances #-}

data Circle'
data Square'

data family ShapeData s

newtype instance ShapeData Circle' = DCircle Float
    deriving (Eq, Show)
data instance ShapeData Square' = DSquare Float String
    deriving (Eq, Show)

data family TShape s

data instance TShape Circle' = TCircle
data instance TShape Square' = TSquare 

class Transformable a where
    setAngle' :: Float -> a -> IO ()

instance Transformable (TShape Circle') where 
    setAngle' _ _ = putStrLn ("Setting a circle angle is a no-op")

instance Transformable (TShape Square') where 
    setAngle' x _ = putStrLn ("Setting the square angle to " ++ show x)

data Shape a = Shape (ShapeData a) (TShape a)

instance Transformable (TShape a) => Transformable (Shape a) where 
    setAngle' x (Shape _ t) = setAngle' x t

Additional remarks:

  • In addition to data families, there are also type families, which result in preexisting types rather than newly introduced ones. Since here we would have to define TCircleShape, TSquareShape etc. separately, we might as well do that through a data family.

  • I replaced your Shape constructors with empty data types, which are then used to fill in the gaps of a now parametric Shape type. One significant difference in relation to your sum type approach is that the set of possible shapes is now open to extension. If you needed it to be closed, I believe it would be possible by reaching to something even fancier: singletons -- you would define a sum type data Shape' = Circle' | Square', then use e.g. the singletons machinery to promote the constructors to type level and using the resulting types as parameters to Shape.