-
Notifications
You must be signed in to change notification settings - Fork 47
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
Changes from 10 commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
b332745
Added Layers trait
jiveshungerford b091de7
Added Envs layer factory method
jiveshungerford b5bfcd0
Added Aborts layer factory method
jiveshungerford fa7f2f1
Added Tries layer factory method
jiveshungerford 534b65f
Added Options layer factory method
jiveshungerford 5e4ce17
Added layers test (testing Envs, Aborts, Options and Tries layers
jiveshungerford 551ee8c
Merge branch 'main' into add-layers
jiveshungerford 36334b1
Updated ++/>>> to add/andThen; fixed Flat implicits
jiveshungerford 68b6fd2
Fixed Layer type parameters and order; changed andThen to chain and s…
jiveshungerford a8e09e4
Added two more ChainLayer instances to cover edge cases
jiveshungerford e9b3e87
Changed order of handled effects in Layer.run
jiveshungerford 4861c9e
Renamed add to andThen
jiveshungerford File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
|
||
final def add[Out1, In1](other: Layer[In1, Out1]): Layer[In with In1, Out with Out1] = | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you rename to There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
} | ||
} | ||
|
||
} | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
120 changes: 120 additions & 0 deletions
120
kyo-core/shared/src/test/scala/kyoTest/layersTest.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} | ||
} | ||
|
||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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)
.There was a problem hiding this comment.
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
.There was a problem hiding this comment.
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.