Refactor `Ref#mapK` to take `Functor[G]` constraint instead of `Functor[F]`
Best practice is for mapK methods to require constraints on the target effect, not the original effect.
Out of curiosity: why is this best practice? And is it worth documenting? (Not in this PR, but maybe a followup.)
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[_].
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.
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.
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.
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 🤔