-
Notifications
You must be signed in to change notification settings - Fork 532
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 Defer instances for Decoder and Encoder #2123
base: series/0.14.x
Are you sure you want to change the base?
Add Defer instances for Decoder and Encoder #2123
Conversation
@@ -763,4 +764,6 @@ class DecoderSuite extends CirceMunitSuite with LargeNumberDecoderTestsMunit { | |||
assert(result.isInvalid) | |||
assertEquals(result.swap.toOption.map(_.size), Some(2)) | |||
} | |||
|
|||
checkAll("Defer[Decoder]", DeferTests[Decoder].defer[MiniInt]) |
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.
Can you add some property tests for recursive datastructures like what was used in the example? Just to verify further the stack safety and serve as an example for users on how to user defer?
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 added non-property test, as generating recursive data structures are kind of annoying to write Gen
s for.
Not impossible though, so if you feel strongly that they need to be property tests, I can take a pass at converting them.
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 we need this test to have some depth to it. We need to have the test ensure the stack safety of the implementation of defer. Its probably fine to use List
as the type the way you did, but using a generated list of some significant length should do the trick.
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.
Clarification question: are you concerned about the stack safety of the Defer
instance, or the Decoder
it's used to write?
The former is tested by the Defer
laws:
+ Defer[Decoder]: defer.defer Identity 0.156s
+ Defer[Decoder]: defer.defer does not evaluate 0.001s
+ Defer[Decoder]: defer.defer is stack safe 0.319s
+ Defer[Decoder]: defer.defer matches fix 0.429s
The later I think may be out of our control.
Could we add some documentation as well to the docs for this? You could easily transform what you have in the accommodating issue and it would be great for discoverability of this. |
- Cache the resolution loop so we don't have to do it every time the instance is called. - Preserve short-circuit decoding behavior when not accumulating errors
@morgen-peschke Thanks for adding the tests. It looks like this build failed MIMA for some reason, do you mind taking a look? |
implicit final val decoderInstances | ||
: SemigroupK[Decoder] with MonadError[Decoder, DecodingFailure] with Defer[Decoder] = | ||
new SemigroupK[Decoder] with MonadError[Decoder, DecodingFailure] with Defer[Decoder] { |
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 you'll need to do something like this to fix bincompat
implicit final val decoderInstances | |
: SemigroupK[Decoder] with MonadError[Decoder, DecodingFailure] with Defer[Decoder] = | |
new SemigroupK[Decoder] with MonadError[Decoder, DecodingFailure] with Defer[Decoder] { | |
@deprecated | |
def decoderInstances: MonadError[Decoder, DecodingFailure] = decoderInstancesBinCompat | |
implicit final val decoderInstancesBinCompat = | |
: SemigroupK[Decoder] with MonadError[Decoder, DecodingFailure] with Defer[Decoder] = | |
new SemigroupK[Decoder] with MonadError[Decoder, DecodingFailure] with Defer[Decoder] { |
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.
Alternatively, we could just make a separate val for the Defer
couldn't we? Defer
has no parents and no children so there shouldn't be any real need for it to be part of decoderInstances
right?
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.
Sure, good point!
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.
Also, does the order matter on those with
clauses? Maybe moving Defer[Decoder]
to before MonadError[Decoder, DecodingFailure]
instead of after?
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. But I do know that Scala 2 and Scala 3 have different algorithms for this, so it may be impossible to reconcile in a way to make all versions happy. We had a similar issue in Cats (actually we accidentally broke bincompat on Scala 3 since weren't checking it back then ... )
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 you mind fixing the merge conflics here? |
implicit def uglyListDecoder[A: Decoder]: Decoder[List[A]] = | ||
Decoder.recursive[List[A]] { implicit recurse => | ||
Decoder.instance { c => | ||
(c.downField("car").as[A], c.downField("cdr").as[Option[List[A]]]).mapN(_ :: _.getOrElse(Nil)) |
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 don't think this is stack safe, and it looks like the tests fail.
So, recurse is doing to be used at c.downField("cdr").as[Option[List[A]]]
but that's nested inside of a mapN
. While nesteding Defer
is stack safe in the construction you have here, composing with map2, which is what is happening here, isn't. Something like: lazy val d = map2(a, defer(d))(_ :: _)
, map2
would need to somehow unroll that loop. It's not easy to see how that could happen.
So, I think the stack depth will be the same depth as the object graph.
cats-parse has the same issue on parsing (which is the same basic typeclass as Decoder).
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 you ask me, we should merely add a comment to recursive and remove this test. Many structures have depth = log(size), so they are perfectly safe to parse with this. If you have a linear depth object (such as a linked list), this isn't suitable if the lists are big.
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.
Something like this?
Note: while a
Decoder
written usingDecoder.recursive
can prevent unneeded creation ofDecoder
instances when recursing, the resultingDecoder
cannot be guaranteed to be stack-safe.
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, something like that seems right.
import java.time.ZoneOffset | ||
import java.time.ZonedDateTime | ||
import java.time.format.DateTimeFormatter | ||
import java.time.format.DateTimeFormatter.ISO_LOCAL_DATE_TIME |
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.
personally, I don't like these import reformats. I think grouped imports make it easier to see the package level dependencies and I would rather use { }
to the maximum vs the minimum.
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 don't have strong opinions on it, other than "let the tooling handle it" 😅
In this case, it was the result of running scalafixAll
and scalafmtAll
(then reverting the changes to the files I hadn't touched).
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 can revert the changes manually if you find it particularly objectionable, but it might be worth revisiting the formatting config if that is the case 🤷🏻
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.
Also, in my experience, unfolded imports tend to trigger less conflicts on merges and rebases. I guess this could one of the reasons why it was made a default behavior for the OrganizeImports rule in scalafix.
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 needed to re-run the CI, so I've rolled back the import reformat
Implements #2122