Okasaki describes persistent real-time queues which can be realized in Haskell using the type
data Queue a = forall x . Queue
{ front :: [a]
, rear :: [a]
, schedule :: [x]
}
where incremental rotations maintain the invariant
length schedule = length front - length rear
More details
If you're familiar with the queues involved, you can skip this section.
The rotation function looks like
rotate :: [a] -> [a] -> [a] -> [a]
rotate [] (y : _) a = y : a
rotate (x : xs) (y : ys) a =
x : rotate xs ys (y : a)
and it's called by a smart constructor
exec :: [a] -> [a] -> [x] -> Queue a
exec f r (_ : s) = Queue f r s
exec f r [] = Queue f' [] f' where
f' = rotate f r []
after each queue operation. The smart constructor is always called when length s = length f - length r + 1
, ensuring that the pattern match in rotate
will succeed.
The problem
I hate partial functions! I'd love to find a way to express the structural invariant in the types. The usual dependent vector seems a likely choice:
data Nat = Z | S Nat
data Vec n a where
Nil :: Vec 'Z a
Cons :: a -> Vec n a -> Vec ('S n) a
and then (perhaps)
data Queue a = forall x rl sl . Queue
{ front :: Vec (sl :+ rl) a
, rear :: Vec rl a
, schedule :: Vec sl x
}
The trouble is that I haven't been able to figure out how to juggle the types. It seems extremely likely that some amount of unsafeCoerce
will be needed to make this efficient. However, I haven't been able to come up with an approach that's even vaguely manageable. Is it possible to do this nicely in Haskell?
The other answer is super clever (please take a moment to upvote it), but as someone not familiar with Agda, how this would be implemented in Haskell was not obvious to me. Here's a full Haskell version. We'll need a whole slew of extensions, as well as
Data.Type.Equality
(since we will need to do some limited amount of type-proofs).Defining
Nat
,Vec
, andQueue
Next, we define the usual type-level natural numbers (this looks like just a regular
data
definition, but because we haveTypeInType
enabled, it will get automatically promoted when we use it in a type) and a type function (atype family
) for addition. Note that although there are multiple ways of defining+
, our choice here will impact what follows. We'll also define the usualVec
which is very much like a list except that it encodes its length in the phantom typen
. With that, we can go ahead and define the type of our queue.Defining
rotate
Now, things start to get hairier. We want to define a function
rotate
that has typerotate :: Vec a n -> Vec a (S n + m) -> Vec a m -> Vec a (S n + m)
, but you quickly run into a variety of proof related problems with just defining this recursively. The solution is instead to define a slightly more generalgrotate
, which can be defined recursively, and for whichrotate
is a special case.The point of
Bump
is to circumvent the fact that there is no such thing as type level composition in Haskell. There is no way of writing things an operator like(∘)
such that(S ∘ S) x
isS (S x)
. The workaround is to continuously wrap/unwrap withBump
/lower
.We need explicit
forall
s here to make it very clear which type variables are getting captured and which aren't, as well as to denote higher-rank types.Singleton natural numbers
SNat
Before we proceed to
exec
, we set up some machinery that will allow us to prove some type-level arithmetic claims (which we need to getexec
to typecheck). We start by making anSNat
type (which is a singleton type corresponding toNat
).SNat
reflects its value in a phantom type variable.We can then make a couple useful functions to do things with
SNat
.Finally, we are prepared to prove some arithmetic, namely that
n + S m ~ S (n + m)
andn + Z ~ n
.Defining
exec
Now that we have
rotate
, we can defineexec
. This definition looks almost identical to the one in the question (with lists), except annotated withgcastWith <some-proof>
.It is probably worth noting that we can get some stuff for free by using
singletons
. With the right extensions enabled, the following more readable codedefines,
Nat
, the type family:+
(equivalent to my+
), and the singleton typeSNat
(with constructorsSZ
andSS
equivalent to mySZero
andSSucc
) all in one.Here is what I got:
rotate
is defined in terms ofgrotate
for the same reasonreverse
is defined in terms offoldl
(orenumerate
in terms ofgenumerate
): becauseVec A (suc n + m)
is not definitionallyVec A (n + suc m)
, while(B ∘ suc) m
is definitionallyB (suc m)
.exec
has the same implementation as you provided (modulo thosesubst
s), but I'm not sure about the types: is it OK thatr
must be non-empty?