flix

0.77.0

Peekable.flix

/*
 *  Copyright 2025 Ry Wiese
 *
 * Use of this source code is governed by the Apache 2.0 license
 * that can be found in the LICENSE.md file.
 */

pub mod Peekable {

    import java.lang.String

    ///
    /// Extends `Readable` with the ability to `peek` at values that have not yet been `read`,
    /// `skip` values that don't need to be read, and perform other kinds of conditional reads.
    ///
    pub trait Peekable[t] with Readable[t] {

        ///
        /// Returns `Ok(Some(x))` where x is the next value to be read, or `Ok(None)` if the `reader` is empty.
        /// Does not read `x` from the `reader`.
        ///
        /// Returns `Err(err)` if there is an IO error.
        ///
        pub def peek(reader: t): Result[IoError, Option[Readable.Elm[t]]] \ Readable.Aef[t]

        ///
        /// Advances the reader by `n`, skipping over the elements without reading them.
        ///
        /// Returns `Ok(k)` where `k` is the number of items skipped.
        ///
        /// Guarantees that `0 <= k <= max(n, 0)`. If `k < n`, then EOF has been reached.
        ///
        /// Returns `Err(err)` if there is an IO error.
        ///
        pub def skip(n: Int32, reader: t): Result[IoError, Int32] \ Readable.Aef[t]

        ///
        /// Reads from `reader` until an element is reached that does not satisfy predicate `p`.
        ///
        /// Returns `Ok(vec)` where `vec` is a vector of the first `k` elements where `k` is the index of the first element that does not satisfy `p`.
        /// This element is not included in the vector, and will be read on the next read.
        ///
        /// All elements of `vec` are guaranteed to satisfy `p`.
        ///
        /// Returns `Err(err)` if there is an IO error.
        ///
        pub def readWhile(p: Readable.Elm[t] -> Bool, reader: t): Result[IoError, Vector[Readable.Elm[t]]] \ Readable.Aef[t]

        ///
        /// Reads from `reader` until an element is reached that satisfies predicate `p`.
        ///
        /// Returns `Ok(vec)` where `vec` is a vector of the first `k` elements where `k` is the index of the first element that satisfies `p`.
        /// This element is not included in the vector, and will be read on the next read.
        ///
        /// All elements of `vec` are guaranteed to not satisfy `p`.
        ///
        /// Returns `Err(err)` if there is an IO error.
        ///
        pub def readUntil(p: Readable.Elm[t] -> Bool, reader: t): Result[IoError, Vector[Readable.Elm[t]]] \ Readable.Aef[t] = Peekable.readWhile(x -> not p(x), reader)

        ///
        /// Reads a single line from the `reader`.
        ///
        /// Returns `Ok(l)` where `l` is the first line. Returns `Ok("")` if EOF has been reached, or if an empty line was read.
        ///
        /// Lines are delineated by `\n`, `\r`, or `\r\n`. `\n` and `\r` characters are not included in the returned String.
        ///
        /// Returns `Err(err)` if there is an IO error.
        ///
        pub def readLine(reader: t): Result[IoError, String] \ IO + Readable.Aef[t] where Readable.Elm[t] ~ Char =
            forM (
                line <- Peekable.readUntil(c -> c == '\n' or c == '\r', reader);
                _ <- Peekable.skipElem('\r', reader);
                _ <- Peekable.skipElem('\n', reader)
            ) yield new String(line)

        ///
        /// Skips over the next element if it equals `a`.
        ///
        /// Returns `Ok(true)` if the element was skipped, `Ok(false)` if the element did not match or EOF was reached.
        ///
        /// Returns `Err(err)` if there is an IO error.
        ///
        pub def skipElem(elem: a, reader: t): Result[IoError, Bool] \ Readable.Aef[t] with Eq[a] where Readable.Elm[t] ~ a = Peekable.skipIf(e -> e == elem, reader)

        ///
        /// Skips over the next element if it matches the predicate `p`.
        ///
        /// Returns `Ok(true)` if the element was skipped, `Ok(false)` if the predicate did not match or EOF was reached.
        ///
        /// Returns `Err(err)` if there is an IO error.
        ///
        pub def skipIf(p: (a) -> Bool \ ef, reader: t): Result[IoError, Bool] \ ef + Readable.Aef[t] where Readable.Elm[t] ~ a =
            forM (
                peeked <- reader |> Peekable.peek;
                n <- match peeked {
                    case Some(next) =>
                        if (p(next)) {
                            reader |> Peekable.skip(1)
                        } else {
                            Ok(0)
                        }
                    case None => Ok(0)
                }
            ) yield n != 0

    }

}