Skip to content

Restrict/generalize derives clauses #6961

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
merged 4 commits into from
Aug 9, 2019

Conversation

milessabin
Copy link
Contributor

@milessabin milessabin commented Jul 29, 2019

This PR reworks the earlier support for derives clauses for multi-parameter type classes (TL;DR it restricts MPTC derives to Eql whilst preserving the generation of givens for single parameter type classes and ADTs with type parameters of kind *) and generalizes the handling of single-parameter type classes to support (un)currying of the instances of type classes for ADTs which have type parameters of matching kinds, but possibly more or fewer (illustrative examples below).

The handling of derives Eql has been left largely unchanged apart from some slight simplifications due to it now only handling Eql.

Note that all cases which don't fall into one of the categories described below are rejected with an error message reporting that the deriving ADT cannot be unified with type argument(s) of the derived type class. Note that zero parameter type classes are now also rejected.

Special case: derives semantics for the Eql type class

This has been extracted from the earlier more general multi-parameter type class model. Modulo the assumptions below, the implied semantics are reasonable defaults for Eql.

Assumptions

  1. Type parameters of the deriving class correspond to all and only the elements of the deriving class which are relevant to equality (caveat: type params could be phantom, or the deriving class might have an element of a non-Eql type non-parametrically).

  2. Type parameters of kinds other than * can be assumed to be irrelevant to the derivation (caveat: consider ADTs of the form Foo[F[_]](fi: F[Int])).

Are these assumptions reasonable? They cover some important cases (eg. Tuples of all arities). derives Eql is opt-in, so if the semantics don't match those appropriate for the deriving class the author of that class can provide their own instance in the normal way. That being so, the question turns on whether there are enough types which fit these semantics for the feature to pay its way.

Note also that because Eql is a marker trait there are no methods to be implemented in the derived instance ... mere existence is enough.

Procedure

Example: Eql[L, R] with deriving class: class A[T, U].

We will be constructing an instance for Eql[A[T_L, U_L], A[T_R, U_R]]. We do this by constructing a polymorphic instance with pairwise givens for Eql on the elements on the left and right,

given derived$Eql [T_L, U_L, T_R, U_R] as Eql[A[T_L, U_L], A[T_R, U_R]] 
  given Eql[T_L, T_R], Eql[U_L, U_R] = Eql.derived

Note that instances will be polymorphic iff the ADT is polymorphic.

Single parameter type classes

"natural" instances

A type class instance is called "natural" if the kind of the ADT exactly matches the kind of the type class's single type parameter. Examples,

trait Show[T]
case class Prod0
def derived$Show: Show[Prod0] = Show.derived

trait Functor[F[_]]
case class Prod1[A]
def derived$Functor: Functor[Prod1] = Functor.derived

trait FunctorK[F[_[_]]]
case class Prod11[A[_]]
def derived$FunctorK: Functor[Prod11] = FunctorK.derived

trait Bifunctor[F[_, _]]
case class Prod2[A, B]
def derived$Bifunctor: Bifunctor[Prod2] = Bifunctor.derived

Note that all "natural" instances will be monomorphic.

(un)curried instances

ADTs which have more or fewer type parameters than the type class's single type parameter are adapted to the type class's shape by the insertion of a type lambda and (if the ADT has more type parameters, the instance method will be polymorphic). The ADT and type class parameters must overlap on the right and have the same kinds at the overlap.

The following illustrates the type parameter alignment process,

Type class: TC[F[T, U]]

ADT: C[A, B, C, D]         (C, D have same kinds as T, U)

     given derived$TC[a, b]: TC[[t, u] =>> C[a, b, t, u]]

ADT: C[A, B, C]            (B, C have same kinds as T, U)

     given derived$TC   [a]: TC[[t, u] =>>    C[a, t, u]]

ADT: C[A, B]               (A, B have same kinds as T, U)

     given derived$TC      : TC[           C            ]  // a "natural" instance

ADT: C[A]                  (A has same kind as U)

     given derived$TC      : TC[[t, u] =>>          C[u]]

This typically yields instances of the expected form, eg.

trait Functor[F[_]]

sealed trait Either[L, R]
given derived$Functor[L]: Functor[[r] => Either[L, r]] = Functor.derived

case class Prod0()
given derived$Functor: Functor[[t] => Prod0] = Functor.derived

which are the instances you would expect to write by hand for those ADTs (consitent with partial unification in the Either case, and a typical "const" instance in the Prod0 case).

In the implementation, the form of the "natural" instances described in the previous section drop out naturally from the process of aligning type parameters between the ADT and the type class.

Note that instances will never have given arguments and with be polymorphic iff the ADT has more type parameters than the type class argument type.

"lowered" instances with givens

In the case where the type class type parameter and all ADT type parameters are of kind * a derives clause generates instances similarly to the way that the previous multi-parameter type class model did and the Eql derivation still does. From an implementation point of view this is mainly a special case extracted from the earlier implementation, and like the Eql derivation it typically produces the instances which are desired.

Note that In this case the ADT has at least one type parameter of kind *, which means that it is not a natural type for the type class.

The derived instance has a type parameter and a given for each of the type parameters of the ADT,

Show[T]
case class C[A, B]
given derived$Show[a, b] given Show[a], Show[b]: Show[C[a, b]] = Show.derived

@odersky
Copy link
Contributor

odersky commented Aug 6, 2019

I think I understand the principle. I agree it's the right thing to do.

Copy link
Contributor

@odersky odersky left a comment

Choose a reason for hiding this comment

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

Otherwise LGTM

@odersky odersky assigned milessabin and unassigned odersky Aug 8, 2019
@milessabin milessabin force-pushed the topic/multi-param-derives branch from 486eedb to dccd734 Compare August 9, 2019 16:20
@milessabin
Copy link
Contributor Author

Rebased and responded to review.

@milessabin milessabin merged commit b1f6bb2 into scala:master Aug 9, 2019
@milessabin milessabin deleted the topic/multi-param-derives branch August 9, 2019 16:51
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.

2 participants