I'm building a toy where you can draw circuits on a grid and we can simulate their behaviour. I thought it would be a fun experiment to abstract over the dimensionality of the board and try to make the code work (in a type-safe way) over any board dimensions (2D, 3D, 4D, etc).
I can do most of the work with GADTs and Nats; Assuming I'm using a vector as a 2D base abstraction we can represent any dimensionality by composing it;
type family Count t where
Count (Compose _ g) = 1 + (Count g)
Count _ = 0
data Grid (n::Nat) a where
Grid :: f a -> Grid (Count f) a
This works for the most part (unfortunately the type family requires UndecidableInstances)
With this I can express that operations over grids stay consistent in dimensionality, i.e.
alter :: Grid n a -> Grid n b
The tricky bit is that I want to also allow moving around in the grids.
I've written a Representable instance for Grid which relies on the underlying Representable for Compose
, basically you just pair up the representation for each functor being composed. In my case here are some example representations:
Rep (Grid 2) ~ (Sum Int, Sum Int)
Rep (Grid 3) ~ (Sum Int, (Sum Int, Sum Int))
Rep (Grid 3) ~ (Sum Int, (Sum Int, (Sum Int, Sum Int)))
And so on.
Also assume that we can index into a Grid by keeping an index alongside it as a store comonad type IGrid n a = (Rep (Grid n), Grid n a)
I've written a few functions which move around in a certain dimensionality. Conceptually if a function moves the focus on the y-axis, we can still call that function on any dimensionality with at least 2 dimensions:
e.g.
moveUp :: (n >= 2) => IGrid n a -> IGrid n a
This is doable and easy when n==2, but for higher dimensions it's probably easiest to implement by promoting a lower dimensionality index into some higher one (padding unknown dimensional coords with mempty) so that I can use seek :: Rep (Grid n) -> Grid n a -> Grid n a
properly.
promote :: (m <= n) => Rep (Grid m) -> Rep (Grid n)
Then I can just promote a given index to any dim before using it:
moveBy :: Rep (Grid n) -> IGrid n a -> IGrid n a
moveBy m (rep, grid) = (rep <> m, grid)
moveAround :: IGrid n a -> IGrid n a
moveAround grid = grid
& moveBy (promote (Sum 3, Sum 2))
& moveBy (promote (Sum 1))
Most of my attempts have centered around using a typeclass and implementing it over certain Nats and using lots of type assertions. I've been able to promote an index by one or two finite levels, but can't figure out the general case.
I've been trying to write this promote
function for a month or two now, coming back to it from time to time and it seems possible but I just can't figure it out. Any help would be greatly appreciated. Using Nats and the singletons lib is fine if that's the way to do it :)
Thanks for taking the time to read my dilemma!
Measuring the size of your type expression with a
Nat
, and then trying to inject the index for a small grid into the index for a big one, is overkill here. All you really need to do is identify how deep into the nestedCompose
type you want to go before you modify the grid.Under g f
is shaped like a natural number - compareUnder (Under Over)
withS (S Z)
- and it tells you how many layers ofCompose
you have to unpeel fromf
in order to find ag
.You mentioned in a comment that you're working with grids of infinite zippers. It's easier to use
Under
with nestedCompose
s where theZipper
is always the left argument. MyGrid2
is isomorphic to yourCompose Zipper Zipper
.Now, given
move :: Int -> Zipper a -> Zipper a
, you can goUnder
any number ofCompose
constructors tomove
a particular dimension of your grid.For example, to go
up
in a 2D grid, you move all the inner zippers left.Now if you want to move a variety of dimensions of your grid at once you can just call
moveUnder
multiple times. Here I'm putting a sequence ofMove
s in a list. Note that I'm existentially quantifying theg
inMove
's constructor.