Currently wumpus-extra has a doodle that doesn't even try to enforce row size, its just about useful as it tracks a coordinate space with a state monad - each new node gets attributed with its coordinate position from the current state and 1 gets added to the column number. The row command resets the column count and adds 1 to the row number.
Here's one with type level naturals measuring a snoc list that does enforce row width. Currently its via an Identity monad with an extra shape parameter. Hopefully it will extend to a state monad to track coordinates:
{-# LANGUAGE EmptyDataDecls #-}
module GridTemp03 where
data Z
data S a
newtype Id sh a = Id a
runId :: Id sh a -> a
runId (Id a) = a
instance Monad (Id sh) where
return a = Id a
Id a >>= f = Id $ (runId . f) a
nil :: Id Z ()
nil = Id ()
blank :: Id sh a -> Id (S sh) a
blank (Id acc) = Id acc
node :: Int -> Id sh a -> Id (S sh) (a,Int)
node i (Id acc) = Id (acc,i)
infixl 5 &
(&) :: Id sh a -> (Id sh a -> Id (S sh) b) -> Id (S sh) b
tl & h = h tl
elem0 :: Id sh () -> Id sh ()
elem0 (Id ()) = Id ()
elem1 :: Id sh ((),a) -> Id sh a
elem1 (Id ((),a)) = Id a
elem2 :: Id sh (((),a),b) -> Id sh (a,b)
elem2 (Id (((),a),b)) = Id (a,b)
elem3 :: Id sh ((((),a),b),c) -> Id sh (a,b,c)
elem3 (Id ((((),a),b),c)) = Id (a,b,c)
-- Rows must have the same width and the number of
-- nodes in the row content must match the respective
-- elemN function.
--
demo1 = runId $ do
r1 <- elem3 $ nil & node 1 & blank & node 2 & node 3
r2 <- elem0 $ nil & blank & blank & blank & blank
return (r1,r2)