Skip to content

Conversation

@johnynek
Copy link
Contributor

I reached for tupleLeft in this PR:

typelevel/cats-collections#779

But then looked and noticed that actually it and related functions haven't been overridden in many places. This PR addresses that.

@johnynek
Copy link
Contributor Author

I couldn't find the native error. I guess is may be some issue with the native being flakey? I can't see why the changes would fail on native but not JVM.

danicheg
danicheg previously approved these changes Dec 19, 2025
Copy link
Member

@danicheg danicheg left a comment

Choose a reason for hiding this comment

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

Adding some tests would be nice. Anyway, LGTM

@johnynek
Copy link
Contributor Author

Adding some tests would be nice. Anyway, LGTM

I've added some tests. Ideally these would be added to FunctorLaws saying that those methods need to match the map based implementations. Doing that, however, would break compatibility of FunctorLaws because it requires more Arbitrary and Eq instances (a major weakness of our Laws).

Honestly, I wonder if the laws need some higher kinded change. Something like ArbitraryK[[A] =>> [Arbitrary[A] => Arbitrary[F[A]]] and similiar for Eq and Cogen and then maybe define case class TestType[A](arbA: Arbitrary[A], eqA: Eq[A], cogenA: Cogen[A]) and then the system defines ArbitraryK[TestType] and then we could define new laws without requiring changes to the function types and breaking compatibility. But obviously this is a major rewrite of how the laws work.

@johnynek
Copy link
Contributor Author

@danicheg how does this look? I think it is ready to merge.

}
override def distribute[F[_], A, B](fa: F[A])(f: A => B)(implicit F: Functor[F]): Id[F[B]] = F.map(fa)(f)
override def map[A, B](fa: A)(f: A => B): B = f(fa)
override def as[A, B](fa: A, b: B): B = b
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder – should we get void here for completeness? As long as as is here.

@satorg
Copy link
Contributor

satorg commented Dec 20, 2025

@johnynek I totally agree that ideally these tests should be covered in the Laws. However, since it not feasible to achieve it easily, wouldn't it make sense to generalize them anyway. E.g. something like this:

trait FunctorSuiteBase { self: CatsSuite =>
  def testMapBasedImplementations[F[_]: Functor](implicit
    arbFI: Arbitrary[F[Int]], arbFIS: Arbitrary[F[(Int, String)]],
    eqFU: Eq[F[Unit]], eqFI: Eq[F[Int]], eqFS: Eq[F[String]], eqFIS: Eq[F[(Int, String)]], eqFSI: Eq[F[(String, Int)]], eqFIL: Eq[F[(Int, Long)]], eqFLI: Eq[F[(Long, Int)]]
  ): Unit =
    test("functor default methods match map-based implementations") {
      val F = Functor[F]
      forAll { (fa: F[Int], b: String, f: Int => Long) =>
        assert(F.as(fa, b) === F.map(fa)(_ => b))
        assert(F.tupleLeft(fa, b) === F.map(fa)(a => (b, a)))
        assert(F.tupleRight(fa, b) === F.map(fa)(a => (a, b)))
        assert(F.fproduct(fa)(f) === F.map(fa)(a => (a, f(a))))
        assert(F.fproductLeft(fa)(f) === F.map(fa)(a => (f(a), a)))
        assert(F.void(fa) === F.map(fa)(_ => ()))
      }
      forAll { (fab: F[(Int, String)]) =>
        assert(F.unzip(fab) === (F.map(fab)(_._1), F.map(fab)(_._2)))
      }
    }
}

and then call testMapBasedImplementations from every affected suite.

All these new tests look so identical that I couldn't help but suggest it out :)

@johnynek
Copy link
Contributor Author

@satorg I've added unit and void to cats.Id but I'd like to hold off on the abstraction you suggested. In my experience, the wrong abstraction is worse that a bit of simple duplication like this, and I've recently been tripped up by the law code, which is published and has binary compatibility requirements, causing issues. I'd rather just leave this very simple duplication for now vs get into designing the right abstraction here.

For instance, what if we add more map-based methods we'd like to test, but they require more implicits than the ones we add to that trait? Ideally we would have a clean story to add laws to the original FunctorLaws, (such as the higher kinded Arbitrary and Eq suggestion I made).

Really we want something like

trait Higher[F[_], G[_]] {
  def lower[A](f: F[A]): F[G[A]]
}

Then we would have these laws be given Higher[Arbitrary, F], Higher[Eq, F], Higher[Cogen, F]. We would also have have something like:

class TestType {
  type T
  def arbT: Arbitrary[T]
  def eqT: Eq[T]
  def cogenT: Cogen[T]
}

class TestTypeK {
  type T[_]
  def arbT: Higher[Arbitrary, T]
  def eqT: Higher[Eq, T]
  def cogenT: Higher[Cogen, T]
}

with this, I think maybe all laws on F[_] could be parameterized on Higher[Arbitrary, F], Higher[Eq, F], Higher[Cogen, F], Arbitrary[TestType], Arbitrary[TestTypeK]. or something like this...

In any case, I think this is way beyond the scope of this PR. Can we punt the abstraction of tests and just merge this relatively simple improvement to map-based functor methods for these small common types?

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.

Thanks!

@johnynek johnynek merged commit be5dd58 into main Dec 20, 2025
46 of 49 checks passed
@johnynek johnynek deleted the oscar/20251218-optimize_functor_methods branch December 20, 2025 21:51
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.

4 participants