-
Notifications
You must be signed in to change notification settings - Fork 280
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
Float Like A Butterfly: Introduce PresenceBox, TryBox, and ParamTryBox. #1876
base: main
Are you sure you want to change the base?
Conversation
These three intermediary types allow APIs and consumers of boxes to indicate that they expect a particular subset of Box types. This is useful to restrict types to what is expected, while still being able to interoperate with APIs designed to interact with many all Box types. For example, you can declare that a function expectes a PresenceBox to ensure that interactors guarantee either a Full or Empty is passed. Or, you can specify that your function returns a TryBox so callers only need to worry about handling a Full or a Failure. Lastly, ParamTryBox allows indicating that a given situation will only get a Full or a ParamFailure with a particular error type. A lot of tests and additional API conversions are required to fully bring this vision to fruition, but this is the foundation for that work.
This allows us to make sure that we don't lose the specific type of the Box when doing a pass operation. This was less important when the only supertype was `Box`, but now that we can have the more specific `PresenceBox`, `TryBox`, and `ParamTryBox`, preserving those supertypes is more important.
Ah, looks like I was a bit late in pushing these changes. I did have most of it, was stuck with a serializer and didn't get enough time for that one. Looking great. 🎉 |
Ugh, sorry @Bhashit… I kind of did this all in one day, and in my head you were working on the combinators but not the typing changes. Re-reading the original issue, I had no reason to think that, and should have let you know I was going to put pedal to the metal on cranking something out. |
In particular, collect and collectFirst will always return a PresenceBox when invoked on a PresenceBox. This is not the case for the other subtypes.
In particular, cases where each of these can guarantee a subtype of Box have reimplementations that do so. PresenceBox also adds a specialization of withFilter, which is used in for comprehensions, that ensures that typing carries through.
These can now have more specific types.
Also added a blurb to the Box scaladocs regarding the Box subtypes.
No problem at all. I am pretty sure you'd do it better.
Sounds good. |
Holding off on review here until the tests are passing, but this looks exciting. :) |
Tests still are not passing. :( |
Well no changes have been pushed so that seems perfectly reasonable :p |
BUT I WANT THE FEATURES NOWWWWWWWWWWWWW 👶 |
Due to the way Scala resolves method overloads with implicit parameters, I haven't been able to find a good way of defining that a TryBox that contains a TryBox should flatten to a TryBox, and a PresenceBox that contains a PresenceBox should flatten to a PresenceBox. Unfortunately Scala seems to resolve the overload of flatten based solely on class hierarchy and number of parameters, and then fails the compile if it can't plug the proper implicits in, rather than using the available implicit evidence to choose between overloads. Since we rely entirely on implicit evidence to know what we can do with the contained type of the box, there's no way (that I could find) to express diverging return types based on the contained type :( Instead, TryBox and PresenceBox each have their own flattenTry and flattenPresence methods, respectively, which will only compile if their contents match the container type. This means the caller has to choose the appropriate one to call, but at least it ensures you *can* flatten that way if you really want to.
Sadly I had to jettison a properly-typed I don't like this gotcha one bit :( |
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.
Left some initial remarks throughout.
I'm a bit concerned about the added conceptual complexity here, but that could just be me not having enough coffee yet. Broadly, I think that Box
in its current form is quite easy to understand today. After seeing some of the hoops this code has to jump through to work, I'm wondering whether or not we've made Box
harder to understand conceptually and therefore increased the barrier to entry for using it and contributing to it.
This is one of those moments where I sincerely wish scala.util.Try
supported arbitrary error messages in its constructor, because if it did I would say "Hey, let's not re-invent the wheel here." :/
In any case, the code itself looks like it's moving in the right direction for a workable implementation. :)
def or[B >: A](alternative: => Full[B]): Full[B] | ||
def or[B >: A](alternative: => PresenceBox[B])(implicit a: DummyImplicit): PresenceBox[B] | ||
def or[B >: A](alternative: => TryBox[B])(implicit a: DummyImplicit, b: DummyImplicit): TryBox[B] | ||
def or[B >: A, E](alternative: => ParamTryBox[B, E])(implicit a: DummyImplicit, b: DummyImplicit, c: DummyImplicit): ParamTryBox[B, E] |
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 idly wondering if the presence of these DummyImplicit
s might be a sign we've tried to be a bit too clever by incorporating this into the overall box structure instead of having them as a parallel data type with implicit conversions.
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.
Parallel data types with implicit conversions are only an option for a small subset of these methods, because of the fact that some of the methods overlap with Iterable and such, which means if someone imports Box._
(which is not an unreasonable thing to do in a few situations), they'll be unable to get our implicits at a higher specificity than the iterable conversions.
It ain't fun.
That said, these dummy implicits don't have anything to do with the way in which we attach these methods; instead, they are here because we're trying to overload on an parameter type with a call-by-name parameter. Overloading on parameter type is directly supported in the JVM, but by making these by-name we turn them into parameterized Function1
s, or something roughly equivalent. That means we're suddenly overloading by the parameter's generic type, which isn't directly supported. Dummy implicits step in to fill this gap.
|
||
override def map[B](f: A => B): PresenceBox[B] = Empty | ||
def flatMap[B](f: A => PresenceBox[B]): PresenceBox[B] | ||
override def collect[B](pf: PartialFunction[A, B]): PresenceBox[B] = ??? |
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.
We should remove the presence of ???
from anything that'll be production code.
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'll double-check this. I introduced it for a reason, but it may have been an earlier iteration.
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.
Worth noting though---neither of the ???
implementations are used in practice, because they are overridden further in all of their subclasses, and dynamic binding ensures it's those implementations, not these, that are executed.
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'm not concerned about that. More about accident proofing the code. If there is a ???
implementation here, the compiler would say, for example, "Hey this interface is satisfied!" If someone were to somehow break the override of a child implementation, this would become the used implementation.
Whereas, if we don't provide ???
here, breaking the override implementation should break the compile.
*/ | ||
sealed trait TryBox[+A] extends Box[A] { | ||
def or[B >: A](alternative: => Full[B]): Full[B] | ||
def or[B >: A](alternative: => PresenceBox[B])(implicit a: DummyImplicit): PresenceBox[B] // * |
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.
Rouge comment on this line?
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.
Tru.
def or[B >: A](alternative: => TryBox[B])(implicit a: DummyImplicit, b: DummyImplicit): TryBox[B] | ||
def or[B >: A, E2](alternative: => ParamTryBox[B, E2])(implicit a: DummyImplicit, b: DummyImplicit, c: DummyImplicit): ParamTryBox[B, E2] | ||
|
||
override def map[B](f: A => B): ParamTryBox[B, E] = ??? |
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.
Ditto here with ???
We've definitely increased complexity here, but a lot of the hoops we jump through here are to make the intuition of this added complexity work as expected. Specifically, we added With that said, it definitely increases the barrier to entry for contributing to
I definitely understand the complexity concern here though, so if we collectively think this is too much, I'm happy to leave it on the table. |
I think the complexity concern here is probably worth it given what we're getting. And I can't think of any way to simplify things significantly. |
FWIW, I'd love having this new hieararchy. The pattern matches with |
Ok, so slight complication here which we'll need to decide if we're okay with… In a sense, this branch is too effective heh. We can change An example is this chunk of framework/web/webkit/src/main/scala/net/liftweb/builtin/snippet/WithParam.scala Lines 44 to 58 in aa24c2d
EDIT to say: I think this is still a big step up, and that given the relatively benign nature of the compilation errors and that they happen at compile time (rather than runtime), this is acceptable. |
The existing definition wasn't actually catching for all ParamTryBoxes.
Also add some scaladocs to those two methods.
We do this by dropping the concrete definitions of these two methods in Box, preferring instead to lean 100% on late binding to resolve the correct concrete definition of the method based on the concrete type it is being invoked on.
Ok, wrapped up the specs, made a fix or two, nuked the I think I consider this PR good to go, provided travis doesn't slap me repeatedly in the face with red ❌es. |
Ohai Scala 2.11. Looks like still some cleanup to be done. Will circle back! |
Don’t know what I was thinking. Minor releases are API-compatible, by definition. So... that’s awkward heh. Will noodle. |
Moved this to hypothetical 4.0 for now… Really bummed about it though :( |
(May be worth discussing if this is worth doing an earlier-than-expected 4.0 release with the community… Perhaps after another couple of minor releases.) |
Hey there, I'm closing PR as orphaned since it's been more than a year since it's had any updates. Feel free to reopen when you have time to update it! |
Not much to update here, tbh. This PR was left open because it can't land before a 4.0 release; it may have some merge conflicts but I suspect I'd be able to dispense with them when the time comes. |
Hm, gotcha. I can reopen it. |
Also converted it to a Draft so we know it's not an "actually actionable thing" right now |
These three intermediary types allow APIs and consumers of boxes to indicate
that they expect a particular subset of
Box
types. This is useful to restricttypes to what is expected, while still being able to interoperate with
APIs designed to interact with many all
Box
types.For example, you can declare that a function expectes a
PresenceBox
to ensurethat interactors guarantee either a
Full
orEmpty
is passed. Or, you canspecify that your function returns a
TryBox
so callers only need to worry abouthandling a
Full
or aFailure
. Lastly,ParamTryBox
allows indicating that agiven situation will only get a
Full
or aParamFailure
with a particular errortype.
A lot of tests and additional API conversions are required to fully bring this
vision to fruition, but this is the foundation for that work.
Required before this is ready to merge:
Box.scala
Of note, the compiler will already properly infer, say,
PresenceBox
if yourcode only produces
Full
orEmpty
. It will also inferParamTryBox[String, Int]
,for example, if your code only produces
Full
orParamFailure[Int]
.Hoping I can get this wrapped up in time for 3.2.0-M2, but we'll see ;)