Skip to content

Instantly share code, notes, and snippets.

@CJSmith-0141
Created March 2, 2025 05:01
Show Gist options
  • Select an option

  • Save CJSmith-0141/f2a2fc08367304706f403e6bd04d5eb5 to your computer and use it in GitHub Desktop.

Select an option

Save CJSmith-0141/f2a2fc08367304706f403e6bd04d5eb5 to your computer and use it in GitHub Desktop.
//> using toolkit typelevel:default
//> using scala 3.6.3
//> using dep org.typelevel::cats-mtl:1.5.0
import cats.effect.*
import cats.mtl.{Handle, Raise}
import cats.syntax.all.*
import cats.mtl.syntax.all.*
import cats.*
import cats.data.NonEmptyList
import scala.util.control.NoStackTrace
import scala.reflect.ClassTag
import fs2.io.file.Files
/** Algebra represents a task that can fetch a token which can failed We have
* different way to fetch token, so we want a way to combine multiple tasks
* into one The goal is keep algebra simple but still handle some business
* error so that I don't want to use MonadTransformer like EitherT this is my
* efford using mtl.Hanlde and ApplicativeThrow to solve this problem
*/
sealed trait Algebra[F[_], A]:
def fetch: F[A]
object Algebra:
sealed trait Error extends Throwable with NoStackTrace
object Error:
case class Single(msg: String) extends Error:
override def toString() = msg
case class Multiple(errors: NonEmptyList[Error]) extends Error:
override def toString() = errors.toList.mkString("", ", ", "")
def apply(msg: String): Error = Single(msg)
given Semigroup[Error] with
def combine(x: Error, y: Error): Error =
(x, y) match
case (Error.Single(msg1), Error.Single(msg2)) =>
Error.Multiple(
NonEmptyList.of(Error.Single(msg1), Error.Single(msg2))
)
case (Error.Single(msg1), Error.Multiple(errors)) =>
Error.Multiple(errors.prepend(Error.Single(msg1)))
case (Error.Multiple(errors), Error.Single(msg2)) =>
Error.Multiple(errors.append(Error.Single(msg2)))
case (Error.Multiple(errors1), Error.Multiple(errors2)) =>
Error.Multiple(errors1.concatNel(errors2))
/** If We have Handle[F, E] then we have SemigroupK[F] and we accumulate all
* errors to report to users if needed
*/
given [F[_], E: Semigroup] => Handle[F, E] => SemigroupK[F]:
def combineK[A](x: F[A], y: F[A]): F[A] =
x.handleWith[E] { e1 =>
y.handleWith[E] { e2 =>
(e1 |+| e2).raise[F, A]
}
}
// derive Handle[F, E] via ApplicativeThrow as long as E extends Throwable
// inspire by the yellow sumarine pr in mtl repo
given [F[_]: ApplicativeThrow, E <: Throwable] => (ClassTag[E])
=> Handle[F, E]:
def applicative: Applicative[F] = summon[Applicative[F]]
def raise[E2 <: E, A](e: E2): F[A] = e.raiseError
def handleWith[A](fa: F[A])(f: E => F[A]): F[A] =
fa.handleErrorWith:
case e: E => f(e)
case e => e.raiseError
// We need this to make Algebra a `ApplicativeThrow`
type AlgebraApp = [F[_]] =>> [x] =>> Algebra[F, x]
given [F[_]: Applicative]: Applicative[AlgebraApp[F]] with
def pure[A](x: A): Algebra[F, A] =
new Algebra[F, A]:
def fetch: F[A] = x.pure[F]
def ap[A, B](ff: Algebra[F, A => B])(fa: Algebra[F, A]): Algebra[F, B] =
new Algebra[F, B]:
def fetch: F[B] =
ff.fetch.ap(fa.fetch)
given [F[_]: ApplicativeThrow] => Handle[AlgebraApp[F], Error]:
def applicative: Applicative[AlgebraApp[F]] =
summon[Applicative[AlgebraApp[F]]]
def raise[E2 <: Error, A](e: E2): Algebra[F, A] = new Algebra[F, A]:
def fetch: F[A] = e.raiseError
def handleWith[A](fa: Algebra[F, A])(
f: Error => Algebra[F, A]
): Algebra[F, A] =
new Algebra[F, A]:
def fetch: F[A] =
fa.fetch.handleErrorWith {
case e: Error => f(e).fetch
case e => e.raiseError
}
/*==========some constructor for Algebra============== */
def pure[F[_]: Applicative, A](a: A): Algebra[F, A] =
new:
def fetch: F[A] = a.pure[F]
def apply[F[_]: Monad, A](f: F[Either[Error, A]])(using
Raise[F, Error]
): Algebra[F, A] =
new:
def fetch: F[A] =
f.flatMap(Raise[F, Error].fromEither)
def raise[F[_], E, A](error: E)(using Raise[F, E]): Algebra[F, A] =
new:
def fetch: F[A] = error.raise
type Token = String
/*==========some pre implemented ============== */
def local[F[_]: Concurrent: Files](): Algebra[F, Token] =
raise(Error("Local Not implemented"))
def env[F[_]: Concurrent]: Algebra[F, Token] =
raise(Error("Env Not implemented"))
def kubernetes[F[_]: Concurrent: Files](): Algebra[F, Token] =
raise(Error("Kubernetes Not implemented"))
def hardcoded[F[_]: Applicative]: Algebra[F, Token] = pure("token")
object App extends IOApp.Simple:
import Algebra.{*, given}
def run: IO[Unit] =
(local[IO]() <+> env[IO]).fetch.attempt.flatMap {
case Right(token) => IO.println(s"Token: $token")
case Left(error) => IO.println(s"Error: $error") // println error
} >>
(local[IO]() <+> hardcoded[IO]).fetch.attempt.flatMap {
case Right(token) => IO.println(s"Token: $token") // println token
case Left(error) => IO.println(s"Error: $error")
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment