diff --git a/README.md b/README.md index 406a0d2..88ed955 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,16 @@ # Flix Extras A collection of functionality that extend the official Flix library. + +## Modules + +- `Extras.Queue` — an immutable first-in, first-out queue. + +## Usage + +Add the package to the `flix.toml` of your own project: + +```toml +[dependencies] +"github:flix/extras" = "0.1.0" +``` diff --git a/src/Extras.flix b/src/Extras.flix new file mode 100644 index 0000000..d16fd49 --- /dev/null +++ b/src/Extras.flix @@ -0,0 +1,25 @@ +/* + * Copyright 2026 Flix Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/// +/// The `Extras` module is a collection of submodules that extend the official +/// Flix library, each offering a self-contained piece of functionality: +/// +/// - `Extras.Queue` is an immutable first-in, first-out queue. +/// +pub mod Extras { + /* empty */ +} diff --git a/src/Extras/Queue.flix b/src/Extras/Queue.flix new file mode 100644 index 0000000..71c5e02 --- /dev/null +++ b/src/Extras/Queue.flix @@ -0,0 +1,116 @@ +/* + * Copyright 2026 Flix Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +pub mod Extras.Queue { + + /// + /// The Queue type. + /// + /// An immutable first-in, first-out (FIFO) queue. + /// + /// A queue is represented as a pair of lists: a front list, in queue order, + /// and a back list, in reverse queue order. Elements are dequeued from the + /// front and enqueued onto the back. When the front is exhausted the back is + /// reversed to become the new front, which gives `enqueue`, `dequeue`, and + /// `peek` amortized constant time. + /// + /// The representation maintains the invariant that the front list is empty + /// only if the queue itself is empty. + /// + /// Note - the constructor `Queue` should not be used directly. + /// + pub enum Queue[a] { + case Queue(List[a], List[a]) + } + + instance Eq[Queue[a]] with Eq[a] { + pub def eq(q1: Queue[a], q2: Queue[a]): Bool = Queue.toList(q1) == Queue.toList(q2) + } + + instance ToString[Queue[a]] with ToString[a] { + pub def toString(q: Queue[a]): String = "Queue#{" + List.join(", ", Queue.toList(q)) + "}" + } + + /// + /// Returns the empty queue. + /// + pub def empty(): Queue[a] = Queue.Queue(Nil, Nil) + + /// + /// Returns `true` if and only if the queue `q` is empty. + /// + pub def isEmpty(q: Queue[a]): Bool = match q { + case Queue.Queue(front, _) => List.isEmpty(front) + } + + /// + /// Returns the number of elements in the queue `q`. + /// + pub def size(q: Queue[a]): Int32 = match q { + case Queue.Queue(front, back) => List.length(front) + List.length(back) + } + + /// + /// Returns the queue `q` with the element `x` added to its back. + /// + pub def enqueue(x: a, q: Queue[a]): Queue[a] = match q { + case Queue.Queue(front, back) => unit(front, x :: back) + } + + /// + /// Returns `Some((x, rest))` where `x` is the element at the front of the + /// queue `q` and `rest` is `q` without `x`. + /// + /// Returns `None` if the queue `q` is empty. + /// + pub def dequeue(q: Queue[a]): Option[(a, Queue[a])] = match q { + case Queue.Queue(Nil, _) => None + case Queue.Queue(x :: front, back) => Some((x, unit(front, back))) + } + + /// + /// Returns `Some(x)` where `x` is the element at the front of the queue `q`. + /// + /// Returns `None` if the queue `q` is empty. + /// + pub def peek(q: Queue[a]): Option[a] = match q { + case Queue.Queue(front, _) => List.head(front) + } + + /// + /// Returns the elements of the queue `q` as a list, in queue order. + /// + pub def toList(q: Queue[a]): List[a] = match q { + case Queue.Queue(front, back) => List.append(front, List.reverse(back)) + } + + /// + /// Returns a queue holding the elements of the list `l`, where the head of + /// `l` is at the front of the queue. + /// + pub def fromList(l: List[a]): Queue[a] = Queue.Queue(l, Nil) + + /// + /// Returns a queue holding the elements of `front` followed by the reverse of + /// `back`, restoring the invariant that the front is empty only if the queue + /// is empty. + /// + def unit(front: List[a], back: List[a]): Queue[a] = match front { + case Nil => Queue.Queue(List.reverse(back), Nil) + case _ => Queue.Queue(front, back) + } + +} diff --git a/test/TestQueue.flix b/test/TestQueue.flix new file mode 100644 index 0000000..aae0536 --- /dev/null +++ b/test/TestQueue.flix @@ -0,0 +1,222 @@ +mod TestQueue { + + use Assert.{assertEq, assertNeq, assertTrue, assertFalse, assertNone, fail}; + use Extras.Queue + + ///////////////////////////////////////////////////////////////////////////// + // empty // + ///////////////////////////////////////////////////////////////////////////// + + @Test + def empty01(): Unit \ Assert = + let q: Queue[Int32] = Queue.empty(); + assertEq(expected = Nil, Queue.toList(q)) + + @Test + def empty02(): Unit \ Assert = + let q: Queue[String] = Queue.empty(); + assertTrue(Queue.isEmpty(q)) + + @Test + def empty03(): Unit \ Assert = + let q: Queue[Int32] = Queue.empty(); + assertEq(expected = 0, Queue.size(q)) + + ///////////////////////////////////////////////////////////////////////////// + // isEmpty // + ///////////////////////////////////////////////////////////////////////////// + + @Test + def isEmpty01(): Unit \ Assert = + let q: Queue[Int32] = Queue.empty(); + assertTrue(Queue.isEmpty(q)) + + @Test + def isEmpty02(): Unit \ Assert = + assertFalse(Queue.isEmpty(Queue.enqueue(1, Queue.empty()))) + + @Test + def isEmpty03(): Unit \ Assert = + // A queue emptied by dequeue is empty again. + match Queue.dequeue(Queue.enqueue(1, Queue.empty())) { + case Some((_, rest)) => assertTrue(Queue.isEmpty(rest)) + case None => fail("expected a non-empty queue") + } + + ///////////////////////////////////////////////////////////////////////////// + // size // + ///////////////////////////////////////////////////////////////////////////// + + @Test + def size01(): Unit \ Assert = + let q: Queue[Int32] = Queue.empty(); + assertEq(expected = 0, Queue.size(q)) + + @Test + def size02(): Unit \ Assert = + assertEq(expected = 1, Queue.size(Queue.enqueue(1, Queue.empty()))) + + @Test + def size03(): Unit \ Assert = + // Counts elements in both the front and the back list. + let q = Queue.enqueue(3, Queue.enqueue(2, Queue.fromList(1 :: Nil))); + assertEq(expected = 3, Queue.size(q)) + + ///////////////////////////////////////////////////////////////////////////// + // enqueue // + ///////////////////////////////////////////////////////////////////////////// + + @Test + def enqueue01(): Unit \ Assert = + assertEq(expected = 1 :: Nil, Queue.toList(Queue.enqueue(1, Queue.empty()))) + + @Test + def enqueue02(): Unit \ Assert = + let q = Queue.enqueue(2, Queue.enqueue(1, Queue.empty())); + assertEq(expected = 1 :: 2 :: Nil, Queue.toList(q)) + + @Test + def enqueue03(): Unit \ Assert = + // Enqueueing leaves the original queue unchanged. + let q1 = Queue.fromList(1 :: Nil); + let q2 = Queue.enqueue(2, q1); + assertEq(expected = 1 :: Nil, Queue.toList(q1)); + assertEq(expected = 1 :: 2 :: Nil, Queue.toList(q2)) + + ///////////////////////////////////////////////////////////////////////////// + // dequeue // + ///////////////////////////////////////////////////////////////////////////// + + @Test + def dequeue01(): Unit \ Assert = + let q: Queue[Int32] = Queue.empty(); + assertNone(Queue.dequeue(q)) + + @Test + def dequeue02(): Unit \ Assert = + match Queue.dequeue(Queue.fromList(1 :: 2 :: 3 :: Nil)) { + case Some((x, rest)) => + assertEq(expected = 1, x); + assertEq(expected = 2 :: 3 :: Nil, Queue.toList(rest)) + case None => fail("expected a non-empty queue") + } + + @Test + def dequeue03(): Unit \ Assert = + // Elements leave in the order they were enqueued, even when the front + // list must be refilled from the back list. + let q = Queue.enqueue(3, Queue.enqueue(2, Queue.enqueue(1, Queue.empty()))); + match Queue.dequeue(q) { + case Some((x, rest)) => + assertEq(expected = 1, x); + assertEq(expected = 2 :: 3 :: Nil, Queue.toList(rest)) + case None => fail("expected a non-empty queue") + } + + ///////////////////////////////////////////////////////////////////////////// + // peek // + ///////////////////////////////////////////////////////////////////////////// + + @Test + def peek01(): Unit \ Assert = + let q: Queue[Int32] = Queue.empty(); + assertNone(Queue.peek(q)) + + @Test + def peek02(): Unit \ Assert = + assertEq(expected = Some(1), Queue.peek(Queue.fromList(1 :: 2 :: Nil))) + + @Test + def peek03(): Unit \ Assert = + // Peeking finds the new front element after the back list has been + // reversed into the front. + let q = Queue.enqueue(3, Queue.enqueue(2, Queue.enqueue(1, Queue.empty()))); + match Queue.dequeue(q) { + case Some((_, rest)) => assertEq(expected = Some(2), Queue.peek(rest)) + case None => fail("expected a non-empty queue") + } + + ///////////////////////////////////////////////////////////////////////////// + // toList // + ///////////////////////////////////////////////////////////////////////////// + + @Test + def toList01(): Unit \ Assert = + let q: Queue[Int32] = Queue.empty(); + assertEq(expected = Nil, Queue.toList(q)) + + @Test + def toList02(): Unit \ Assert = + // Returns queue order across both the front and the back list. + let q = Queue.enqueue(3, Queue.enqueue(2, Queue.fromList(1 :: Nil))); + assertEq(expected = 1 :: 2 :: 3 :: Nil, Queue.toList(q)) + + @Test + def toList03(): Unit \ Assert = + // Returns queue order after the back list has been reversed into the front. + let q = Queue.enqueue(3, Queue.enqueue(2, Queue.enqueue(1, Queue.empty()))); + match Queue.dequeue(q) { + case Some((_, rest)) => assertEq(expected = 2 :: 3 :: Nil, Queue.toList(rest)) + case None => fail("expected a non-empty queue") + } + + ///////////////////////////////////////////////////////////////////////////// + // fromList // + ///////////////////////////////////////////////////////////////////////////// + + @Test + def fromList01(): Unit \ Assert = + let q: Queue[Int32] = Queue.fromList(Nil); + assertTrue(Queue.isEmpty(q)) + + @Test + def fromList02(): Unit \ Assert = + assertEq(expected = 1 :: 2 :: 3 :: Nil, Queue.toList(Queue.fromList(1 :: 2 :: 3 :: Nil))) + + @Test + def fromList03(): Unit \ Assert = + // The head of the list is at the front of the queue. + assertEq(expected = Some("a"), Queue.peek(Queue.fromList("a" :: "b" :: Nil))) + + ///////////////////////////////////////////////////////////////////////////// + // Eq.eq // + ///////////////////////////////////////////////////////////////////////////// + + @Test + def eq01(): Unit \ Assert = + let q1: Queue[Int32] = Queue.empty(); + let q2: Queue[Int32] = Queue.fromList(Nil); + assertEq(expected = q1, q2) + + @Test + def eq02(): Unit \ Assert = + // Equality is by queue order, not by representation: these two queues hold + // the same elements but split them differently across front and back. + let q1 = Queue.fromList(1 :: 2 :: Nil); + let q2 = Queue.enqueue(2, Queue.enqueue(1, Queue.empty())); + assertEq(expected = q1, q2) + + @Test + def eq03(): Unit \ Assert = + assertNeq(unexpected = Queue.fromList(1 :: 2 :: Nil), Queue.fromList(2 :: 1 :: Nil)) + + ///////////////////////////////////////////////////////////////////////////// + // ToString.toString // + ///////////////////////////////////////////////////////////////////////////// + + @Test + def toString01(): Unit \ Assert = + let q: Queue[Int32] = Queue.empty(); + assertEq(expected = "Queue#{}", ToString.toString(q)) + + @Test + def toString02(): Unit \ Assert = + assertEq(expected = "Queue#{1}", ToString.toString(Queue.fromList(1 :: Nil))) + + @Test + def toString03(): Unit \ Assert = + // Prints in queue order across both the front and the back list. + let q = Queue.enqueue(3, Queue.enqueue(2, Queue.fromList(1 :: Nil))); + assertEq(expected = "Queue#{1, 2, 3}", ToString.toString(q)) + +}