cats-effect icon indicating copy to clipboard operation
cats-effect copied to clipboard

Refactor `Ref#mapK` to take `Functor[G]` constraint instead of `Functor[F]`

Open armanbilge opened this issue 1 year ago • 2 comments

Best practice is for mapK methods to require constraints on the target effect, not the original effect.

armanbilge avatar Aug 13 '24 22:08 armanbilge

Out of curiosity: why is this best practice? And is it worth documenting? (Not in this PR, but maybe a followup.)

durban avatar Sep 01 '24 07:09 durban

why is this best practice?

The general pattern is that we collect constraints at creation, not at use-site. In fact, if we made this an abstract method, we wouldn't need to collect a use-site constraint on the method. In this case, this is a use-site in F[_] but creation in G[_] so it's reasonable to collect a constraint for G[_].

armanbilge avatar Oct 15 '24 19:10 armanbilge

The general pattern is that we collect constraints at creation, not at use-site.

Fascinatingly, this is a thing that has a bit of nuance. For example, if you have a functional OrderedSet implementation, you wouldn't collect any constraints at creation and instead you would have the constraint on the add method. This would result in a strictly more flexible API, since OrderedSet.empty[A] is defined for all A.

I still essentially agree with what you're saying but I think this is an area that has defied hard and fast idioms.

djspiewak avatar Oct 25 '24 15:10 djspiewak

Fascinatingly, this is a thing that has a bit of nuance. For example, if you have a functional OrderedSet implementation, you wouldn't collect any constraints at creation and instead you would have the constraint on the add method. This would result in a strictly more flexible API, since OrderedSet.empty[A] is defined for all A.

That invites the possibility of using the same set with inconsistent ordering in different places.

joroKr21 avatar Oct 25 '24 22:10 joroKr21

That invites the possibility of using the same set with inconsistent ordering in different places

Yes indeed, which was exactly why I argued in favor of capturing it at point of creation in the past. Basically, incoherence causes all sorts of these types of weirdnesses.

djspiewak avatar Oct 27 '24 17:10 djspiewak

I'm thinking of a type class:

trait FunctorK[T[_[_]]] {
  def mapK[F[_]: Functor, G[_]](tf: T[F])(fk: F ~> G): T[G]
}

because I need it for higher-kinded data but I don't know what to call it and I'm still not convinced the constraint should be on G instead of F. Both seem more or less equivalent but to me it looks better on the F - is there even a use-case when one of them is a functor and the other one isn't?

The general pattern is that we collect constraints at creation, not at use-site. In fact, if we made this an abstract method, we wouldn't need to collect a use-site constraint on the method. In this case, this is a use-site in F[] but creation in G[] so it's reasonable to collect a constraint for G[_].

This all sounds a bit hand-wavy. I'm not even sure what those sentences mean. It's a use-site of going from F to G, that's the whole point. But if we think in terms of data, maybe it's indeed better on G - is it more likely that it will be cheaper or more expensive than F? Well, we can easily discard information, like go from List ~> Option but the opposite doesn't sound that likely 🤔

joroKr21 avatar Mar 24 '25 22:03 joroKr21