Skip to content
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

Inheritance between optics #1088

Merged
merged 16 commits into from
Feb 23, 2021
Merged

Inheritance between optics #1088

merged 16 commits into from
Feb 23, 2021

Conversation

julien-truffaut
Copy link
Member

@julien-truffaut julien-truffaut commented Feb 22, 2021

Consistent method location

Fold and Getter defined each, index, ... as class methods while other optics defined these methods as extension methods. As a result, Getter#each took precedence over Lens#each.

composeXXX

Inheritance introduced regressions in type inference. This is because inheritance overloaded some methods. For example, the code below compiles without inheritance but fails afterward:

Iso.id[Either[String, Int]].composePrism(stdRight)

The compiler says that their are two overloaded versions of composePrism. This makes sense if you look at the signature of Getter and PLens.

trait Fold[S, A] {
    def composePrism[B, C, D](other: PPrism[A, B, C, D]): Fold[S, C] = ???
}

trait POptional[S, T, A, B] extends PTraversal[S, T, A, B] with Fold[S, A] {
  override def composePrism[C, D](other: PPrism[A, B, C, D]): POptional[S, T, C, D] = ???
}

Note that composePrism for Fold has 3 type parameters while the same method for POptional has 2 type parameters.

I thought that variance could help:

trait Fold[-S, +A] {
    def composePrism[C, D](other: PPrism[A, Nothing, C, D]): Fold[S, C] = ???
}

trait POptional[-S, +T, +A, -B] extends PTraversal[S, T, A, B] with Fold[S, A] {
  override def composePrism[C, D](other: PPrism[A, B, C, D]): POptional[S, T, C, D] = ???
}

Unfortunately, it still overlaod composePrism.

So I moved all the composeXXX and symbolic operator to extension method, this way they don't overlaod anything.

modifyF

I found a limitation of Scala with regards to inheritance. As @tpolecat mentioned it, Scala doesn't accept narrowing on inputs. This means we can't do:

trait Traversal[-S, +T, +A, -B] {
  def modifyF[F[_]](update: A => B)(s: S)(implicit ev: Applicative[F]): F[T @uncheckedVariance]
}

trait Lens[-S, +T, +A, -B]  extends Traversal[S, T, A, B]{
  // Overrides nothing, even though Functor is an Applicative
  override def modifyF[F[_]](update: A => B)(s: S)(implicit ev: Functor[F]): F[T @uncheckedVariance]
}

The only solution I could come up with is to rename modifyF in Traversal, Optional and Prism to modifyA and keep modifyF for Lens and Iso. If somone finds a better name, we can rename it later. Thanks to @joroKr21 for the suggestion.

@joroKr21
Copy link
Contributor

joroKr21 commented Feb 22, 2021

Using DummyImplicit would look like this

def modifyF[F[_]: Functor](f: A => F[B])(s: S)(implicit dummy: DummyImplicit): F[T] =
    Functor[F].map(f(get(s)))(reverseGet)

But then it would make delegating from the Applicative version weird and looking recursive.

Copy link
Collaborator

@kenbot kenbot left a comment

Choose a reason for hiding this comment

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

Reading the discussion and the code, this looks ok to me, as far as I understand the issues.

@julien-truffaut julien-truffaut merged commit 625984d into master Feb 23, 2021
@julien-truffaut julien-truffaut deleted the only-inheritance branch February 23, 2021 14:35
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.

3 participants