Cyclic Types

As of March 2020, School of Haskell has been switched to read-only mode.

This is an attempt to capture a concept in Haskell that I have been trying to do for a while. Some of the new extensions and type-level hackery seem to help, though I suspect I am not taking advantage of everything I could. Let me know if this can be simplified or if there is a better way.

Functioning code is at the very bottom.

Type-Cycles

Some datatypes we commonly use are recursive or mutually recursive, leading to what I call a type-cyclic structure where it is possible to traverse back to the original datatype.

This has a type-cycle of 2, even though the data actually is in a cycle of 4:

data A = A Int B
data B = B String A
exampleAB = A 1 $ B "one" $ A 2 $ B "two" exampleAB 

This has a type-cycle of 1 when using drop 1:

data List a = Cons a (List a) | Nil

A tree like this has a type-cycle of 1 as you traverse it manually or with a zipper:

data Tree a = Node a (Tree a) (Tree a) | Leaf a | TNil

Last example inspired by Data.Graph using Data.Array. This has a type-cycle of 1.

type Graph = Table [Int]
type Table a = Array Int a
nextItems :: Graph -> Int -> [Int]
nextItems = (!)

nextUnsafe :: Graph -> Int -> Int
nextUnsafe g = head.(nextItems g)

Repeated applications of nextUnsafe g will traverse the graph one-by-one. Though one can imagine a slower traversal. Taking only a single result (unsafe) when multiple or none are possible leads to a type-cycle of 2 when traversing using outEdge and outNode. This is similar to the concept of using a bipartite double cover to represent a directed graph.

nextEdge :: Graph -> Int -> (Int,Int)
nextEdge g i = (i, head $ g!i)

nextNode :: (Int,Int) -> Int
nextNode = snd

Abstract away

This notion is pretty general and can be captured with the idea that they are categories with n-objects and the traversals are arrows between them. The category diagram can be represented with a multipartite graph. Zippers have abstracted the notion of traversal; we can use the same polymorphic functions to traverse zippers. I wanted to see if I can make the Haskell type system enforce the notion that a type was cyclic.

Attempt 1 - find cycles

class Cycle1 a where
    next :: a -> a

Not very promising.

class Cycle2 a where
    type Next a
    next :: Cycle2 (Next a) => a -> Next a

This is closer, but it doesn't ensure that the cycle returns to the original datatype. Starting at a you may go to b which then loops along itself. Not satisfying. It seems we need a way to identify a cycle at the type-level; enter type families:

data Cycle (n::Nat)
type Counter a = Count (Next a) a 1
type family Count a b n where
    Count a b 20 = Void     -- Used to terminate the type-checker
    Count b b n = Cycle n
    Count a b n = Count (Next a) b (n+1)

This seems to have the right capabilities. We can't use a constraint (or can we? let me know) like class (Counter a ~ Cycle n) => Cycle a where because we need to have n in the class head. Ideally it would not be manually entered by instance writers. With a little type in-equality help:

type a /=/ b = (a == b) ~ False

class (Counter a /=/ Void) => Cycle3 a where
    type Next a
    next :: Cycle3 (Next a) => a -> Next a

Success! This ensures that a type-cycle exists and is of a set length n.

Attempt 2 - traverse faster

At the moment next merely traverses the structure one-by-one. That is fine for 1-cycles, but how can we do a -> a hops for 2-cycles, 3-cycles, etc? I could not think of a way to express this properly:

data Cycle (n::Nat) = Proxy
class (Cycle3 a, Counter a ~ Cycle n) => CycleN a n
    neighbor :: Count n -> a -> a
    neighbor n a = foldr ($) a $ replicate (natVal n) next

So instead we I tried a next typeclass:

data Cycle (n::Nat) {-hi-}a {-/hi-}
type Counter a = Count (Next a) a 1
type family Count a b n where
    Count b b n = Cycle n {-hi-}b {-/hi-}
    Count a b 3 = Void
    Count a b n = Count (Next a) b (n+1)
type CyclicN a r n = (Cyclic a
                    ,Cyclic (Next a)
                    ,Cyclic (Next (Next a))
                    , {-hi-}Counter a ~ Cycle n a {-/hi-},a~r)
class (CyclicN a r n) => Cyc a r n where
    neighbor :: {-hi-}a -> a{-/hi-}

instance (a ~ Next (Next (Next a))
        ,CyclicN a (Next (Next (Next a))) 3) => Cyc a a 3 where 
    neighbor = next . next . next 
instance (a ~ Next (Next a)
        ,CyclicN a (Next (Next a)) 2)        => Cyc a a 2 where 
    neighbor = next . next 
instance (a~Next a
        , CyclicN a (Next a) 1)              => Cyc a a 1 where 
    neighbor = next

This is an improvement. Now we have the capablity to call next the proper number of times to produce the right result to hop through our structure.

Attempt 3 - Functors and Monads, oh my!

Most of these structures are not single-linked, sometimes the natural result is a list, set, Maybe, or anything else. Our function should not be a -> a, but more like Functor => a -> f a and if it wasn't ugly already; we have to add structure to our Counter and even more Constraints:

data Cycle (n::Nat) b {-hi-}r {-/hi-}
type Counter a = Count (Next a) a 1 {-hi-}((F a) a){-/hi-}
type family Count a b n r where
    Count b b n r = Cycle n b r
    Count a b 3 r = Void
    Count a b n r = Count (Next a) b (n+1) ((F a) r)

class (Counter a /=/ Void, {-hi-}Functor (F a){-/hi-}) => Cyclic a where
    type Next a
    {-hi-}type F a :: * -> * {-/hi-}
    {-hi-}nextSet :: (Cyclic (Next a)) => a -> F a (Next a){-/hi-}
    step :: (Cyclic (Next a)) => a -> Next a

type CyclicN a b n r = (Monad (F a),Cyclic a
                    ,Cyclic (Next a)
                    ,Cyclic (Next (Next a))
                    ,Counter a ~ Cycle n b r, b~a)
                    
class (CyclicN a b n r) => Cyc a b n r where
    neighbors :: a -> r

instance (CyclicN a e 3 b,b~F a (F c (F d a)),c~Next a,d~Next c,e~Next d) => Cyc a a 3 b where 
    neighbors = fmap (fmap nextSet . nextSet) . nextSet
instance (CyclicN a d 2 b,{-hi-}b~F a (F c a){-/hi-},c~Next a,d~Next c) => Cyc a a 2 b where 
    {-hi-}neighbors = fmap nextSet . nextSet{-/hi-}
instance (CyclicN a c 1 b,b~F a a,c~Next a) => Cyc a a 1 b where 
    neighbors = nextSet

Now I'm getting the hang of type-level computation, but it is very ugly. Any way to clean this up? Wait, we're not done yet!

I don't like having results like Just (Just (Just 2)). Let's smash them together whenever possible:

data Cycle (n::Nat) b (x :: * -> *) xr (f::Bool)
type Counter a = Count (Next a) a 1 ((F a) a) (F a == F (Next a))
type family Count a b n r f where
    Count b b n (x r) f     = Cycle n b x (x r) f
    Count a b 3 r f         = Void
    Count a b n (x r) True  = Count (Next a) b (n+1) (x r) (F a == F (Next a))
    Count a b n (x r) False = Count (Next a) b (n+1) (x ((F a) r)) False

and

class (CyclicN a b n x xr f ) => Cyc a b n x xr f where
    neighbors :: a -> xr
    
instance (CyclicN a e 3 x xr True, Monad x
            ,xr~F a a,F a ~ F c,F c ~ F d,c~Next a,d~Next c,e~Next d) => Cyc a a 3 x xr True where 
    neighbors = nextSet >=> nextSet >=> nextSet

instance (CyclicN a d 2 x xr True ,Monad x
            ,xr~F a a,F a ~ F c ,c~Next a,d~Next c) => Cyc a a 2 x xr True where 
    neighbors =  nextSet >=> nextSet

Now calling neighbors smashes together any duplicate Monads in a join-like fasion.

Conclusion

The type system can do quite a bit and I'm almost certain I'm doing it wrong. How can this be simpler?

Functioning Code

{-# LANGUAGE TypeFamilies
    ,DataKinds
    ,TypeOperators
    ,ConstraintKinds
    ,GADTs
    ,UndecidableInstances
    ,FlexibleContexts
    ,MultiParamTypeClasses
    ,FlexibleInstances
    ,GeneralizedNewtypeDeriving
    ,StandaloneDeriving
    #-}
import GHC.TypeLits
import Data.Type.Equality
import Data.Word
import Data.Void
import Data.Functor.Identity
import Control.Monad ((>=>))
import qualified Data.Graph as G
import Data.Array
deriving instance Show a => Show (Identity a)

main :: IO ()
main = do 
    print $ "example:                     : " ++ show example
    print $ "step        (Node 2 example) : " ++ show (step (Node 2 example))
    print $ "step $ step (Node 2 example) : " ++ show (step $ step (Node 2 example))
    print $ "neighbors   (Node 2 example) : " ++ show (neighbors (Node 2 example))
    print $ "step     (DirNode 2 example) : " ++ show (step (DirNode 2 example))
    print $ "neighbors(DirNode 1 example) : " ++ show (neighbors (DirNode 1 example))
    print $ "step              (2 :: Int) : " ++ show (step (2 :: Int))
    print $ "step $ step       (2 :: Int) : " ++ show (step $ step (2 :: Int))
    print $ "neighbors         (2 :: Int) : " ++ show (neighbors (2 :: Int))
    print $ "neighbors   (2,exampleGraph) : " ++ show (neighbors (2::Int,exampleGraph))

exampleGraph = G.buildG (0,2) [(0,1),(1,2),(2,0),(2,1)]

type DList = [(Int,[Int])]
example :: DList
example = [(0,[1]),(1,[2]),(2,[0,1])]

data Node = Node Int DList
instance Show Node where
    show (Node x _) = "Node " ++ show x ++ " example"
data DirNode = DirNode Int DList
instance Show DirNode where
    show (DirNode x _) = "DirNode " ++ show x ++ " example"
data Edge = Edge (Int,Int) DList
instance Show Edge where
    show (Edge x _) = "Edge " ++ show x ++ " example"

newtype L a = L [a] deriving (Show,Functor,Monad) -- No type-level lamdas! gar!
instance Cyclic Node where
    type Next Node = Node
    type F Node = L
    step (Node x xs) = Node (fst (xs !! head (snd (xs !! x)))) xs
    nextSet (Node x xs) = L $ map (\y -> Node y xs) (snd $ xs !! x)

instance Cyclic DirNode where
    type Next DirNode = Edge
    type F DirNode = L
    step (DirNode x xs) = Edge (x,fst (xs !! head (snd (xs !! x)))) xs
    nextSet (DirNode x xs) = L $ map (\y -> Edge (x,y) xs) (snd (xs !! x))
instance Cyclic Edge where
    type Next Edge = DirNode
    type F Edge = Identity
    step (Edge x xs) = DirNode (snd x) xs
    nextSet (Edge x xs) = Identity $ DirNode (snd x) xs
-- This instance has a cycle, but not one that returns to the original instance
{-
data BadInstance = BadInstance (Int,Int) GList deriving Show
instance Cyclic BadInstance where
    type Next BadInstance = DirNode
    type F BadInstance = Identity
    step (BadInstance x xs) = DirNode (fst x) xs 
    nextSet = Identity . step
---}
instance Cyclic Int where
    type Next Int = Integer
    type F Int = Identity
    step x = 2 * fromIntegral x
    nextSet = Identity . step
instance Cyclic Integer where
    type Next Integer = Word32
    type F Integer = Identity
    nextSet = Identity . step
    step x = 2 * fromIntegral x
instance Cyclic Word32 where
    type Next Word32 = Int
    type F Word32 = Identity
    step x = 2 * fromIntegral x
    nextSet = Identity . step

instance Cyclic [a] where
    type Next [a] = [a]
    type F [a] = Maybe
    step = tail
    nextSet (_:x:xs) = Just (x:xs)
    nextSet _ = Nothing

data Tree a = TNode a (Tree a) (Tree a) | Leaf a | Nil
instance Cyclic (Tree a) where
    type Next (Tree a) = Tree a
    type F (Tree a) = L
    step (TNode _ a _) = a  -- Can't use Forest without fully applying it! gar!
    step _ = Nil
    nextSet (TNode _ xs ys) = L [xs,ys]
    nextSet _ = L []

instance Cyclic (G.Vertex,G.Graph) where
    type Next (G.Vertex,G.Graph) = (G.Edge,G.Graph)
    type F (G.Vertex,G.Graph) = L
    step (v,g) = ( (v,head $ g!v) ,g)
    nextSet (v,g) = L $ map (\x -> ( (v,x),g)) $ g!v

instance Cyclic (G.Edge,G.Graph) where
    type Next (G.Edge,G.Graph) = (G.Vertex,G.Graph)
    type F (G.Edge,G.Graph) = L
    step ((_,v),g) = (v,g)
    nextSet ((_,v),g) = L [(v,g)]
    
-- show
data Cycle (n::Nat) b (x :: * -> *) xr (f::Bool)
type Counter a = Count (Next a) a 1 ((F a) a) (F a == F (Next a))
type family Count a b n r f where
    Count b b n (x r) f     = Cycle n b x (x r) f
    Count a b 3 r f         = Void
    Count a b n (x r) True  = Count (Next a) b (n+1) (x r) (F a == F (Next a))
    Count a b n (x r) False = Count (Next a) b (n+1) (x ((F a) r)) False

class (Counter a /=/ Void, Functor (F a),Monad (F a)) => Cyclic a where
    type Next a
    type F a :: * -> *
    nextSet :: (Cyclic (Next a)) => a -> F a (Next a)
    step :: (Cyclic (Next a)) => a -> Next a

class (Counter w /=/ Void, Functor (F2 w),Monad (F w)) => Cyclic2 w where
    type Part w
    type F2 w :: * -> *

type CyclicN a b n x xr f= (Monad (F a),Cyclic a
                    ,Cyclic (Next a)
                    ,Cyclic (Next (Next a))
                    ,Counter a ~ Cycle n b x xr f, b~a)
class (CyclicN a b n x xr f ) => Cyc a b n x xr f where
    neighbors :: a -> xr
instance (CyclicN a e 3 x xr True, Monad x
            ,xr~F a a,F a ~ F c,F c ~ F d,c~Next a,d~Next c,e~Next d) => Cyc a a 3 x xr True where 
    neighbors = nextSet >=> nextSet >=> nextSet
instance (CyclicN a e 3 x xr False
            ,xr~F a (F c (F d a)),c~Next a,d~Next c,e~Next d) => Cyc a a 3 x xr False where 
    neighbors = fmap (fmap nextSet . nextSet) . nextSet

instance (CyclicN a d 2 x xr True ,Monad x
            ,xr~F a a,F a ~ F c ,c~Next a,d~Next c) => Cyc a a 2 x xr True where 
    neighbors =  nextSet >=> nextSet
instance (CyclicN a d 2 x xr False
            ,xr~F a (F c a)      ,c~Next a,d~Next c) => Cyc a a 2 x xr False where 
    neighbors = fmap nextSet . nextSet

instance (CyclicN a c 1 x xr f
            ,xr~F a a,c~Next a) => Cyc a a 1 x xr f where 
    neighbors = nextSet

type a /=/ b = (a == b) ~ False
-- /show
comments powered by Disqus