While working on a work project, I found I needed a more Elixir-like queue versus the Erlang :queue module. So I decided to write one. I liked what I did, so I’m sharing it with others.
Yaq (Yet another queue) is a double-ended queue that supports both the Enumerable and Collectable protocols. I wanted something I could use with the pipe operator, since with my use case I found myself having to write a lot of functions to make :queue work correctly.
This is my first library I’m sharing with the community. I not only wanted a library I could use in my day job, but something I could take an opportunity to learn how to “do things right” so with regards to Elixir.
Congratulations on your first library, its a big milestone and will be much appreciated I’m sure. I’ll check it out. @NobbZ is more direct and to the point than I am and his feedback is always very valuable, if sometimes a bit abrupt.
Sorry, in this specific case it makes no sense to me too. I thought you meant as a general rule that nil | term is useless, as term includes nil; but I thought maybe sometimes it is useful to know that the library can expect nil.
Again, in this case I don’t see why this needs to be clarified indeed.
This was a deliberate choice. I can show the size of the
It probably shouldn’t be.
Mostly because I’m still figuring out how typespecs work.
This is probably an artifact of some thinking. The :queue module I drew inspiration from returned an :empty atom if you tried to dequeue an empty list. I guess I didn’t like the specific atom, so I think that’s why it became a nil. My own limited knowledge probably had me explicitly describe this case.
Thinking back, I’m wondering if an atom would be a better empty queue return value.
I did, but because it is mostly wrapper for :quque, it has some of the same issues. :quque (deliberately) does not track the size of the queue, so that is an O(N) operation, which was too slow for my use case.
Oh I haven’t noticed that Yaq is not a wrapper around :queue, that’s interesting. Speaking of efficiency, do you plan to do some benchmarks? Would be nice to see how it performs compared to :queue.
I’ve had this same though about using atoms as markers. It’s a pretty common pattern in Erlang/OTP from what I can see, but the scenario you describe seems likely, especially when using common atoms like :ok (or nil).
I liked your suggestion about following the get/fetch/fetch! paradigm in Map and elsewhere, so I added a couple functions to the API:
fetch/1 and fetch_r/1 will return the tuple {value, updated_queue} if there are elements on the front or back of the queue, respectively, or :error otherwise
fetch!/1 and fetch_r!/1 will return the tuple {value, updated_queue} if there are elements on the front or back of the queue, respectively, or raise Yaq.EmptyQueueError otherwise
I also added a default value specification for dequeue/2 and dequeue_r/2 to follow the pattern from get.
You can only enumerate and collect from front to back, but you can reverse the queue in constant time with Yak.reverse/1, so I think it is a fair compromise.