diff --git a/kyo-core/shared/src/main/scala/kyo/aborts.scala b/kyo-core/shared/src/main/scala/kyo/aborts.scala index 7c31471c7..2f6e62063 100644 --- a/kyo-core/shared/src/main/scala/kyo/aborts.scala +++ b/kyo-core/shared/src/main/scala/kyo/aborts.scala @@ -10,6 +10,7 @@ import scala.util.Success import kyo._ import core._ import tries._ +import layers._ import scala.util.Try object aborts { @@ -28,7 +29,7 @@ object aborts { } final class Aborts[E] private[aborts] (private val tag: Tag[E]) - extends Effect[Abort[E]#Value, Aborts[E]] { + extends Effect[Abort[E]#Value, Aborts[E]] { self => private implicit def _tag: Tag[E] = tag @@ -91,5 +92,19 @@ object aborts { } override def toString = s"Aborts[${tag.tag.longNameWithPrefix}]" + + def layer[Se](handle: E => Nothing > Se): Layer[Aborts[E], Se] = + new Layer[Aborts[E], Se] { + override def run[T, S]( + effect: T > (Aborts[E] with S) + )( + implicit flat: Flat[T > (Aborts[E] with S)] + ): T > (S with Se) = + self.run[T, S](effect)(flat).map { + case Left(err) => handle(err) + case Right(t) => t + } + } } + } diff --git a/kyo-core/shared/src/main/scala/kyo/envs.scala b/kyo-core/shared/src/main/scala/kyo/envs.scala index 560643e6b..83f9a489b 100644 --- a/kyo-core/shared/src/main/scala/kyo/envs.scala +++ b/kyo-core/shared/src/main/scala/kyo/envs.scala @@ -5,6 +5,7 @@ import izumi.reflect._ import scala.reflect.ClassTag import kyo.core._ +import kyo.layers.Layer object envs { @@ -15,7 +16,7 @@ object envs { } final class Envs[E] private[envs] (implicit private val tag: Tag[_]) - extends Effect[Env[E]#Value, Envs[E]] { + extends Effect[Env[E]#Value, Envs[E]] { self => val get: E > Envs[E] = suspend(Input.asInstanceOf[Env[E]#Value[E]]) @@ -47,6 +48,14 @@ object envs { } override def toString = s"Envs[${tag.tag.longNameWithPrefix}]" + + def layer[Sd](construct: E > Sd): Layer[Envs[E], Sd] = + new Layer[Envs[E], Sd] { + override def run[T, S](effect: T > (Envs[E] with S))(implicit + fl: Flat[T > (Envs[E] with S)] + ): T > (Sd with S) = + construct.map(e => self.run[T, S](e)(effect)) + } } object Envs { diff --git a/kyo-core/shared/src/main/scala/kyo/layers.scala b/kyo-core/shared/src/main/scala/kyo/layers.scala new file mode 100644 index 000000000..8ff8da25b --- /dev/null +++ b/kyo-core/shared/src/main/scala/kyo/layers.scala @@ -0,0 +1,131 @@ +package kyo + +object layers { + + trait Layer[In, Out] { self => + def run[T, S](effect: T > (In with S))(implicit fl: Flat[T > (In with S)]): T > (S with Out) + + final def andThen[Out1, In1](other: Layer[In1, Out1]): Layer[In with In1, Out with Out1] = + new Layer[In with In1, Out with Out1] { + override def run[T, S]( + effect: T > (In with In1 with S) + )( + implicit fl: Flat[T > (In with In1 with S)] + ): T > (S with Out with Out1) = { + val selfRun: T > (In1 with Out with S) = + self.run[T, S with In1](effect: T > (In1 with In with S)) + val otherRun: T > (Out with Out1 with S) = + other.run[T, S with Out](selfRun)(Flat.unsafe.unchecked) + otherRun + } + } + + final def chain[In2, Out2](other: Layer[In2, Out2])( + implicit ap: ChainLayer[Out, In2] + ): Layer[In, Out2 with ap.RemainingOut1] = { + ap.applyLayer[In, Out2](self, other) + } + } + + sealed trait ChainLayer[Out1, In2] { + type RemainingOut1 + + def applyLayer[In1, Out2]( + layer1: Layer[In1, Out1], + layer2: Layer[In2, Out2] + ): Layer[In1, RemainingOut1 with Out2] + } + + trait ChainLayers2 { + implicit def application[Out1, Shared, In2] + : ChainLayer.Aux[Out1 with Shared, In2 with Shared, Out1] = + new ChainLayer[Out1 with Shared, In2 with Shared] { + type RemainingOut1 = Out1 + override def applyLayer[In1, Out2]( + layer1: Layer[In1, Out1 with Shared], + layer2: Layer[In2 with Shared, Out2] + ): Layer[In1, Out1 with Out2] = + new Layer[In1, Out1 with Out2] { + override def run[T, S](effect: T > (In1 with S))(implicit + fl: Flat[T > (In1 with S)] + ): T > (S with Out2 with Out1) = { + val handled1: T > (S with Out1 with Shared) = layer1.run[T, S](effect) + val handled2: T > (S with Out2 with Out1) = + layer2.run[T, S with Out1](handled1)(Flat.unsafe.unchecked) + handled2 + } + } + + } + } + + trait ChainLayers1 { + implicit def applyAll1[Shared, In2]: ChainLayer.Aux[Shared, In2 with Shared, Any] = + new ChainLayer[Shared, In2 with Shared] { + type RemainingOut1 = Any + + override def applyLayer[In1, Out2]( + layer1: Layer[In1, Shared], + layer2: Layer[In2 with Shared, Out2] + ): Layer[In1, Out2] = + new Layer[In1, Out2] { + override def run[T, S](effect: T > (In1 with S))(implicit + fl: Flat[T > (In1 with S)] + ): T > (S with Out2) = { + val handled1: T > (S with Shared) = layer1.run[T, S](effect) + val handled2: T > (S with Out2) = + layer2.run[T, S](handled1)(Flat.unsafe.unchecked) + handled2 + } + } + + } + + implicit def applyAll2[Out1, Shared]: ChainLayer.Aux[Out1 with Shared, Shared, Out1] = + new ChainLayer[Out1 with Shared, Shared] { + type RemainingOut1 = Out1 + + override def applyLayer[In1, Out2]( + layer1: Layer[In1, Out1 with Shared], + layer2: Layer[Shared, Out2] + ): Layer[In1, Out1 with Out2] = + new Layer[In1, Out1 with Out2] { + override def run[T, S](effect: T > (In1 with S))(implicit + fl: Flat[T > (In1 with S)] + ): T > (S with Out1 with Out2) = { + val handled1: T > (S with Out1 with Shared) = layer1.run[T, S](effect) + val handled2: T > (S with Out1 with Out2) = + layer2.run[T, S with Out1](handled1)(Flat.unsafe.unchecked) + handled2 + } + } + + } + } + + object ChainLayer extends ChainLayers1 { + type Aux[Out1, In2, R] = ChainLayer[Out1, In2] { type RemainingOut1 = R } + + implicit def simpleChain[Out]: ChainLayer.Aux[Out, Out, Any] = + new ChainLayer[Out, Out] { + type RemainingOut1 = Any + + override def applyLayer[In1, Out2]( + layer1: Layer[In1, Out], + layer2: Layer[Out, Out2] + ): Layer[In1, Any with Out2] = + new Layer[In1, Any with Out2] { + override def run[T, S](effect: T > (In1 with S))(implicit + fl: Flat[T > (In1 with S)] + ): T > (S with Out2) = { + val handled1: T > (Out with S) = layer1.run[T, S](effect) + val handled2: T > (S with Out2) = + layer2.run[T, S](handled1)(Flat.unsafe.unchecked) + handled2 + } + } + + } + } + +} diff --git a/kyo-core/shared/src/main/scala/kyo/options.scala b/kyo-core/shared/src/main/scala/kyo/options.scala index 7fb40748c..796291e12 100644 --- a/kyo-core/shared/src/main/scala/kyo/options.scala +++ b/kyo-core/shared/src/main/scala/kyo/options.scala @@ -1,6 +1,7 @@ package kyo import kyo.aborts._ +import kyo.layers._ object options { @@ -50,5 +51,16 @@ object options { case v => get(v) } } + + def layer[Se](onEmpty: => Nothing > Se): Layer[Options, Se] = + new Layer[Options, Se] { + override def run[T, S](effect: T > (Options with S))(implicit + fl: Flat[T > (Options with S)] + ): T > (S with Se) = + Options.run[T, S](effect).map { + case None => onEmpty + case Some(t) => t + } + } } } diff --git a/kyo-core/shared/src/main/scala/kyo/tries.scala b/kyo-core/shared/src/main/scala/kyo/tries.scala index 1932b1c08..de3161244 100644 --- a/kyo-core/shared/src/main/scala/kyo/tries.scala +++ b/kyo-core/shared/src/main/scala/kyo/tries.scala @@ -1,6 +1,7 @@ package kyo import kyo.aborts._ +import kyo.layers._ import scala.util._ @@ -43,5 +44,16 @@ object tries { case Failure(ex) => fail(ex) } + + def layer[Se](handle: Throwable => Nothing > Se): Layer[Tries, Se] = + new Layer[Tries, Se] { + override def run[T, S](effect: T > (Tries with S))(implicit + fl: Flat[T > (Tries with S)] + ): T > (S with Se) = + Tries.run[T, S](effect).map { + case Failure(exception) => handle(exception) + case Success(t) => t + } + } } } diff --git a/kyo-core/shared/src/test/scala/kyoTest/layersTest.scala b/kyo-core/shared/src/test/scala/kyoTest/layersTest.scala new file mode 100644 index 000000000..d7951ee77 --- /dev/null +++ b/kyo-core/shared/src/test/scala/kyoTest/layersTest.scala @@ -0,0 +1,120 @@ +package kyoTest + +import kyo._ +import kyo.envs._ +import kyo.aborts._ +import kyo.tries._ +import kyo.options._ +import kyo.layers._ +import scala.util.Failure + +class layersTest extends KyoTest { + + final case class Dep1(int: Int) + final case class Dep2(str: String) + final case class Dep3(bool: Boolean) + + final case class Dep(dep1: Int, dep2: String, dep3: Boolean) + + val depLayer = Envs[Dep].layer(Dep(1, "hello", true)) + val dep1Layer = Envs[Dep1].layer(Envs[Dep].get.map(v => Dep1(v.dep1))) + val dep2Layer = Envs[Dep2].layer(Envs[Dep].get.map(v => Dep2(v.dep2))) + val dep3Layer = Envs[Dep3].layer(Envs[Dep].get.map(v => Dep3(v.dep3))) + + "Envs layers should be composable and provide multiple dependencies" in { + val layer = (dep1Layer andThen dep2Layer andThen dep3Layer) chain depLayer + + val effect = for { + dep1 <- Envs[Dep1].get + dep2 <- Envs[Dep2].get + dep3 <- Envs[Dep3].get + } yield s"${dep1.int}-${dep2.str}-${dep3.bool}" + + val handledEffect = layer.run(effect) + + assert(handledEffect == "1-hello-true") + } + + final case class TestError1(msg: String) + final case class TestError2(msg: String) + val stringToTE1Layer = Aborts[String].layer(str => Aborts[TestError1].fail(TestError1(str))) + val dep1ToTE1Layer = Aborts[Dep2].layer(dep => Aborts[TestError1].fail(TestError1(dep.str))) + val throwableToTE1Layer = + Aborts[Throwable].layer(err => Aborts[TestError1].fail(TestError1(err.getMessage()))) + val testError1ToTE2Layer = + Aborts[TestError1].layer(err => Aborts[TestError2].fail(TestError2(err.msg))) + + "Aborts layers should be composable and handle multiple error types" in { + val layer = + (stringToTE1Layer andThen dep1ToTE1Layer andThen throwableToTE1Layer) chain testError1ToTE2Layer + + val effect1 = for { + _ <- Aborts[String].fail("string failure") + _ <- Aborts[Dep2].fail(Dep2("dep2 failure")) + _ <- Aborts[Throwable].fail(new Exception("throwable failure")) + } yield () + + val effect2 = for { + _ <- Aborts[Throwable].fail(new Exception("throwable failure")) + _ <- Aborts[String].fail("string failure") + _ <- Aborts[Dep2].fail(Dep2("dep2 failure")) + } yield () + + val effect3 = for { + _ <- Aborts[Dep2].fail(Dep2("dep2 failure")) + _ <- Aborts[Throwable].fail(new Exception("throwable failure")) + _ <- Aborts[String].fail("string failure") + } yield () + + assert(Aborts[TestError2].run(layer.run(effect1)) == Left(TestError2("string failure"))) + assert(Aborts[TestError2].run(layer.run(effect2)) == Left(TestError2("throwable failure"))) + assert(Aborts[TestError2].run(layer.run(effect3)) == Left(TestError2("dep2 failure"))) + } + + val triesToAbortsLayer = Tries.layer(err => Aborts[Throwable].fail(err)) + val triesToOptionsLayer = Tries.layer(_ => Options.get(None)) + + "Tries layer should handle tries as other failures" in { + val effect = for { + _ <- Tries.fail("fail") + } yield () + + val effectHandledToAborts = triesToAbortsLayer.run(effect) + val effectHandledToOptions = triesToOptionsLayer.run(effect) + + assert { + Aborts[Throwable].run(effectHandledToAborts) match { + case Left(err: Throwable) => err.getMessage == "fail" + case _ => false + } + } + assert(Options.run(effectHandledToOptions) == None) + } + + val optionsToAbortsLayer = Options.layer(Aborts[String].fail("missing value")) + val optionsToTriesLayer = Options.layer(Tries.fail("missing value")) + + "Options layer should handle None as other failures" in { + val effect = for { + _ <- Options.get(None) + } yield () + + val effectHandledToAborts = optionsToAbortsLayer.run(effect) + val effectHandledToTries = optionsToTriesLayer.run(effect) + + assert { + Aborts[String].run(effectHandledToAborts) match { + case Left("missing value") => true + case _ => false + } + } + + assert { + Tries.run(effectHandledToTries) match { + case Failure(err: Throwable) => err.getMessage == "missing value" + case _ => false + } + } + } + +}