Skip to content

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

Merged
djspiewak merged 1 commit into
typelevel:series/3.xfrom
armanbilge:topic/ref-mapk-constraint
Oct 25, 2024
Merged

Refactor Ref#mapK to take Functor[G] constraint instead of Functor[F]#4115
djspiewak merged 1 commit into
typelevel:series/3.xfrom
armanbilge:topic/ref-mapk-constraint

Conversation

@armanbilge

Copy link
Copy Markdown
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

durban commented Sep 1, 2024

Copy link
Copy Markdown
Contributor

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

@armanbilge

Copy link
Copy Markdown
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
Copy Markdown
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.

@djspiewak djspiewak left a comment

Copy link
Copy Markdown
Member

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
@joroKr21

Copy link
Copy Markdown
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
Copy Markdown
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

joroKr21 commented Mar 24, 2025

Copy link
Copy Markdown
Member

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