All Downloads are FREE. Search and download functionalities are using the official Maven repository.

kyo.Result.scala Maven / Gradle / Ivy

The newest version!
package kyo

import Result.*
import scala.annotation.implicitNotFound
import scala.annotation.targetName
import scala.reflect.ClassTag
import scala.util.Try
import scala.util.control.NonFatal

/** Represents a result that can be either a success, a failure or an unexpected panic.
  *
  * @tparam E
  *   The type of the error in case of failure
  * @tparam A
  *   The type of the value in case of success
  */
opaque type Result[+E, +A] >: (Success[A] | Error[E]) = Success[A] | Error[E]

/** Companion object for Result type */
object Result:
    import internal.*

    inline given [E, A](using inline ce: CanEqual[A, A]): CanEqual[Result[E, A], Result[E, A]] = CanEqual.derived
    inline given [E, A]: CanEqual[Result[E, A], Panic]                                         = CanEqual.derived

    /** Creates a Result from an expression that might throw an exception.
      *
      * @param expr
      *   The expression to evaluate
      * @tparam A
      *   The type of the successful result
      * @return
      *   A Result containing either the successful value or a Panic with the caught exception
      */
    inline def apply[A](inline expr: => A): Result[Nothing, A] =
        try
            Success(expr)
        catch
            case ex => Panic(ex)

    class CatchingOps[E <: Throwable](dummy: Unit) extends AnyVal:
        /** Evaluates an expression, catching a specific exception type.
          *
          * @param expr
          *   The expression to evaluate
          * @tparam A
          *   The type of the successful result
          * @return
          *   A Result containing either the successful value, a Fail with the caught exception, or a Panic for other exceptions
          */
        inline def apply[A](inline expr: => A)(using ClassTag[E]): Result[E, A] =
            try
                Success(expr)
            catch
                case ex: E => Fail(ex)
                case ex    => Panic(ex)
    end CatchingOps

    inline def catching[E <: Throwable]: CatchingOps[E] = CatchingOps(())

    /** Creates a successful Result.
      *
      * @param value
      *   The successful value
      * @tparam E
      *   The type of the error (not used in this case)
      * @tparam A
      *   The type of the successful value
      * @return
      *   A successful Result
      */
    inline def success[E, A](inline value: A): Result[E, A] = Success(value)

    /** Creates a failed Result.
      *
      * @param error
      *   The error value
      * @tparam E
      *   The type of the error
      * @tparam A
      *   The type of the successful value (not used in this case)
      * @return
      *   A failed Result
      */
    inline def fail[E, A](inline error: E): Result[E, A] = Fail(error)

    /** Creates a Result representing a panic situation.
      *
      * @param exception
      *   The exception causing the panic
      * @tparam E
      *   The type of the error (not used in this case)
      * @tparam A
      *   The type of the successful value (not used in this case)
      * @return
      *   A Result in a panic state
      */
    inline def panic[E, A](inline exception: Throwable): Result[E, A] = Panic(exception)

    /** Collects a sequence of Results into a single Result containing a sequence of successful values.
      *
      * @param seq
      *   The sequence of Results to collect
      * @tparam E
      *   The type of the error
      * @tparam A
      *   The type of the successful values
      * @return
      *   A Result containing either a sequence of successful values or the first encountered error
      */
    def collect[E, A](seq: Seq[Result[E, A]]): Result[E, Seq[A]] =
        def loop(remaining: Seq[Result[E, A]], acc: Chunk[A]): Result[E, Chunk[A]] =
            remaining match
                case (head: Result[E, A]) +: tail =>
                    head.fold(error => error)(value => loop(tail, acc.append(value)))
                case Seq() => Success(acc)

        loop(seq, Chunk.empty[A])
    end collect

    private val _unit = Success(())

    /** Returns a successful Result containing unit.
      *
      * @tparam E
      *   The type of the error (not used in this case)
      * @return
      *   A successful Result containing unit
      */
    def unit[E]: Result[E, Unit] = _unit

    /** Converts an Either to a Result.
      *
      * @param either
      *   The Either to convert
      * @tparam E
      *   The type of the error (Left value)
      * @tparam A
      *   The type of the successful value (Right value)
      * @return
      *   A Result equivalent to the input Either
      */
    def fromEither[E, A](either: Either[E, A]): Result[E, A] =
        either.fold(fail, success)

    /** Converts a Try to a Result.
      *
      * @param t
      *   The Try to convert
      * @tparam A
      *   The type of the successful value
      * @return
      *   A Result equivalent to the input Try
      */
    def fromTry[A](t: Try[A]): Result[Throwable, A] =
        t.fold(fail, success)

    /** Represents a successful Result. */
    opaque type Success[+A] = A | SuccessError[A]

    /** Companion object for Success type */
    object Success:
        /** Creates a Success instance.
          *
          * @param value
          *   The successful value
          * @tparam A
          *   The type of the successful value
          * @return
          *   A Success instance
          */
        def apply[A](value: A): Success[A] =
            value match
                case v: SuccessError[?]    => v.nest.asInstanceOf[Success[A]]
                case v: Fail[A] @unchecked => SuccessError(v)
                case v                     => v

        /** Extracts the value from a Success Result.
          *
          * @param self
          *   The Result to extract from
          * @tparam E
          *   The type of the error (not used in this case)
          * @tparam A
          *   The type of the successful value
          * @return
          *   A Maybe containing the successful value, or empty for non-Success Results
          */
        def unapply[E, A](self: Result[E, A]): Maybe.Ops[A] =
            self.fold(_ => Maybe.empty)(Maybe(_))

    end Success

    /** Represents an error in a Result. */
    sealed abstract class Error[+E]:
        /** Gets the failure value.
          *
          * @return
          *   The error value or exception
          */
        def getFailure: E | Throwable
    end Error
    object Error:
        def unapply[E, A](self: Result[E, A]): Maybe.Ops[E | Throwable] =
            self match
                case error: Error[E] @unchecked => Maybe(error.getFailure)
                case _                          => Maybe.empty
    end Error

    /** Represents a failure in a Result. */
    case class Fail[+E](error: E) extends Error[E]:
        def getFailure = error

    object Fail:
        /** Extracts the error value from a Fail Result.
          *
          * @param result
          *   The Result to extract from
          * @tparam E
          *   The type of the error
          * @tparam A
          *   The type of the successful value (not used in this case)
          * @return
          *   A Maybe containing the error value, or empty for non-Fail Results
          */
        def unapply[E, A](result: Result[E, A]): Maybe.Ops[E] =
            result match
                case result: Fail[E] @unchecked =>
                    Maybe(result.error)
                case _ => Maybe.empty
    end Fail

    /** Represents a panic situation in a Result. */
    case class Panic(exception: Throwable) extends Error[Nothing]:
        def getFailure = exception

    object Panic:
        /** Creates a Panic instance.
          *
          * @param exception
          *   The exception causing the panic
          * @return
          *   A Panic instance, or throws the exception if it's fatal
          */
        def apply(exception: Throwable): Panic =
            if NonFatal(exception) then
                new Panic(exception)
            else
                throw exception
    end Panic

    extension [E](self: Error[E])
        /** Gets the exception from an Error.
          *
          * @param ev
          *   Evidence that E is a subtype of Throwable
          * @return
          *   The exception
          */
        def exception(
            using
            @implicitNotFound("Error must be a 'Throwable'")
            ev: E <:< Throwable
        ): Throwable =
            self match
                case self: Fail[E] => self.error
                case self: Panic   => self.exception

    /** Provides extension methods for Result type */
    extension [E, A](self: Result[E, A])

        /** Checks if the Result is a Success.
          *
          * @return
          *   true if the Result is a Success, false otherwise
          */
        def isSuccess: Boolean =
            self match
                case _: Error[?] => false
                case _           => true

        /** Checks if the Result is a Fail.
          *
          * @return
          *   true if the Result is a Fail, false otherwise
          */
        def isFail =
            self.isInstanceOf[Fail[?]]

        /** Checks if the Result is a Panic.
          *
          * @return
          *   true if the Result is a Panic, false otherwise
          */
        def isPanic: Boolean =
            self.isInstanceOf[Panic]

        /** Gets the successful value if present.
          *
          * @return
          *   A Maybe containing the successful value, or empty for non-Success Results
          */
        def value: Maybe[A] =
            self match
                case self: Error[?] => Maybe.empty
                case self           => Maybe(self.asInstanceOf[A])

        /** Gets the error value if present.
          *
          * @return
          *   A Maybe containing the error value, or empty for non-Fail Results
          */
        @targetName("maybeError")
        def failure: Maybe[E] =
            self match
                case self: Fail[E] @unchecked => Maybe(self.error)
                case _                        => Maybe.empty

        /** Gets the panic exception if present.
          *
          * @return
          *   A Maybe containing the panic exception, or empty for non-Panic Results
          */
        @targetName("maybePanic")
        def panic: Maybe[Throwable] =
            self match
                case self: Panic => Maybe(self.exception)
                case _           => Maybe.empty

        /** Folds the Result into a value.
          *
          * @param ifFailure
          *   Function to apply if the Result is a failure
          * @param ifSuccess
          *   Function to apply if the Result is a success
          * @tparam B
          *   The type of the result of folding
          * @return
          *   The result of applying the appropriate function
          */
        inline def fold[B](inline ifFailure: Error[E] => B)(inline ifSuccess: A => B): B =
            self match
                case self: Error[E] @unchecked => ifFailure(self)
                case _ =>
                    try ifSuccess(self.asInstanceOf[Result[Nothing, A]].get)
                    catch
                        case ex => ifFailure(Panic(ex))

        /** Gets the successful value.
          *
          * @param ev
          *   Evidence that E is Nothing
          * @return
          *   The successful value
          * @throws NoSuchElementException
          *   if the Result is a Fail
          * @throws Throwable
          *   if the Result is a Panic
          */
        def get(
            using
            @implicitNotFound("Can't get result due to pending error: '${E}'")
            ev: E =:= Nothing
        ): A =
            self match
                case self: Fail[E] @unchecked => throw new NoSuchElementException(s"Error: ${self.error}")
                case self: Panic              => throw self.exception
                case self: SuccessError[?]    => self.unnest.asInstanceOf[A]
                case self                     => self.asInstanceOf[A]
            end match
        end get

        /** Gets the successful value or throws the error.
          *
          * @param ev
          *   Evidence that E is a subtype of Throwable
          * @return
          *   The successful value
          * @throws E
          *   if the Result is a Fail
          * @throws Throwable
          *   if the Result is a Panic
          */
        def getOrThrow(
            using
            @implicitNotFound("Error must be a 'Throwable' to invoke 'getOrThrow'. Found: '${E}'")
            ev: E <:< Throwable
        ): A =
            fold(e => throw e.exception)(identity)

        /** Gets the successful value or returns a default value.
          *
          * @param default
          *   The default value to return if the Result is not a Success
          * @tparam B
          *   A supertype of A
          * @return
          *   The successful value or the default value
          */
        inline def getOrElse[B >: A](inline default: => B): B =
            fold(_ => default)(identity)

        /** Returns this Result if it's a Success, or an alternative Result if it's not.
          *
          * @param alternative
          *   The alternative Result to return if this Result is not a Success
          * @tparam E2
          *   The error type of the alternative Result
          * @tparam B
          *   A supertype of A
          * @return
          *   This Result if it's a Success, or the alternative Result
          */
        def orElse[E2, B >: A](alternative: => Result[E2, B]): Result[E | E2, B] =
            fold(_ => alternative)(Result.success)

        /** Applies a function to the successful value of this Result.
          *
          * @param f
          *   The function to apply
          * @tparam E2
          *   The error type of the resulting Result
          * @tparam B
          *   The type of the resulting successful value
          * @return
          *   A new Result after applying the function
          */
        inline def flatMap[E2, B](inline f: A => Result[E2, B]): Result[E | E2, B] =
            self match
                case self: Error[E] @unchecked => self
                case self =>
                    try f(self.asInstanceOf[Success[A]].get)
                    catch
                        case ex => Panic(ex)

        /** Flattens a nested Result.
          *
          * @param ev
          *   Evidence that A is a Result
          * @tparam E2
          *   The error type of the inner Result
          * @tparam B
          *   The successful type of the inner Result
          * @return
          *   The flattened Result
          */
        def flatten[E2, B](using ev: A <:< Result[E2, B]): Result[E | E2, B] =
            flatMap(ev)

        /** Applies a function to the successful value of this Result.
          *
          * @param f
          *   The function to apply
          * @tparam B
          *   The type of the resulting successful value
          * @return
          *   A new Result after applying the function
          */
        inline def map[B](inline f: A => B): Result[E, B] =
            flatMap(v => Result.success(f(v)))

        /** Applies a function to the error value of this Result.
          *
          * @param f
          *   The function to apply
          * @tparam E2
          *   The type of the resulting error
          * @return
          *   A new Result after applying the function to the error
          */
        inline def mapFail[E2](inline f: E => E2): Result[E2, A] =
            self match
                case Fail(e) =>
                    try Fail(f(e))
                    catch
                        case ex => Panic(ex)
                case _ => self.asInstanceOf[Result[E2, A]]

        /** Applies a predicate to the successful value of this Result.
          *
          * @param p
          *   The predicate to apply
          * @return
          *   A new Result that fails if the predicate doesn't hold
          */
        inline def withFilter(inline p: A => Boolean): Result[E | NoSuchElementException, A] =
            filter(p)

        /** Applies a predicate to the successful value of this Result.
          *
          * @param p
          *   The predicate to apply
          * @return
          *   A new Result that fails if the predicate doesn't hold
          */
        inline def filter(inline p: A => Boolean): Result[E | NoSuchElementException, A] =
            flatMap { v =>
                if !p(v) then
                    Fail(new NoSuchElementException("Predicate does not hold for " + v))
                else
                    v
            }

        /** Recovers from an error by applying a partial function.
          *
          * @param pf
          *   The partial function to apply to the error
          * @tparam B
          *   A supertype of A
          * @return
          *   A new Result with the error potentially recovered
          */
        inline def recover[B >: A](pf: PartialFunction[Error[E], B]): Result[E, B] =
            try
                self match
                    case self: Error[E] @unchecked if pf.isDefinedAt(self) =>
                        Result.success(pf(self))
                    case _ => self
            catch
                case ex => Panic(ex)

        /** Recovers from an error by applying a partial function that returns a new Result.
          *
          * @param pf
          *   The partial function to apply to the error
          * @tparam E2
          *   The error type of the resulting Result
          * @tparam B
          *   A supertype of A
          * @return
          *   A new Result with the error potentially recovered
          */
        inline def recoverWith[E2, B >: A](pf: PartialFunction[Error[E], Result[E2, B]]): Result[E | E2, B] =
            try
                self match
                    case self: Error[E] @unchecked if pf.isDefinedAt(self) =>
                        pf(self)
                    case _ => self
            catch
                case ex => Panic(ex)

        /** Converts the Result to an Either.
          *
          * @return
          *   An Either with Left containing the error or exception, and Right containing the successful value
          */
        def toEither: Either[E | Throwable, A] =
            fold(e => Left(e.getFailure))(Right(_))

        /** Converts the Result to a Maybe.
          *
          * @return
          *   A Maybe containing the successful value, or empty for non-Success Results
          */
        def toMaybe: Maybe[A] =
            fold(_ => Maybe.empty)(Maybe(_))

        /** Converts the Result to a Try.
          *
          * @param ev
          *   Evidence that E is a subtype of Throwable
          * @return
          *   A Try containing the successful value, or Failure with the error
          */
        def toTry(using
            @implicitNotFound("Fail type must be a 'Throwable' to invoke 'toTry'. Found: '${E}'")
            ev: E <:< Throwable
        ): Try[A] =
            fold(e => scala.util.Failure(e.getFailure.asInstanceOf[Throwable]))(scala.util.Success(_))

        /** Swaps the success and failure cases of the Result.
          *
          * @return
          *   A new Result with success and failure swapped
          */
        def swap: Result[A, E] =
            self match
                case Fail(e)    => Result.success(e)
                case Success(v) => Result.fail(v)
                case _          => self.asInstanceOf[Result[A, E]]

        /** Returns a string representation of the Result.
          *
          * @return
          *   A string describing the Result's state and value
          */
        def show: String =
            self match
                case Panic(ex) => s"Panic($ex)"
                case Fail(ex)  => s"Fail($ex)"
                case v         => s"Success($v)"

    end extension

    private object internal:
        case class SuccessError[+A](failure: Error[A], depth: Int = 1):
            def unnest: Result[Any, A] =
                if depth > 1 then
                    SuccessError(failure, depth - 1)
                else
                    failure
            def nest: Success[A] =
                SuccessError(failure, depth + 1)

            override def toString: String =
                "Success(" * depth + failure.toString + ")" * depth
        end SuccessError
    end internal
end Result




© 2015 - 2024 Weber Informatics LLC | Privacy Policy