-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
Conversation
We can't change the signature of the |
@travisbrown thanks for the feedback. I'm looking at how it it was implemented here: #3234 . Hope I'm on the same page. |
Codecov Report
@@ 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
Continue to review full report at Codecov.
|
//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) | ||
} |
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.
@travisbrown Overloading seems to work as well but I'm not sure if there are any negative implication.
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.
@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.
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.
@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 Report
@@ 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
Continue to review full report at Codecov.
|
…3234) * backported unzip method in functor typeclass * fixed nonfunctioning doctest
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. |
* }}} | ||
* | ||
*/ | ||
def unzip[X, Y](implicit F: Functor[F], ev: A <~< (X, Y)): (F[X], F[Y]) = F.unzip(F.map(fa)(ev)) |
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 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.
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.
@johnynek sorry, I was MIA for awhile. Let me have a look, will get back to you sometime this week.
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.
@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)
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.
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?
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.
actually I see @travisbrown suggested this originally before I derailed things: #3318 (comment)
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.
@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 Report
@@ 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 |
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 |
I mean doing doc tests using |
* scala> import cats.Id | ||
* scala> import cats.syntax.functor._ | ||
* | ||
* scala> ((1, 2): Id[(Int, Int)])._2F == 2 |
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 these docs and test would be nicer with a more interesting Functor than Id. Maybe Option?
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.
@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.
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.
@johnynek is Chain
fine or should replace it with something else?
@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. |
@johnynek Sure, I'll take a look tomorrow. |
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.