Skip to content

Add Queue datatype - #3084

Draft
silas-hw wants to merge 54 commits into
agda:masterfrom
silas-hw:queue
Draft

Add Queue datatype#3084
silas-hw wants to merge 54 commits into
agda:masterfrom
silas-hw:queue

Conversation

@silas-hw

@silas-hw silas-hw commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Will eventually resolve #3083. Uses the two-list method mentioned in #3072 (but in a PR not generated by an LLM this time).

There may be some poor stylistic choices due to my unfamiliarity with the standard library, for which I am sorry! There are some 'low hanging' basic operations that could still be added, but I think those would be good for separate PRs.

@silas-hw
silas-hw marked this pull request as draft July 20, 2026 21:29
@silas-hw
silas-hw marked this pull request as ready for review July 20, 2026 21:32
@JacquesCarette

Copy link
Copy Markdown
Collaborator

When adding new functionality, we tend to make somewhat larger PRs than when fixing things. In particular, a bunch of properties should be proved too. The main reason is that quite often the proofs reveal when some of functionality was written in a correct-but-hard-to-use (or reason about) manner. So basically all of #3083 will need to be done in this PR.

On the other hand, asking for reviews of the intermediate steps is a good idea.

Comment thread src/Data/Queue/Base.agda Outdated
Comment thread src/Data/Queue/Base.agda Outdated
Comment thread src/Data/Queue/Base.agda Outdated
Comment thread src/Data/Queue/Base.agda Outdated
Comment thread src/Data/Queue/Base.agda Outdated
Comment thread src/Data/Queue/Base.agda Outdated
Comment thread src/Data/Queue/Base.agda Outdated
Comment thread src/Data/Queue/TwoList/Base.agda Outdated
Comment thread src/Data/Queue/Base.agda Outdated
toList empty = []
toList (queue dq-hd dq-tail eq) = dq-hd ∷ (dq-tail ++ (reverse eq))

-- Create a Queue from a List, such that the elements

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably consider fromSnocList too.

@jamesmckinna jamesmckinna left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some alternative suggestions as to implementation.

Against @JacquesCarette , and because I'm a bit more 'OO-minded', I tend to put things like Empty/isEmpty as manifest fields of the record, because then I don't have to think about the scope management. Similarly toList, because all the pieces are at-hand without further ado... but YMMV.

As for references for this kind of implementation, probably Okasaki's "Purely functional data structures" would be the go-to citation?

Oh, and: fix-whitespace!

Comment thread src/Data/Queue/Base.agda Outdated
Comment thread src/Data/Queue/Base.agda Outdated
Comment thread src/Data/Queue/Base.agda Outdated
@gallais

gallais commented Jul 21, 2026

Copy link
Copy Markdown
Member

As for references for this kind of implementation, probably Okasaki's "Purely functional data structures" would be the go-to citation?

There is also https://doi.org/10.1017/S0956796800001489

@jamesmckinna

Copy link
Copy Markdown
Collaborator

When adding new functionality, we tend to make somewhat larger PRs than when fixing things. In particular, a bunch of properties should be proved too. The main reason is that quite often the proofs reveal when some of functionality was written in a correct-but-hard-to-use (or reason about) manner. So basically all of #3083 will need to be done in this PR.

On the other hand, asking for reviews of the intermediate steps is a good idea.

So, eg.:

toList-fromList : toList (fromList xs) ≡ xs

The converse direction requires some simulation relation on Queues... but that's (probably) worth discussing/thinking about a bit more... although the toList observer of 'internal state' is already one kind of characteristic...

@silas-hw
silas-hw marked this pull request as draft July 21, 2026 14:03
Comment thread src/Data/Queue/Base.agda Outdated
Comment thread src/Data/Queue/Base.agda Outdated
Comment thread src/Data/Queue/Base.agda Outdated
Comment thread src/Data/Queue/Base.agda Outdated
silas-hw and others added 5 commits July 22, 2026 11:18
Co-authored-by: jamesmckinna <31931406+jamesmckinna@users.noreply.github.com>
Co-authored-by: jamesmckinna <31931406+jamesmckinna@users.noreply.github.com>
@gallais

gallais commented Jul 27, 2026

Copy link
Copy Markdown
Member

Isn't _≈_ defined inductively equivalent to _≡_?

@silas-hw

silas-hw commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

Isn't _≈_ defined inductively equivalent to _≡_?

Yeah, it is...

How else would you go about defining it? Or is it good to keep this alongside a proof of logical equivalence?

Edit: I see where I'm going wrong! It's not observational at all...

@jamesmckinna

Copy link
Copy Markdown
Collaborator
* ... permitting the following [ DRY ] simplifications
  ```agda
  queue : List A → List A → Queue A
  queue []         ys = fromList (reverse ys)
  queue xs@(_ ∷ _) ys = mkQ xs ys null-∷

fromList uses the queue smart constructor! In a way which fails the termination checker as well (at least currently)...

Sorry! I'd in the meantime refactored my own experiment to have

fromList : List A  Queue A
fromList xs = mkQ xs [] (const [])

which is obviously uncoupled from queue (and indeed, that is the point!).

Comment thread src/Data/Queue/QueueSpec.agda Outdated
Comment on lines +71 to +72
empty-toList : toList (empty {A = A}) ≡ []
fromList-empty : empty {A = A} ≈ fromList []

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this... I'd needed some specimen fields, but it occurs to me that we should only add IsQueue once we actually agree what properties we want!

@jamesmckinna jamesmckinna Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Eg the above two properties, I think, follow from:

record IsQueue {Q : Set a  Set a} (rawQ : RawQueue Q) : Set (suc a) where

  open RawQueue rawQ

  field
    isEquivalence   : IsEquivalence (_≈_ {A = A})
    ≈-resp-Empty    : Empty Respects (_≈_ {A = A})
    ≈-=[toList]⇒-≡  : _≈_ {A = A} =[ toList ]⇒ _≡_
    empty-toList    :  {q : Q A}  Empty q  Null (toList q)
    empty-fromList  : Null {A = A} xs  Empty (fromList xs)
    toList-fromList :  {q : Q A}  q ≈ fromList xs  toList q ≡ xs
    fromList-toList :  {q : Q A}  xs ≡ toList q  fromList xs ≈ q
    toList-enqueue  :  {q : Q A}  toList (enqueue x q) ≡ toList q List.∷ʳ x
    toList-dequeue  :  {q : Q A}  .{{i : False (empty? q)}} 
                      let x , r = dequeue q {{i}} in toList q ≡ x ∷ toList r

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think enqueue-suc is good to add as well. And maybe something like

empty-enqueue-dequeue :  {q : Q A} {x : A}  Empty q  proj₁ (dequeue (enqueue x q)) ≡ x

And just generally some more properties on the behaviour of enqueue and dequeue

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given toList-enqueue and toList-dequeue, I think _≈_ can be defined generically as

_≈_ :  {A : Set a}  Rel (Q A) a
q ≈ q' = (toList q) ≡ (toList q')

Which is how it is currently defined in TwoList.Base!

But would this mean moving _≈_ to IsQueue? which feels like it somewhat betrays the distinction between RawQueue and IsQueue...

@jamesmckinna jamesmckinna Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To maintain 'abstraction', it seemed that simply requiring an implication in the spec was preferably to insisting this be definitional... the fact that the proof of that property is then trivial for TwoList.Base is a bonus...?

Ah.. you think it's definable as _≈_ = _≡_ on toList (see Function.Base._on_) given the toList characterisations? Could be!?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think enqueue-suc is good to add as well. And maybe something like

empty-enqueue-dequeue : ∀ {q : Q A} {x : A} → Empty q → proj₁ (dequeue (enqueue x q)) ≡ x

And just generally some more properties on the behaviour of enqueue and dequeue

Hmmm... there is a whole thing here, and I'd like @JacquesCarette and @MatthewDaggitt to weigh in on this, as it's yet another twist on the 'narrow' vs. 'wide' signature for a given 'algebraic specification'. The enqueue-suc property might seems good to have, but it is in any case derivable once you have toList-enqueue, isn't it?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See also @MatthewDaggitt 's comment on #1552 ... it seems my instinct towards minimality/definitional extension may be at odds with usability. So a design more akin to that introduced as Effect.Foldable.RawFoldable{WithDefaults} might be more suitable?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the general idea of Effect.Foldable.RawFoldable{WithDefaults} a lot, but wonder if there's an approach that doesn't require two records?

Obviously, native 'defaults' would be nice, but in is place, maybe a function/constructor that could take an implementation of the minimal interface and fill in the 'defaults'?

For lemmas/proofs, maybe there's also room to consider what should just be generic properties of Queues and not part of the interface, such that they would be placed in say Queue.Properties.

All open to discussion!

Comment thread src/Data/Queue/TwoList/Base.agda Outdated
... | [] = queue (x ∷ []) []
... | front@(_ ∷ _) = queue front (x ∷ bs)

dequeue : ∀ (q : Queue A) .{{_ : False (empty? q)}} → A × Queue A

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this will be ergonomic to use as opposed to the
Maybe-returning one.

Note that the List equivalents are all Maybe-based:
https://agda.github.io/agda-stdlib/master/Data.List.Base.html#8038

I'm not against having a dequeue-style variant that always succeeds
by constraining its input but in that case it should be suffixed IMO. We
may also want to go for full-blown size-indexed queues in that case.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm torn between the two directions myself; I don't have enough 'proof intuition' to know which is best! It's an easy change between the two for the most part, so I'll let other maintainers discuss :-)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, in a way, the definition of dequeue′ as a derived form shows the clunkiness, so in that sense I agree.

I'd understood from @Taneb 's comments above that this was an opportunity for something other than the pure-haskell-ish style, and of course that style is a major influence on the 'core' of stdlib. And yes: on whatever types things should have, in the spec or in any candidate implementation, we should reach a reasonable consensus! But nothing stops us from having the Maybe style...

What name do you think the 'guarded' version should have?

@jamesmckinna jamesmckinna Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, and regarding the ergonomics (I've always been a bit shaky on True/False... ;-)), we can simplify this a bit to

dequeue : Q A  Maybe (A × Q A)
dequeue q with isEmpty q in eq
... | true  = nothing
... | false = just (dequeue-guarded q)
  where instance
  _ : T (not (isEmpty q))
  _ rewrite eq = _

for the 'obvious' renamings of everything that has gone before, and unfolding False...

Co-authored-by: jamesmckinna <31931406+jamesmckinna@users.noreply.github.com>
Comment thread src/Data/Queue.agda

{-# OPTIONS --without-K --safe #-}

module Data.Queue where

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this should itself be Data.Queue.TwoList as the top-level (sic) module for this implementation?
Again, up for discussion!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As in have a file Data/Queue/TwoList.agda with this content?

Indeed, this file itself will be changed to not just have TwoList.Base imported, and will then likely import Data,Queue.TwoList instead?

Comment thread src/Data/Queue/TwoList/Base.agda
Comment thread src/Data/Queue/TwoList/Base.agda
Comment thread src/Data/Queue/QueueSpec.agda Outdated
Comment on lines +52 to +53
to𝔹 : Q A → Bool
to𝔹 = isYes ∘ empty?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly, should this be called null? or isEmpty? or what, in general...

Comment thread src/Data/Queue/QueueSpec.agda Outdated
@silas-hw

Copy link
Copy Markdown
Contributor Author

Note: sorry for pushing a file with holes in!

{!!} ≡⟨⟩
{!!}
++-[] : ∀ {xs ys : List A} → (xs ++ ys) ≡ [] → ys ≡ []
++-[] {xs = []} {ys = []} xs++ys≡[] = xs++ys≡[]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that the equivalent properties in Data.Nat.Properties have more complex names:

m+n≡0⇒m≡0 :  m {n}  m + n ≡ 0  m ≡ 0
m+n≡0⇒n≡0 :  m {n}  m + n ≡ 0  n ≡ 0

I personally would expect ++-[] to be a proof that xs ++ [] ≡ xs

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are two hard things in computer science...

Descriptive names is definitely something I need to get a hold of!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add Queue datatype

6 participants