Skip to content

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

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

Merged

Conversation

armanbilge
Copy link
Member

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

@armanbilge armanbilge changed the title Refactor Ref#mapK to take Functor[G] constraint instead of `Functor[F] Refactor Ref#mapK to take Functor[G] constraint instead of Functor[F] Aug 13, 2024
@armanbilge armanbilge added this to the v3.6.0 milestone Aug 13, 2024
@durban
Copy link
Contributor

durban commented Sep 1, 2024

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

@armanbilge
Copy link
Member Author

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[_].

@djspiewak
Copy link
Member

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.

Copy link
Member

@djspiewak djspiewak left a comment

Choose a reason for hiding this comment

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

This does result in some theoretical compatibility breakage if someone is overriding mapK to do something weird. I'm pretty comfortable saying that's unsupported behavior though, and the deprecation warning will trigger.

@djspiewak djspiewak merged commit ecf93db into typelevel:series/3.x Oct 25, 2024
29 of 33 checks passed
@joroKr21
Copy link
Member

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.

@djspiewak
Copy link
Member

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.

@joroKr21
Copy link
Member

joroKr21 commented Mar 24, 2025

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 🤔

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants