-
Couldn't load subscription status.
- Fork 1.4k
Macro for Ref-backed proxy class #8061
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
Conversation
|
|
||
| def makeImpl[A: c.WeakTypeTag](service: c.Expr[ScopedRef[A]]): c.Expr[A] = { | ||
|
|
||
| val methods = weakTypeOf[A].decls.collect { |
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 be good to proxy val and non-ZIO-returning methods directly, rather than just abort with error.
Imagine:
trait MyService {
val MyConstant = 4
def square(x: Int): Int
def effectful(x: Int): Task[Int] = ZIO.succeed(x * x)
}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.
For this use case, we need an unsafe way to access the underlying service, because ScopedRef#get is an effectful operation.
Possible solutions:
- Unwrap
ScopedRef#getdirectly using Runtime - Replace
ScopedRefwithRef.Atomicfor unsafe get, and let the caller ensure resource safety
What do you think?
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 seems to me that val makes no sense, because val means the value cannot change.
So we are strictly talking about def which can return different values every time.
Rather than support this, I think maybe your existing implementation makes more sense: just provide a good error message explaining why we can only proxy ZIO-returning methods.
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.
If val also be hotswapped, I think val also makes sense.
test("Forwards abstract vals") {
trait Foo { val bar: UIO[String] }
val service1: Foo = new Foo { val bar: UIO[String] = ZIO.succeed("zio1") }
val service2: Foo = new Foo { val bar: UIO[String] = ZIO.succeed("zio2") }
for {
ref <- ScopedRef.make[Foo](service1)
proxy = Proxy.generate(ref)
res1 <- proxy.bar
_ <- ref.set(ZIO.succeed(service2))
res2 <- proxy.bar
} yield assertTrue(res1 == "zio1" && res2 == "zio2")
}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.
As @SHSongs mentioned, forwarding ZIO vals still makes sense thanks to referential transparency.
Non-ZIO vals can cause trouble as you pointed out, even if it's concrete.
I'd suggest:
- Forwards ZIO vals as the test case above shows
- Reject non-ZIO abstract vals
- Warns about concrete non-ZIO vals that it might cause unexpected behavior
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 would be inclined to just reject all methods not returning a ZIO value. The layer constructing a service could provide different implementation of the interface when it is reloaded so I don't think it is safe to expose values that aren't ZIO workflows. Exposing all interfaces as ZIO workflows is idiomatic anyway and this is special functionality we are adding to improve ergonomics in common situations so I think it is fine to be opinionated.
5887c5c to
fdfb6de
Compare
fdfb6de to
65d610e
Compare
|
Added initial working version for Scala 3. TODO:
/cc @SHSongs |
| private object ProxyMacros { | ||
|
|
||
| @experimental | ||
| def makeImpl[A <: AnyRef : Type](service: Expr[ScopedRef[A]])(using Quotes): Expr[A] = { |
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.
Should be @experimental due to Symbol.newClass and ClassDef.apply.
| import scala.annotation.experimental | ||
| import zio.test._ | ||
|
|
||
| @experimental |
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 breaks cross compilation currently. @SHSongs
| @@ -0,0 +1,3 @@ | |||
| package zio | |||
|
|
|||
| object Proxy extends ProxyVersionSpecific | |||
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.
There will probably be a better place than zio.Proxy.
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.
Let's call it zio.ServiceProxy because it's specifically a type of proxy for service interfaces (also the name Proxy is so generic, it will lead to clashes in other name spaces).
ec5a106 to
6ae04f2
Compare
6f399a6 to
edd9646
Compare
|
@jdegoes Also, I have a question about the "keep non-ZIO default implementations" test case. Should default implementations be kept for concrete methods, or should they raise an error?" |
| test("keeps non-ZIO default implementations") { | ||
| trait Foo { | ||
| def bar: UIO[String] | ||
| def qux: String = "quux" |
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 think this is correct behavior for things like constants. Could be extra strict and require they be final for bulletproof semantics.
|
Excellent work! Thank you for this amazing pull request! 🎉 |
/claim #7556
I wrote a draft for Scala 2, and I plan to follow up with Scala 3.
co-authored with @guersam