You get a way of understanding your problem domain in terms of types and type classes. Here is what I can gather from looking at V3 and its instances, without thinking about what the type is used to represent (3D vector).
type V3 :: Type -> Type
data V3 a = V3 a a a
It's a simple type. And non-commital. There is no restriction on what a can be at all. This is where its power comes from, Edward Kmett has a classic talk on these 'dumb reusable datatypes'.
V3 has basic properties. This one deriving clause probably gives you over 20 functions to operate on V3. Foldable amounts to defining toList (V3 a b c) = [a, b, c].
deriving stock (Functor, Foldable, Traversable)
I focus on type constructor classes. There are many type classes that we can derive automatically but those instances say less about the structure of V3. They are the opposite indicator, if a type doesn't derive Eq then it may because one cannot be defined or because it uses a non-standard definition which can be noteworthy.
deriving stock (Eq, Ord, Show, Read, Data, Generic, Generic1, Lift)
Then I see it is Representable, this makes sense because a representable indicates a sort of "static shape". Actually representable means V3 a is (isomorphic to) a function!
type Index3 :: Type
data Index3 = O | I | II
instance Representable V3 where
type Rep V3 = Index3
index :: V3 a -> (Index3 -> a)
index (V3 a b c) = \case
O -> a
I -> b
II -> c
tabulate :: (Index3 -> a) -> V3 a
tabulate make = V3 (make O) (make I) (make II)
Instead of writing V3 1.0 2.0 3.0 you can write tabulate vec:
vec :: Index3 -> Double
vec = \case
O -> 1.0
I -> 2.0
II -> 3.0
Showing that V3 is (naturally) isomorphic to (Index3 ->) (= Representable) is significant. We are well familiar with functions and we can inherit any function instance by witnessing the isomorphism.
This rule is packaged by Co and you can list all possible instances V3 can give us through its function representation (:instances Co V3)
> :instances Co V3
instance Applicative (Co V3)
instance Functor (Co V3)
instance Monad (Co V3)
instance Apply (Co V3)
instance Bind (Co V3)
instance Distributive (Co V3)
instance Representable (Co V3)
So we can derive Applicative and Monad at least from that, if Index3 is a Monoid we can define a Comonad
deriving (Applicative, Monad)
via Co V3
We also see a lot of instances that are lifted versions of their own algebra.
instance Num a => Num (V3 a) where
(+) = liftA2 (+)
(-) = liftA2 (-)
(*) = liftA2 (*)
negate = fmap negate
abs = fmap abs
signum = fmap signum
fromInteger = pure . fromInteger
instance Semigroup a => Semigroup (V3 a) where
(<>) = liftA2 (<>)
instance Monoid a => Monoid (V3 a) where
mempty = pure mempty
This can be summarized with Ap V3 a which allows these operations over any Applicative.
deriving (Num, Semigroup, Monoid)
via Ap V3 a
I have characterised this simple type thoroughly, even if I had never seen this datatype before it would be very clear to me. The instances (and no-instances) give it character and deriving is like an executive summary that tells you what's going on at exactly the right level of detail.
When you have defined V3 you can now define a TicTacToe board via the composition of two vectors which defines TicTacToe as isomorphic to functions from (Index3, Index3). We can build an empty board with pure Nothing :: TicTacToe (Maybe Player) and index into a board with (Index3, Index3).
type TicTacToe :: Type -> Type
newtype TicTacToe a = TicTacToe (V3 (V3 a))
deriving (Functor, Foldable, Applicative, Representable)
via Compose V3 V3
deriving (Monad)
via Co TicTacToe
deriving (Num, Semigroup, Monoid)
via Ap TicTacToe a
-- > index board (O, O)
-- Nothing
-- > index board (O, II)
-- Just Player2
board :: TicTacToe (Maybe Player)
board = TicTacToe do
V3 do V3 Nothing Nothing (Just Player2)
do V3 Nothing Nothing Nothing
do V3 Nothing Nothing (Just Player1)
And the whole story repeats itself, TicTacToe is now a Monad, numbers and monoid operations can be lifted over it.