Skip to content
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 mapOrKeep to Functor #4582

Merged
merged 1 commit into from
May 1, 2024
Merged

Add mapOrKeep to Functor #4582

merged 1 commit into from
May 1, 2024

Conversation

jozic
Copy link
Contributor

@jozic jozic commented Apr 4, 2024

I've found myself (and others) repeating this over and over, and it seems as primitive as void or as, so would be nice to have it here. If this looks fine, i can also add flatMapOrKeep to Monad in another PR

@jozic jozic marked this pull request as ready for review April 4, 2024 02:53
@satorg
Copy link
Contributor

satorg commented Apr 4, 2024

Thank you for the contribution!
It seems that the suggested function is supposed to shorten expressions like this one below:

fa.map {
 case `cond_a1` => calc_a1
 ...
 case `cond_aN` => calc_aN
 case default => default
}

into something like this:

fa.alter {
 case `cond_a1` => calc_a1
 ...
 case `cond_aN` => calc_aN
}

So basically it should help to avoid the "default" case, would it be an accurate assumption?

I feel that since it is basically a map but with an additional feature, then its name would better look like mapOrSomething. Maybe mapOrDefault?

Sorry for bikeshedding but I feel that names like alter would be difficult to discover – naturally, there's no alteration implied.

@jozic
Copy link
Contributor Author

jozic commented Apr 5, 2024

So basically it should help to avoid the "default" case, would it be an accurate assumption?

yep, that's correct

I feel that since it is basically a map but with an additional feature, then its name would better look like mapOrSomething. Maybe mapOrDefault?
Sorry for bikeshedding but I feel that names like alter would be difficult to discover – naturally, there's no alteration implied.

Don't be sorry :) you concern makes sense. I also wanted to use something like mapSomething, but my main concern with that is that it's more limited than map because of A1 >: A constraint. And given, that it seems to me that it's not really mapping A, but rather transforming (or altering) it. So that's why to me alter seems like a good name here, but it indeed can be harder to discover.
As an alternative, how about mapWhen?
it's gonna read as
assert(l.mapWhen { case i if i % 2 == 0 => i + 1 } === l.map(i => if (i % 2 == 0) i + 1 else i))

@jozic jozic force-pushed the functor.alter branch 3 times, most recently from 0c12d73 to 6cc5ddf Compare April 5, 2024 22:29
@jozic jozic changed the title Add alter to Functor Add mapOrKeep to Functor Apr 5, 2024
@jozic jozic force-pushed the functor.alter branch 2 times, most recently from ab4b7af to a041a94 Compare April 6, 2024 02:09
johnynek
johnynek previously approved these changes Apr 6, 2024
@jducoeur
Copy link
Member

jducoeur commented Apr 8, 2024

The name map feels wrong to me here. I expect map to be total, and this isn't.

My gut says that the correct name here is collectOrKeep -- in general, I expect PartialFunction to go with collect.

@jozic
Copy link
Contributor Author

jozic commented Apr 8, 2024

Just for the record: my original name was alter, then the rest of the folks who kindly reviewed this more or less agreed on mapOrKeep. Which also makes sense to me, but I don't agree this has something in common with collect (except using PF). Oscar earlier pointed out that we don't want someone to confuse this new method with some version of collect, which I agree with.

Comment on lines 49 to 57
forAll { (l: List[Int], o: Option[Int], m: Map[String, Int]) =>
assert(l.mapOrKeep { case i if i % 2 == 0 => i + 1 } === l.map(i => if (i % 2 == 0) i + 1 else i))
assert(
o.mapOrKeep { case i if i > 0 => i + 1 } === (if (o.nonEmpty) if (o.get > 0) Some(o.get + 1) else o else None)
)
assert(
m.mapOrKeep { case v if v % 2 == 0 => v + 1 } === m.map { case (k, v) => k -> (if (v % 2 == 0) v + 1 else v) }
)
}
Copy link
Contributor

@satorg satorg Apr 8, 2024

Choose a reason for hiding this comment

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

Although it is a valid test case - no doubt in that, but it is not really necessary to invent a custom test logic there:

    forAll { (l: List[Int], o: Option[Int], m: Map[String, Int], pf: PartialFunction[Int, Number]) =>
      assertEquals(l.mapOrKeep(pf), l.map(x => pf.applyOrElse(x, _ => x: Integer))
      // same for `o: Option[Int]` and `m: Map[String, Int]`
    }

It would be way clearer and more generic.
Also:

  1. PartialFunction[Int, Number] so it check that the A1 >: A constraint holds too.
  2. I would probably go with assertEquals instead of just assert because the former provides better reporting in a case of test failures.

That said, I feel that this equivalence could be just a part of the FunctorLaws:

Functor[A].mapOrKeep(fa)(pf) <-> Functor[A].map(fa)(a => pf.applyOrElse(a, _ => a: A1)

If we could add such a rule to the laws, we would not need to check for special cases individually – all possible implementations would be got checked automatically.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@satorg i guess I'm missing something again, but Int is not a subtype of Number
type arguments [Int,Number] do not conform to method m's type parameter bounds [A,A1 >: A]

Copy link
Contributor

Choose a reason for hiding this comment

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

Correct, Int is definitely not, but Integer is.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

But there are no generators for Integer. As I mentioned in another comment, I didn't find any examples of A and A1 with A1 >: A

Copy link
Contributor

@satorg satorg Apr 15, 2024

Choose a reason for hiding this comment

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

Well, I thought that since there's a generator for Int then it wouldn't be too difficult to get one for Integer via boxing.

But on the second thought, maybe it's not really worth the hassle – perhaps PartialFunction[Int, Int] should be just enough for this sort of test.

Besides, if some method needs tests for syntax correctness (which may also include type correspondence/variance testing), then the SyntaxSuite could be a better bet for that.

def testFunctor[F[_]: Functor, A, B]: Unit = {

Copy link
Contributor Author

Choose a reason for hiding this comment

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

given that we agreed to use PF[A, A] in the law test, seems like we should be covered by it, and it also uses syntax

@danicheg
Copy link
Member

danicheg commented Apr 8, 2024

Continuing bikeshedding on naming, there were several inquiries to consider the collect* name (collectOrKeep / collectOrElse) from the community. In my humble opinion, if we are about to add the new method to Functor — it's much more likely a map operation and therefore mapOrKeep is fine. But if one adds that method to FunctorFilter, I would agree that collectOrKeep better fits. Because FunctorFilter is seemingly for collections, but Functor is a more general abstraction.

@jozic
Copy link
Contributor Author

jozic commented Apr 14, 2024

@danicheg
Do you want to re-review this again?

@jozic jozic requested a review from johnynek April 15, 2024 23:35
Copy link
Contributor

@satorg satorg left a comment

Choose a reason for hiding this comment

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

Looks great to me, thanks!

@jozic
Copy link
Contributor Author

jozic commented Apr 21, 2024

@johnynek could you please quickly re-review this? Thank you!

@jozic
Copy link
Contributor Author

jozic commented May 1, 2024

@danicheg @johnynek
A gentle ping. Thank you.

Is there anyone else who can review this?

@johnynek johnynek merged commit fa61d34 into typelevel:main May 1, 2024
16 checks passed
@johnynek
Copy link
Contributor

johnynek commented May 1, 2024

Thank you for the PR

@jozic jozic deleted the functor.alter branch May 1, 2024 17:23
* res0: List[Int] = List(1, 42, 3)
* }}}
*/
def mapOrKeep[A, A1 >: A](fa: F[A])(pf: PartialFunction[A, A1]): F[A1] = map(fa)(a => pf.applyOrElse(a, identity[A1]))
Copy link
Member

@joroKr21 joroKr21 May 13, 2024

Choose a reason for hiding this comment

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

I thought we could use mapConserve for List, but it works only for AnyRef.
Ideally collections would avoid allocations when we keep.

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.

6 participants