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

Drop noop for Functor unzip with Liskov evidence #3318

Merged
merged 19 commits into from
Feb 6, 2021

Conversation

yangzai
Copy link
Contributor

@yangzai yangzai commented Feb 24, 2020

The main motivation of this PR is to reduce the verbosity of calling unzip.

As mentioned by @kailuowang in #3062 (comment) the $conforms related implicit conversion obscures type signature.

This PR proposes an alternative solution with an implicit Liskov evidence that requires explicit conversions instead.

@travisbrown
Copy link
Contributor

We can't change the signature of the unzip in Functor before 3.0, but we could probably just add an unzip to FunctorSyntax (possibly with a workaround like this if that's necessary)? We've done that in many other cases where Simulacrum didn't work for some reason.

@yangzai
Copy link
Contributor Author

yangzai commented Feb 25, 2020

@travisbrown thanks for the feedback. I'm looking at how it it was implemented here: #3234 . Hope I'm on the same page.

@codecov-io
Copy link

codecov-io commented Feb 25, 2020

Codecov Report

Merging #3318 into master will increase coverage by <.01%.
The diff coverage is 100%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #3318      +/-   ##
==========================================
+ Coverage   93.41%   93.42%   +<.01%     
==========================================
  Files         378      378              
  Lines        7629     7631       +2     
  Branches      206      201       -5     
==========================================
+ Hits         7127     7129       +2     
  Misses        502      502
Flag Coverage Δ
#scala_version_212 93.49% <100%> (ø) ⬆️
#scala_version_213 93.2% <100%> (ø) ⬆️
Impacted Files Coverage Δ
core/src/main/scala/cats/Functor.scala 100% <100%> (ø) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update bce605e...1cac349. Read the comment docs.

Comment on lines 162 to 172
//TODO: Drop from Cats 3.0
@noop
def unzip[A, B](fab: F[(A, B)]): (F[A], F[B]) = (map(fab)(_._1), map(fab)(_._2))

//TODO: Drop `final` when the `noop` version deprecates
// `final` restricts functor instance overriding of `unzip` to the noop version
final def unzip[A, X, Y](fa: F[A])(implicit ev: A <~< (X, Y)): (F[X], F[Y]) = {
val fxy = map(fa)(ev)

(map(fxy)(_._1), map(fxy)(_._2))
unzip(fxy)
}
Copy link
Contributor Author

@yangzai yangzai Feb 25, 2020

Choose a reason for hiding this comment

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

@travisbrown Overloading seems to work as well but I'm not sure if there are any negative implication.

Copy link
Contributor

Choose a reason for hiding this comment

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

@yangzai I think it's a good idea to avoid overloading when it's not absolutely necessary—see e.g. this SO answer for some reasons. Not everything there is still applicable in the latest Scala versions, but some is. For example, this is a nicer error message:

scala> import cats.Functor, cats.instances.all._
import cats.Functor
import cats.instances.all._

scala> Functor[Option].unzip(1)
                             ^
       error: type mismatch;
        found   : Int(1)
        required: Option[(?, ?)]

…than this:

scala> Functor[Option].unzip(1)
<console>:16: error: overloaded method value unzip with alternatives:
  [A, X, Y](fa: Option[A])(implicit ev: A <~< (X, Y))(Option[X], Option[Y]) <and>
  [A, B](fab: Option[(A, B)])(Option[A], Option[B])
 cannot be applied to (Int)
       Functor[Option].unzip(1)
                       ^

If we had to do this to get the syntax method, it'd be different, but I don't think overloading is worth it just to get Simulacrum to generate the syntax method. Implementing it explicitly in FunctorOps is also more consistent with the approach we use in other cases where Simulacrum hasn't done what we needed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@travisbrown Yeah it was a lazy hack. Sorry was kind of busy the past week. Will reference #3234 for the FunctorOps approach if the <~< based type signature is fine going forward.

@codecov-io
Copy link

codecov-io commented Feb 25, 2020

Codecov Report

Merging #3318 into master will increase coverage by 0.01%.
The diff coverage is 100.00%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #3318      +/-   ##
==========================================
+ Coverage   93.32%   93.34%   +0.01%     
==========================================
  Files         378      379       +1     
  Lines        7689     7694       +5     
  Branches      206      206              
==========================================
+ Hits         7176     7182       +6     
+ Misses        513      512       -1     
Flag Coverage Δ
#scala_version_212 93.40% <100.00%> (+0.04%) ⬆️
#scala_version_213 93.12% <100.00%> (+0.01%) ⬆️
Impacted Files Coverage Δ
core/src/main/scala/cats/syntax/functor.scala 100.00% <100.00%> (ø)
core/src/main/scala/cats/instances/either.scala 100.00% <0.00%> (ø)
core/src/main/scala/cats/syntax/either.scala 83.33% <0.00%> (+0.24%) ⬆️
core/src/main/scala/cats/instances/option.scala 100.00% <0.00%> (+1.69%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update fc23079...aff4a51. Read the comment docs.

core/src/main/scala/cats/syntax/functor.scala Outdated Show resolved Hide resolved
core/src/main/scala/cats/syntax/functor.scala Outdated Show resolved Hide resolved
@travisbrown
Copy link
Contributor

Is there any reason not to just write the following?

final class FunctorTupleOps[F[_], A, B](private val fab: F[(A, B)]) extends AnyVal {
  def unzip(implicit F: Functor[F]): (F[A], F[B]) = F.unzip(fab)
}

I just tried it with your tests and everything seems fine.

@yangzai yangzai closed this Mar 11, 2020
@yangzai yangzai reopened this Mar 11, 2020
* }}}
*
*/
def unzip[X, Y](implicit F: Functor[F], ev: A <~< (X, Y)): (F[X], F[Y]) = F.unzip(F.map(fa)(ev))
Copy link
Contributor

@johnynek johnynek Aug 31, 2020

Choose a reason for hiding this comment

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

I don't think we should use map with ev. Instead, we should use substitution to avoid any chance we do any work. If instead we took ev: A Is (X, Y), then we can use F.unzip(ev.substitute[F](fa))

I think it is fine to use Is rather than As (<~<) because in practice we never really have subclasses of tuples. We don't know that F is covariant, even though I think effectively it has to be in order to be a functor, but we didn't require that on the trait.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@johnynek sorry, I was MIA for awhile. Let me have a look, will get back to you sometime this week.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@johnynek I've done some testing withIs/Leibinz. I had to flip the type parameters A and (X, Y) around to make it work, i.e:

def unzip[X, Y](implicit F: Functor[F], ev: (X, Y) Is A): (F[X], F[Y]) = F.unzip(ev.flip.substitute(fa))

Also, because tuples are covariant, there is still a possibility of a Tuple2 to be a subtype of another Tuple2, eg:

    sealed trait Animal
    final case class Cat() extends Animal
    final case class Dog() extends Animal

    val l: NonEmptyList[(Cat, Dog)] = NonEmptyList one Tuple2(Cat(), Dog())
    Functor[NonEmptyList].unzip[Animal, Animal](l) //compiles for both `Is` and `As`
    l.unzip[Animal, Animal] //does not compile for `Is`

This would result in slight inconsistency between the prefix and postfix versions of unzip.

As for As I've tried to replace map with widen like this:

def unzip[X, Y](implicit F: Functor[F], ev: A <~< (X, Y)): (F[X], F[Y]) = F.unzip(F.widen[A, (X, Y)](fa))

but it would not fit the type bound. Otherwise, calling asInstanceOf directly would work:

def unzip[X, Y](implicit F: Functor[F], ev: A <~< (X, Y)): (F[X], F[Y]) = F.unzip(fa.asInstanceOf[F[(X, Y)]])

or we could implement another widen that requires an evidence of As instead of a type bound constraint:

  def widenAs[A, B: A <~< *](fa: F[A]): F[B] = fa.asInstanceOf[F[B]]
  def widen[A, B >: A](fa: F[A]): F[B] = widenAs(fa)

Copy link
Contributor

@johnynek johnynek Sep 9, 2020

Choose a reason for hiding this comment

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

what about this:

final class FunctorTuple2Ops[F[_], A, B](private val fa: F[(A, B)]) extends AnyVal {
  def _1F(implicit F: Functor[F]): F[A] = F.map(fa)(_._1)
  def _2F(implicit F: Functor[F]): F[B] = F.map(fa)(_._2)
  def swapF(implicit F: Functor[F]): F[(B, A)] = F.map(fa)(_.swap)
  def unzip(implicit F: Functor[F]): (F[A], F[B]) = F.unzip(fa)
}

or something... sidestep using the evidence at all and only put the enrichment on Tuple2 types?

Copy link
Contributor

Choose a reason for hiding this comment

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

actually I see @travisbrown suggested this originally before I derailed things: #3318 (comment)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@johnynek @travisbrown I've made the change to FunctorTuple2Ops. I think some of my comments on that might have gone to the wrong PR when I accidentally closed and reopened this PR. The original intention was to fix unzip directly in the Functor typeclass but either should be fine now since a method extension would be required either way. We might still need type evidence for efficientunzip for specific types.

@codecov-commenter
Copy link

codecov-commenter commented Sep 9, 2020

Codecov Report

Merging #3318 into master will increase coverage by 0.00%.
The diff coverage is 100.00%.

@@           Coverage Diff           @@
##           master    #3318   +/-   ##
=======================================
  Coverage   91.31%   91.32%           
=======================================
  Files         386      387    +1     
  Lines        8617     8622    +5     
  Branches      243      248    +5     
=======================================
+ Hits         7869     7874    +5     
  Misses        748      748           

@johnynek
Copy link
Contributor

I think this looks good other than a comment about the Functor to use in the doc tests.

@yangzai
Copy link
Contributor Author

yangzai commented Sep 11, 2020

I think this looks good other than a comment about the Functor to use in the doc tests.

@johnynek do you mean that the doc tests with "Lifts XXX to Functor" should to be more concise about the shape of the functor?

@johnynek
Copy link
Contributor

I mean doing doc tests using Id[(Int, Int)] might be less enlightening to new comers since Id seems useless to newcomers. Option would be just about as easy to write the doc test examples and wouldn't be as trivial.

johnynek
johnynek previously approved these changes Sep 11, 2020
* scala> import cats.Id
* scala> import cats.syntax.functor._
*
* scala> ((1, 2): Id[(Int, Int)])._2F == 2
Copy link
Contributor

Choose a reason for hiding this comment

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

I think these docs and test would be nicer with a more interesting Functor than Id. Maybe Option?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@johnynek the doc test for unzip was ported from the 2.11 branch so I decided to keep to the same type. Tried Option but it failed for 2.13 because Option#unzip is already defined in 2.13. Switched to Chain as it is Cats specific.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@johnynek is Chain fine or should replace it with something else?

barambani
barambani previously approved these changes Sep 17, 2020
johnynek
johnynek previously approved these changes Sep 17, 2020
@johnynek
Copy link
Contributor

@travisbrown can you double check before we merge? Any time we change API surface area I like to make sure there is good consensus.

Side note: it would be interesting if mima could tell you if a patch does or doesn't change surface area and we could label each PR. PRs that only change implementations for which there is coverage can be merged with less review IMO.

@travisbrown
Copy link
Contributor

@johnynek Sure, I'll take a look tomorrow.

@larsrh larsrh added this to the 2.4 milestone Feb 5, 2021
@larsrh larsrh dismissed stale reviews from johnynek and barambani via 79b6f06 February 5, 2021 21:08
@larsrh larsrh merged commit 1edbbea into typelevel:master Feb 6, 2021
@armanbilge armanbilge mentioned this pull request Feb 8, 2022
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.

8 participants