Skip to content

Commit

Permalink
Merge pull request #154 from johnhungerford/add-layers
Browse files Browse the repository at this point in the history
Add Layer trait and Layer factory methods on Envs, Aborts, Tries, and Options
  • Loading branch information
fwbrasil authored Dec 14, 2023
2 parents 0cc10f0 + 4861c9e commit 4fbe334
Show file tree
Hide file tree
Showing 6 changed files with 301 additions and 2 deletions.
17 changes: 16 additions & 1 deletion kyo-core/shared/src/main/scala/kyo/aborts.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import scala.util.Success
import kyo._
import core._
import tries._
import layers._
import scala.util.Try

object aborts {
Expand All @@ -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

Expand Down Expand Up @@ -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
}
}
}

}
11 changes: 10 additions & 1 deletion kyo-core/shared/src/main/scala/kyo/envs.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import izumi.reflect._
import scala.reflect.ClassTag

import kyo.core._
import kyo.layers.Layer

object envs {

Expand All @@ -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]])
Expand Down Expand Up @@ -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 {
Expand Down
131 changes: 131 additions & 0 deletions kyo-core/shared/src/main/scala/kyo/layers.scala
Original file line number Diff line number Diff line change
@@ -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
}
}

}
}

}
12 changes: 12 additions & 0 deletions kyo-core/shared/src/main/scala/kyo/options.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package kyo

import kyo.aborts._
import kyo.layers._

object options {

Expand Down Expand Up @@ -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
}
}
}
}
12 changes: 12 additions & 0 deletions kyo-core/shared/src/main/scala/kyo/tries.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package kyo

import kyo.aborts._
import kyo.layers._

import scala.util._

Expand Down Expand Up @@ -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
}
}
}
}
120 changes: 120 additions & 0 deletions kyo-core/shared/src/test/scala/kyoTest/layersTest.scala
Original file line number Diff line number Diff line change
@@ -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
}
}
}

}

0 comments on commit 4fbe334

Please sign in to comment.