-
Notifications
You must be signed in to change notification settings - Fork 279
Add transform and flip methods. #1864
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 transform and flip methods. #1864
Conversation
Will be adding tests for this as well. Just wanted to get this in early for a review. |
The suggestion by |
🙄 Codacy. Feel free to ignore it's output if it's about something silly. I feel like there are many, many times where I need to disregard one of its suggestions because it's not actionable. |
Looks like you removed the only |
Thanks @Shadowfiend. I'll add the tests for About the |
< rubs eyes > lol, guess I'd missed that! Need to make a note to myself to change that to use |
As I'm working on lift-formality, it occurred to me it could also be particularly useful to add 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.
Couple of quick things.
@@ -152,14 +152,17 @@ class BoxSpec extends Specification with ScalaCheck with BoxGenerator { | |||
Full(Empty).flatten must_== Empty | |||
} | |||
} | |||
"define a 'mapFailure' method returning itself." in { | |||
Full(1).mapFailure(identity) must_== Full(1) |
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.
Even if mapFailure
didn't map just over a failure, this would return Full(1)
. How about a parameter that returns some standard Failure
?
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 did that get that niggling feeling when I wrote this. You are right. That could be confusing. I'll change it to something more appropriate.
@@ -298,14 +301,17 @@ class BoxSpec extends Specification with ScalaCheck with BoxGenerator { | |||
"define a 'flatten' method returning Empty" in { | |||
Empty.flatten must beEmpty | |||
} | |||
"define a 'mapFailure' method returning itself." in { | |||
Empty.mapFailure(identity) must beEmpty |
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 re: above.
"return a itself when asked for its status with the operator ?~" in { | ||
"define a 'mapFailure' method that transforms it into another Failure instance." in { | ||
val exception = new Exception("transformed") | ||
Failure("error", Empty, Empty) mapFailure { _ => Failure("new-error", Full(exception), Empty) } must_=== Failure("new-error", Full(exception), Empty) |
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.
Any particular need for ===
here?
I'll do that.
I'll push an implementation for review. |
…h Empty or Full. Replace must_=== with must_==
if (pf.isDefinedAt(value)) Full(pf(value)) | ||
else Empty) | ||
} | ||
final def collect[B](pf: PartialFunction[A, B]): Box[B] = filter(pf.isDefinedAt).map(pf) |
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.
@Shadowfiend thought this was more readable than
final def collect[B](pf: PartialFunction[A, B]): Box[B] = flatMap { value =>
pf.andThen(Full.apply).applyOrElse(value, _ => Empty)
}
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 do think this version will result in a double-application of the PartialFunction
guards/pattern matches (a problem that the previous version in the repo also shared):
@ def slap(in: String) = println(s"Uh-oh ${in}"); false
defined function slap
@ val booyan: PartialFunction[String, Int] = { case "boom" if slap("jam") => 5; case _ => 8 }
booyan: PartialFunction[String, Int] = <function1>
@ Full("boom").filter(booyan.isDefinedAt).map(booyan)
Uh-oh jam
Uh-oh jam
res13: Box[Int] = Full(8)
@ booyan.andThen(Full.apply).applyOrElse("boom", { _: String => Empty })
Uh-oh jam
res12: Box[Int] = Full(8)
So while I agree this is slightly more readable, I think you nailed it with the original alternative formulation.
@Shadowfiend just pushed an initial version of |
final def collectFailure[B](pf: PartialFunction[Failure, Failure]): Box[Failure] = this match { | ||
case f: Failure if pf.isDefinedAt(f) => Full(pf(f)) | ||
case _ => Empty | ||
} |
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 feels like this should be a PartialFunction[Failure, B]
and result in a Box[B]
, no?
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.
That was my first instinct as well. Almost sent a comment asking about these two signatures. I think I'll go with that one then. Now that you said it, it makes more sense. Convert specific cases of Failure into something else.
Add tests and comments
@Shadowfiend Never mind, I forgot about the |
@Bhashit |
Wrap a couple of functions in braces. Replace if-else for checking partial function applicability with an apply-or-else. Add a `flip` method to Box
@Shadowfiend added the |
@@ -731,7 +757,7 @@ sealed abstract class Box[+A] extends Product with Serializable{ | |||
|
|||
|
|||
/** | |||
* If the `Box` is `Full`, apply the transform function `f` on the value `v`; | |||
* If the `Box` is `Fuvall`, apply the transform function `f` on the value `v`; |
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.
Whoops!
"define a 'mapFailure' method returning itself." in { | ||
val failure = Failure("new-error", Empty, Empty) | ||
Full(1).mapFailure(_ => failure) must_== Full(1) | ||
} |
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.
This looks like it's a duplicate of one below?
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.
Oh wait jk I just remembered how this spec is structured heh.
"define a 'collectFailure' method that takes a PartialFunction to transform this Failure into something else" in { | ||
"If the partial-function is defined for this Failure, returns a full box containing the result of applying the partial function to it" in { | ||
Failure("The Phantom Menace") collectFailure { case Failure("The Phantom Menace", Empty, Empty) => "Return Of The Jedi" } must_== Full("Return Of The Jedi") | ||
Failure("The Phantom Menace") collectFailure { case Failure("The Phantom Menace", Empty, Empty) => Failure("Attack") } must_== Full(Failure("Attack")) |
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.
So a random interesting observation here tying into your question about making flip
take a PartialFunction
… Now that we've added flip
, collectFailure
is really just a special subcase of flip
… Do we want to keep both? Or do we, as you initially suggested, make flip
take a PartialFunction
, and drop collectFailure
?
Also of interest is that mapFailure
and collectFailure
work very differently despite their very similar names (probably the reason why initially you defined collectFailure
as PartialFunction[Failure,Failure]
instead of PartialFunction[Failure,B]
).
Not sure what the best way forward is, but I'm slightly concerned about these two things… Would be interested in thoughts/ideas before we commit to this setup as it currently stands. Going with mapFailure
+ flip
seems like a good way to handle the common use cases without adding the oddity of collectFailure
.
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 do like flip
that takes a PartialFunction
, in which case, collectFailure
is indeed a special case of flip
. I'll go ahead and make the changes this Saturday.
Also, on a related note, let me know if we later need to adapt these methods (or any existing ones) to the new hierarchy you are implementing. I'd be happy to take that up.
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. I expect this PR will go in before mine, and if that's the case I'll take on the task of pulling it into my refactoring. If mine goes in first, I'll let you take it on :)
Remove `collectFailure` as `flip` covers the case it was written to handle
@Shadowfiend changes pushed. Although, the following case of Empty flip { case Failure("error", Empty, Empty) => "alternative" } This would return |
Just a gentle reminder (not sure if I'm supposed to do that): I think this is ready for review. The only remaining codacy complaint is about the |
You are well within your rights to poke me, yes :) I've been short on time, but will have another look soon. Can you remove the WIP from the PR title? |
This is a very, very fair concern... Enough so that I think it maybe calls into question the idea of giving flip a That point made me think a little bit about This definition of Sorry to come back with yet another “let's rework this yet again”---let me know what you think, and if you have time to look at doing it. If it sounds good to you, I can also make the change and let you review it. Whatever works! I think it would be great to see this land by M2 next week. EDIT to say: |
Sounds good. I'll try to get that in over this weekend. Please don't worry about revising. That's what gets us a good API eventually. |
Absolutely, but I try to keep in mind how long these feedback cycles have been… I appreciate your patience! |
I'm somewhat embarrassed that I didn't spot this until I started working on it: since Box is covariant in type A, and the return type param of PartialFunction is contravariant, we can't use We'll need to use something like: transform[B >: A](transformFn: PartialFunction[Box[A], Box[B]]): Box[B] One caveat with that signature is that, unless What do you think? Any other ideas, or is this the way we want to go? I'm pushing some changes, as I think this might be okay since users will probably know they are doing. (just for review, we can revise as needed) |
Remove the `mapFailure` method as it's convered by the `transform` method. Implement `or` using `transform`. Change `flip` to take a function, instead of a `PartialFunction`
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.
since Box is covariant in type A, and the return type param of PartialFunction is contravariant
ಠ_ಠ
That said, defining it as [B >: A]
is really strictly more flexible than the alternative, so I think that's okay.
* Return this Box if `Full`, or the specified alternative if it is | ||
* empty. Equivalent to `Option`'s `[[scala.Option.orElse orElse]]`. | ||
*/ | ||
def or[B >: A](alternative: => Box[B]): Box[B] = transform { case _: EmptyBox => alternative } |
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.
While this is quite clean, I wonder if we should leave or
as-is. The JVM will be able to turbo-inline the simple definitions of or
, whereas this definition will probably (though not necessarily) be near-impossible to optimize. Given how common it is to call or
, I'm tempted to say we should leave it as-is.
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.
Hmm. Yeah, I didn't think it along those lines. I'll change that back.
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.
Do you think keeping the or
abstract in the base Box
class will make much of a difference? I mean, that way, there's one implementation in Full
and one in EmptyBox
. It sort of seems cleaner.
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 that's fine.
* `ParamFailure` or `Empty`, and `flipFn` is defined for it. Returns `Empty` if this box is `Full`. | ||
* In other words, it "flips" the full/empty status of this Box. | ||
* Transforms this box using the `transformFn`. If `transformFn` is defined for this box, | ||
* returns the result of applying `transformFn` to it. Otherwise, returns this box unchanged. |
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 should at a note here that if you are looking to change the value inside a Full
box, you should really be using map
, and if you're looking to change a Failure
or Empty
into a Full
, you should really be using flip
.
There's enough complexity in Box
that providing some helpful guides in some of this method-specific documentation starts making sense for me. 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.
Yeah, that sounds pretty helpful. Will do so.
Add an additional explanatory comment on `transform` about its intended use
@@ -793,6 +793,10 @@ sealed abstract class Box[+A] extends Product with Serializable{ | |||
* Transforms this box using the `transformFn`. If `transformFn` is defined for this box, | |||
* returns the result of applying `transformFn` to it. Otherwise, returns this box unchanged. | |||
* | |||
* If you want to change the content of a `Full` box, using [[map]] or [[collect]] might be better | |||
* suited to that purpose. If you want to convert an `Empty`, `Failure` or a `ParamFailure` into a | |||
* `Full` box, you should use [[flip]]. |
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 put backticks around flip
, map
, and collect
(in addition to the [[
and ]]
).
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.
Generally, as a part of the review, I would take the words I'd put backticks
to mean that I (as the author) should make that change. But since you approved, I am guessing you are making that change? Let me know if I should do it, and I'll push the change.
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.
Ok, I am 100% sold here. Tremendous work, and thanks for sticking with it. Hard to believe how few lines of code were added… Turned out to be mostly analysis work!
🙇
I think you did the most of the hard work here though. So, a 'thank you' is in order. 🙇♂️ |
Yeah, the request was for you to make the change, but I approved the PR because there's really no reason to block on it :) |
If/when this gets merged, do you need to adapt the new methods in your branch? More specifically, you need an extra hand with that? Otherwise, I'm planning to work on the vanilla JS artifacts this week. |
I'll disentangle my branch, no worries. Have to finish up test coverage in there before starting on that anyway. |
If anyone else wants to have a glance here and sign off, I'm good on merging… I'll probably merge later this week otherwise. |
Bit late, buuuut in we go! |
Tweaked the description and title to show what we ended up with ;) |
Issue #1856
Add transform and flip methods.
transform allows callers to take a Box and, via PartialFunction, turn it into any
other Box. If the PartialFunction fails, the original Box is returned.
flip allows callers to take a Box and, if it is an EmptyBox, flip it into a Full box
with a specific type. If it is a Full, an Empty is returned, flipping the box from
full to empty or vice versa.