import cats.Monad import cats.syntax.functor.* import cats.effect.IO import cats.effect.unsafe.implicits.global case class Frunfles(id: Long, name: String) trait DatabaseConnection: def exec[F[_]: Monad](op: String): F[Unit] object DatabaseConnection: given DatabaseConnection with def exec[F[_]: Monad](op: String): F[Unit] = Monad[F].pure(println(s">>> DB op = $op")) // single-dependency "primitive" environments type MonadEnv[F[_], A] = Monad[F] ?=> F[A] type DatabaseEnv[A] = DatabaseConnection ?=> A // composing envs by nesting // both are equivalent! type Env[F[_], A] = MonadEnv[F, DatabaseEnv[A]] type Env2[F[_], A] = DatabaseEnv[MonadEnv[F, A]] // multi-dependency environments // both are equivalent! type Env3[F[_], A] = (DatabaseConnection, Monad[F]) ?=> F[A] type Env4[F[_], A] = (Monad[F], DatabaseConnection) ?=> F[A] // need "Env" def findAllFrunfles[F[_]]: Env[F, List[Frunfles]] = summon[DatabaseConnection].exec("findAll").map(_ => List.empty) def findFrunflesById[F[_]](id: Long): Env[F, Option[Frunfles]] = summon[DatabaseConnection].exec("findById").map(_ => None) // need "Env2" def insertFrunfles[F[_]](f: Frunfles): Env2[F, Unit] = summon[DatabaseConnection].exec("insert") // need "Env3" def updateFrunfles[F[_]](f: Frunfles): Env3[F, Unit] = summon[DatabaseConnection].exec("update") // need "Env4" def deleteFrunfles[F[_]](id: Long): Env4[F, Unit] = summon[DatabaseConnection].exec("delete") // nesting order does not matter! // parameter order does not matter! // mixing different envs does not matter! // the functions are called as if the envs don't exist at all! val io: IO[Unit] = for _ <- insertFrunfles[IO](Frunfles(1L, "frunfles")) _ <- findFrunflesById[IO](1L) _ <- updateFrunfles[IO](Frunfles(1L, "sbrubbles")) _ <- deleteFrunfles[IO](1L) yield () val res1 = io.unsafeRunSync() val res2 = findAllFrunfles[IO].unsafeRunSync()