-
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
Conversation
Addresses issue #153 |
|
||
object layers { | ||
|
||
trait Layer[Sin, Sout] { self => |
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.
It was a bit counterintuitive to me that the effects that the layer can handle is "out" and effects it can produce is "in". I image that's thinking of the operations in the pending effect set. What do you think about flipping the meaning of the type parameters? I think it's more intuitive to think of a Layer
like an effect-level arrow that takes effects to handle and produces new effects if needed.
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.
Yeah I agree. How about Layer[Handled, S]
or something along those lines, where Handled
are the effects that are handled and S
are the new unhandled effects?
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.
Maybe Input
/Output
to make it more explicit?
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.
It's now Layer[In, Out]
, where In
is the intersection of all effects the layer will handle, and Out
is the intersection of the effects that will remain to be handled.
trait Layer[Sin, Sout] { self => | ||
def run[T, S](effect: T > (S with Sout))(implicit fl: Flat[T]): T > (S with Sin) | ||
|
||
final def ++[Sin1, Sout1](other: Layer[Sin1, Sout1]): Layer[Sin with Sin1, Sout with Sout1] = |
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'd like to avoid symbolic operators in Kyo. Maybe we could use add
for ++
and andThen
for >>>
?
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.
Changed ++
to add
and >>>
to chain
. I'm avoiding andThen
for now, since that could easily mean what is currently add
(since add
just handles applies the first layer and then the second).
Whether to keep add
as add
and not change it to andThen
will depend on whether it is effectively commutative so long as it does not change the type of T
when it handles an effect.
} | ||
|
||
final def >>>[Sin1, Sout1](other: Layer[Sin1, Sout1])(implicit | ||
ap: ApplyLayer[Sout, Sin1] |
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.
Would it be possible to express the type-level transformations in the method signature itself instead of relying on the implicit mechanism? I imagine it's not the case, just to double check.
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'm not sure. Basically some mechanism is needed to extract the intersection between the effects that one layer handles and the effects that another layer needs to be handled, and keep track of the remaining effects of each. I'm pretty sure I can least simplify and clarify ApplyLayer
, but I can't think of a way to produce the same functionality with just type bounds. I'll give it some more thought though.
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.
sounds good! the current version is ok with me btw
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 wasn't able to figure out an alternative, but I did improve the type class used to chain layers together. Now it's called ChainLayer
, and it's easier to follow because it reverses the ZIO approach. We can now think of it in this way: the output effects of the first layer are the input effects of the second layer. The main function of ChainLayer
, then, is to derive which outputs effects from the first layer remain unhandled.
I had forgotten how joyful OSS can be! It's great how well the layers compose 🤯 Regarding effect handling ordering, the result of the computations can change depending on the order but it seems reasonable to assume that ordering is a decision users can make when building layers. We might need better names for the operators, though. I've been working on a similar mechanism but for another purpose. Users currently need to handle other pending effects before forking a fiber. Since Kyo supports arbitrary effects, it's not easy to "propagate" the effect to the forked computation. For this use case, I need more features than only handling the effect so I created a new
The approach works well for methods that don't return a def parallel[T, S](j: Joins[S])(l: Seq[T > (S with Fibers)])(
implicit f: Flat[T > Fibers]
): Seq[T] > (S with Fibers) =
j.save.map { st =>
j.handle(st, l).map(parallel(_)).map(j.resume)
} But it doesn't work well for def init[T, S](j: Joins[S])( /*inline*/ v: => T > (S with Fibers))(implicit
f: Flat[T > (S with Fibers)]
): Fiber[j.M[T]] > (S with IOs) =
j.save.map { st =>
init(j.handle(st, v))
} I wonder if we could try to unify the joins and layers. The joins impl is in this branch: https://github.com/getkyo/kyo/blob/joins |
Regarding the build failure, the issue is compilation in Scala 2. The macro for |
Re: the build failure, when I run |
I suspect that def handle[T, S](
s: State,
v: T > (E with S)
)(implicit
f: Flat[T > (E with S)]
): M[T] > (S with IOs) would become def handle[T, S](
s: State,
v: T > (E with S)
)(implicit
f: Flat[T > (E with S)]
): M[T] > (S with Unhandled) In the case of using it for joins, If we went this route, what you are calling trait HandleArrow[E, Unhandled] { self =>
type M[_]
type State
def save: State > E
def handle[T, S](
s: State,
v: T > (E with S)
)(implicit
f: Flat[T > (E with S)]
): M[T] > (S with Unhandled)
// Not sure about this one...
def resume[T, S](v: M[T] > S): T > (E with S) In this case I think type Layer[E, Unhandled] = HandleArrow[E, Unhandled] { type M[X] = Id[X]; type State = Unit }
type Join[E] = HandleArrow[E, IOs] For the use cases I have in mind, it is important that |
I get the same error as the CI when I run |
I think the build error is resolved: I had not defined the implicit |
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 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.
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.
Done. Agreed that this is clearer.
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) |
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.
Other than the two last minor comments, the change LGTM! Reasoning about Kyo's effect handling and how the layers are composed, I can't see potential issues. I normally try to design effects in Kyo by breaking up problems into smaller reusable pieces that handle specific behaviors. The use case for Regarding ordering, I think if we have layers only for Thanks for the contribution! This will be the first meta-effect in Kyo, it's nice to see how it generalized from ZIO's version :) |
I have renamed |
Ok I think it's good to go. This was fun! (This is my first significant OSS contribution, by the way. Prior to this, I've only made one trivial change to zio-http). |
Nice!! You are definitely off to a good start 🙏 |
Introduces new type
Layer[Sin, Sout]
, representing a handler ofSout
that requires further handling ofSin
and that cannot transform the output of the handled effect. The latter restriction allows for commutative composition, so that layers can be combined in any order without affecting (as far as I can tell) the resulting effect.Layer
provides a mechanism for dependency injection, by allowing you to composeEnvs
handlers. It should be possible to add a function similar to ZIO's.provide
, which automatically wires layers together to satisfy all dependencies.However,
Layer
can be used more broadly as a means to simplify certain kinds of effect handling. For instance, it can be used to between different kinds of failures.