Skip to content

Add transform and flip methods. - #1864

Merged
Shadowfiend merged 15 commits into
lift:masterfrom
Bhashit:issue-1856-box-combinator-and-other-improvements
Sep 25, 2017
Merged

Add transform and flip methods.#1864
Shadowfiend merged 15 commits into
lift:masterfrom
Bhashit:issue-1856-box-combinator-and-other-improvements

Conversation

@Bhashit

@Bhashit Bhashit commented Jun 17, 2017

Copy link
Copy Markdown
Contributor

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.

@Bhashit

Bhashit commented Jun 17, 2017

Copy link
Copy Markdown
Contributor Author

Will be adding tests for this as well. Just wanted to get this in early for a review.

@Bhashit

Bhashit commented Jun 18, 2017

Copy link
Copy Markdown
Contributor Author

The suggestion by codacy is about the fact that mapFailure includes the word Failure, which is also the name of one of the classes that has that method.

@farmdawgnation

Copy link
Copy Markdown
Member

🙄 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.

@Shadowfiend

Copy link
Copy Markdown
Member

Looks like you removed the only collect method, not a duplicate? mapFailure looks sweet.

@Bhashit

Bhashit commented Jun 25, 2017

Copy link
Copy Markdown
Contributor Author

Thanks @Shadowfiend. I'll add the tests for mapFailure today.

About the collect method: I think I removed the duplicate collect method I had added. We already have an implementation here.

@Shadowfiend

Copy link
Copy Markdown
Member

About the collect method

< rubs eyes > lol, guess I'd missed that! Need to make a note to myself to change that to use applyOrElse.

@Shadowfiend

Copy link
Copy Markdown
Member

As I'm working on lift-formality, it occurred to me it could also be particularly useful to add a collectFailure, since pattern matching is used heavily when dealing with ParamFailure. Thoughts?

@Shadowfiend Shadowfiend left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couple of quick things.

}
}
"define a 'mapFailure' method returning itself." in {
Full(1).mapFailure(identity) must_== Full(1)

Copy link
Copy Markdown
Member

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?

@Bhashit Bhashit Jun 26, 2017

Copy link
Copy Markdown
Contributor Author

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.

Empty.flatten must beEmpty
}
"define a 'mapFailure' method returning itself." in {
Empty.mapFailure(identity) must beEmpty

Copy link
Copy Markdown
Member

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)

Copy link
Copy Markdown
Member

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?

@Bhashit

Bhashit commented Jun 26, 2017

Copy link
Copy Markdown
Contributor Author

Need to make a note to myself to change that to use applyOrElse

I'll do that.

it could also be particularly useful to add a collectFailure, since pattern matching is used heavily when dealing with ParamFailure. Thoughts?

I'll push an implementation for review.

if (pf.isDefinedAt(value)) Full(pf(value))
else Empty)
}
final def collect[B](pf: PartialFunction[A, B]): Box[B] = filter(pf.isDefinedAt).map(pf)

@Bhashit Bhashit Jun 26, 2017

Copy link
Copy Markdown
Contributor Author

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)
}

Copy link
Copy Markdown
Member

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.

@Bhashit

Bhashit commented Jul 9, 2017

Copy link
Copy Markdown
Contributor Author

@Shadowfiend just pushed an initial version of collectFailure. If it looks good, I'll add some comments and tests, and if it doesn't, I'll make the necessary changes.

final def collectFailure[B](pf: PartialFunction[Failure, Failure]): Box[Failure] = this match {
case f: Failure if pf.isDefinedAt(f) => Full(pf(f))
case _ => Empty
}

Copy link
Copy Markdown
Member

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?

@Bhashit Bhashit Jul 10, 2017

Copy link
Copy Markdown
Contributor Author

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.

@Bhashit

Bhashit commented Jul 10, 2017

Copy link
Copy Markdown
Contributor Author

@Shadowfiend I think this might be ready for review. I.e. now it contains all the methods that I set out to add, along with their respective tests.

Never mind, I forgot about the flip method that you mentioned. Will add that one tonight.

@Shadowfiend

Copy link
Copy Markdown
Member

@Bhashit flip can be a separate PR if you'd like. Up to you.

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
@Bhashit

Bhashit commented Jul 23, 2017

Copy link
Copy Markdown
Contributor Author

@Shadowfiend added the flip method. And corrected the collect implementation. It turns out that Box has a couple of apply implementations that take a PartialFunction and do what we need. So, used those. And used applyOrElse in those existing methods. The remaining codacy complaint is about the method mapFailure having "Failure" in its name.


/**
* 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`;

Copy link
Copy Markdown
Member

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)
}

Copy link
Copy Markdown
Member

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?

Copy link
Copy Markdown
Member

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"))

Copy link
Copy Markdown
Member

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.

@Bhashit Bhashit Jul 27, 2017

Copy link
Copy Markdown
Contributor Author

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.

Copy link
Copy Markdown
Member

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
@Bhashit

Bhashit commented Aug 6, 2017

Copy link
Copy Markdown
Contributor Author

@Shadowfiend changes pushed. Although, the following case of flip did make me somewhat uncomfortable.

Empty flip { case Failure("error", Empty, Empty) => "alternative" }

This would return Empty. The thing that bothered me was that flip sounds definitive, whereas collect has a well-known meaning that it will only apply in some cases. Although, I think that should be okay.

@Bhashit

Bhashit commented Sep 3, 2017

Copy link
Copy Markdown
Contributor Author

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 Failure having a method mapFailure ("You should not name methods after their defining object").

@Shadowfiend

Copy link
Copy Markdown
Member

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?

@Bhashit Bhashit changed the title Add collect and mapFailure methods. WIP Add collect and mapFailure methods. Sep 6, 2017
@Shadowfiend

Shadowfiend commented Sep 6, 2017

Copy link
Copy Markdown
Member

Although, the following case of flip did make me somewhat uncomfortable.

This is a very, very fair concern... Enough so that I think it maybe calls into question the idea of giving flip a PartialFunction.

That point made me think a little bit about mapFailure as well. The trouble with that name is that it implies a tie to map, but map deals with the contents of a Box, not with the Box itself. I'm wondering if we tilt this a little bit… We keep flip as a total function that can flip EmptyBox into Full. And we introduce def transform[A](transformFn: PartialFunction[Box[A], Box[A]): Box[A]. transform can transform the box in any way it wants, as long as the contained value type remains unchanged (to change the contained value type you'd have to use collect or map).

This definition of transform encompasses what we currently consider mapFailure (by simply passing a PartialFunction[Failure, Failure]). It also encompasses the PartialFunction variant of flip. And it leaves flip to be as definitive as it sounds.

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: transform basically also acts as a generalized form of or---or can be defined as transform { case _: EmptyBox => other }. It essentially allows some introspective manipulation for the box along a transformation chain.

@Bhashit

Bhashit commented Sep 7, 2017

Copy link
Copy Markdown
Contributor Author

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.

@Shadowfiend

Copy link
Copy Markdown
Member

Absolutely, but I try to keep in mind how long these feedback cycles have been… I appreciate your patience!

@Bhashit

Bhashit commented Sep 10, 2017

Copy link
Copy Markdown
Contributor Author

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 PartialFunction[Box[A], Box[A]] here.

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 A and B are the same type, the returned box will be some common super-type of both (and in may cases, that turns out to be Any or AnyRef).

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`

@Shadowfiend Shadowfiend left a comment

Copy link
Copy Markdown
Member

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]]`.
*/

Copy link
Copy Markdown
Member

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.

Copy link
Copy Markdown
Contributor Author

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.

@Bhashit Bhashit Sep 11, 2017

Copy link
Copy Markdown
Contributor Author

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.

Copy link
Copy Markdown
Member

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.

Copy link
Copy Markdown
Member

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?

Copy link
Copy Markdown
Contributor Author

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
*
* 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]].

Copy link
Copy Markdown
Member

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 ]]).

Copy link
Copy Markdown
Contributor Author

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.

@Shadowfiend Shadowfiend left a comment

Copy link
Copy Markdown
Member

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!

🙇

@Bhashit

Bhashit commented Sep 11, 2017

Copy link
Copy Markdown
Contributor Author

I think you did the most of the hard work here though. So, a 'thank you' is in order. 🙇‍♂️

@Shadowfiend

Copy link
Copy Markdown
Member

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 :)

@Bhashit

Bhashit commented Sep 11, 2017

Copy link
Copy Markdown
Contributor Author

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.

@Shadowfiend

Copy link
Copy Markdown
Member

I'll disentangle my branch, no worries. Have to finish up test coverage in there before starting on that anyway.

@Shadowfiend Shadowfiend added this to the 3.2.0-M3 milestone Sep 17, 2017
@Shadowfiend

Copy link
Copy Markdown
Member

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.

@Shadowfiend

Copy link
Copy Markdown
Member

Bit late, buuuut in we go!

@Shadowfiend
Shadowfiend merged commit 50a556c into lift:master Sep 25, 2017
@Shadowfiend Shadowfiend changed the title Add collect and mapFailure methods. Add transform and flip methods. Sep 25, 2017
@Shadowfiend

Copy link
Copy Markdown
Member

Tweaked the description and title to show what we ended up with ;)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants