As per @AntC's comment, it might be worth rethinking why you need Op as a GADT. However, here's one approach that seems to work...
It's kind of fundamental to Haskell that you can demand an instance Ixed a but not act conditionally depending on whether or not an instance Ixed a exists. So, one way or another, you're going to have to explicitly enumerate all the types a you want to use in this serialization and manually indicate which ones will and will not be treated as Ixed.
Once you've resigned yourself to that, there's a glaringly obvious solution. If you want to support Op a for a ~ Int (not Ixed) and a ~ [Int] (with Ixed), you can define two instances:
instance Serialize (Op Int) where
put (Update a) = putWord8 1 >> put a
put (Reorder ks) = putWord8 2 >> put ks
get = do
i <- getWord8
case i of
1 -> Update <$> get
_ -> error "instance Serialize (Op a) - corrupt data"
instance Serialize (Op [Int]) where
put (Update a) = putWord8 1 >> put a
put (Reorder ks) = putWord8 2 >> put ks
get = do
i <- getWord8
case i of
1 -> Update <$> get
2 -> Reorder <$> get
_ -> error "instance Serialize (Op a) - corrupt data"
and the main problem is solved. The remaining problem is how to make this boilerplate palatable.
Here's one way. We can define a type class to provide a getOp :: Op a operation, equipped with two instances, one for Ixed and one for non-Ixed types. The type class is parametrized in both a data kind Bool for the presence of Ixed and the underlying type, like so:
class OpVal' (hasixed :: Bool) a where
getOp :: Get (Op a)
and the two instances are selected by the hasixed type, which specifies the capabilities of a:
instance (Serialize a) => OpVal' False a where
getOp = do
i <- getWord8
case i of
1 -> Update <$> get
_ -> error "instance Serialize (Op a) - corrupt data"
instance (Ixed a, Serialize (Index a), Serialize a) => OpVal' True a where
getOp = do
i <- getWord8
case i of
1 -> Update <$> get
2 -> Reorder <$> get
_ -> error "instance Serialize (Op a) - corrupt data"
To select the proper instance for a type, we define a type family:
type family HasIxed a :: Bool
which specifies whether or not a type a has Ixed a. Then, we can use another type family to select the correct OpVal' instance based on HasIxed:
type family OpVal a where
OpVal a = OpVal' (HasIxed a) a
Finally, we can define our Serialize (Op a) instance:
instance OpVal a => Serialize (Op a) where
put (Update a) = putWord8 1 >> put a
put (Reorder ks) = putWord8 2 >> put ks
get = getOp @(HasIxed a)
With this in place, you can add types a to the open HasIxed type family:
type instance HasIxed Int = False
type instance HasIxed [Int] = True
and it all just kind of works:
instance OpVal a => Serialize (Op a) where
put (Update a) = putWord8 1 >> put a
put (Reorder ks) = putWord8 2 >> put ks
get = getOp @(HasIxed a)
data BigThing a b = BigThing (Op a) (Op b) deriving (Generic)
instance (OpVal a, OpVal b) => Serialize (BigThing a b)
main = do
let s = runPut $ put (BigThing (Update (5 :: Int)) (Reorder @[Int] [1,2,3]))
Right (BigThing (Update x) (Reorder xs)) = runGet (get :: Get (BigThing Int [Int])) s
print (x, xs)
The full example:
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}
import GHC.Generics (Generic)
import Control.Lens (Index, Ixed)
import Data.Serialize
data Op a where
Update :: Serialize a => a -> Op a
Reorder :: (Ixed a, Serialize (Index a)) => [Index a] -> Op a
class OpVal' (hasixed :: Bool) a where
getOp :: Get (Op a)
instance (Serialize a) => OpVal' False a where
getOp = do
i <- getWord8
case i of
1 -> Update <$> get
_ -> error "instance Serialize (Op a) - corrupt data"
instance (Ixed a, Serialize (Index a), Serialize a) => OpVal' True a where
getOp = do
i <- getWord8
case i of
1 -> Update <$> get
2 -> Reorder <$> get
_ -> error "instance Serialize (Op a) - corrupt data"
type family HasIxed a :: Bool
type instance HasIxed Int = False
type instance HasIxed [Int] = True
type family OpVal a where
OpVal a = OpVal' (HasIxed a) a
instance OpVal a => Serialize (Op a) where
put (Update a) = putWord8 1 >> put a
put (Reorder ks) = putWord8 2 >> put ks
get = getOp @(HasIxed a)
data BigThing a b = BigThing (Op a) (Op b) deriving (Generic)
instance (OpVal a, OpVal b) => Serialize (BigThing a b)
main = do
let s = runPut $ put (BigThing (Update (5 :: Int)) (Reorder @[Int] [1,2,3]))
Right (BigThing (Update x) (Reorder xs)) = runGet (get :: Get (BigThing Int [Int])) s
print (x, xs)