Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Layer trait and Layer factory methods on Envs, Aborts, Tries, and Options #154

Merged
merged 12 commits into from
Dec 14, 2023
Merged
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 > (S with Aborts[E])
)(
implicit flat: Flat[T > (S with Aborts[E])]
): 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 > (S with Envs[E]))(implicit
fl: Flat[T > (S with Envs[E])]
): T > (S with Sd) =
construct.map(e => self.run[T, S](e)(effect: T > (Envs[E] with S)))
}
}

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 > (S with In))(implicit fl: Flat[T > (S with In)]): T > (S with Out)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In Scala 2, methods that remove items from the pending effects need to have the effect to be removed as the first in the type intersection otherwise the compiler isn't able to infer types correctly. You might be able to avoid type annotations if you use effect: T > (In with S).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I pushed this change, although it looks like I still need the type annotations in those places where I've called Layer#run.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. It could be that the compiler isn't able to infer the removal based on the generic type parameter.


final def add[Out1, In1](other: Layer[In1, Out1]): Layer[In with In1, Out with Out1] =
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you rename to andThen? I think it'd be better to communicate that order is important.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Agreed that this is clearer.

new Layer[In with In1, Out with Out1] {
override def run[T, S](
effect: T > (S with In with In1)
)(
implicit fl: Flat[T > (S with In with In1)]
): T > (S with Out with Out1) = {
val selfRun: T > (S with In1 with Out) =
self.run[T, S with In1](effect: T > (S with In1 with In))
val otherRun: T > (S with Out with Out1) =
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 > (S with In1))(implicit
fl: Flat[T > (S with In1)]
): 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 > (S with In1))(implicit
fl: Flat[T > (S with In1)]
): 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 > (S with In1))(implicit
fl: Flat[T > (S with In1)]
): 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 > (S with In1))(implicit
fl: Flat[T > (S with In1)]
): T > (S with Out2) = {
val handled1: T > (S with Out) = 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 > (S with Options))(implicit
fl: Flat[T > (S with Options)]
): 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 > (S with Tries))(implicit
fl: Flat[T > (S with Tries)]
): 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 add dep2Layer add 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 add dep1ToTE1Layer add 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
}
}
}

}
Loading