Skip to content

Latest commit

 

History

History
60 lines (51 loc) · 4.18 KB

local-coherence.md

File metadata and controls

60 lines (51 loc) · 4.18 KB
layout title
doc-page
Local Coherence

In the context of instance declarations, global coherence means that for every type I and typeclass trait TC there is never more than one way in which I implements TC. Scala does not have a global coherence requirement for its implicits. Instead it reports an ambiguity error if an implicit search yields several different results. The advantage of Scala's approach lies in its flexibility, in particular its support for scoped implicits. But sometimes the ambiguity rules get in the way. Consider for instance the typeclass trait hierarchy presented in the last section, which involves Functor, Applicative, Monad, and Traverse. Now add a function like transform below that requires its parameter type to be both a Monad and a Traverse.

def transform[F[_] : Monad : Traverse](x: F[A], f: A => A) = { ... x.map(f) ... }

The call to map in the body of that function would be in error, because it is ambiguous - we don't know whether we should call the map method defined in Monad or the one defined in Traverse. To see this in more detail, expand the context bounds in transform and add a type ascription for x:

def transform[F[_], A](x: F[A], f: A => A)(
  implicit
    impl1: Monad.Impl[F],
    impl2: Traversable.Impl[F])
  = { ... (x: Functor[A]).map(f) ... }

There are two ways x can be converted to a Functor[A], one through impl1.inject the other through impl2.inject. Hence, an ambiguity error is reported. This problem was brought up in issue #2029 and was presented in a paper at the Scala Symposium 2017.

Note that if we assume List as the actual implementation, this ambiguity is a false negative, since List's implementation of map is the same for Monad and Traverse, being defined in the common extension ListApplicative. But in general, a type might well have two different implementations for map (or Functor methods in general) when seen as a Monad or as a Traverse.

The (as yet tentative) idea to solve the problem is to provide a way to constrain implicit values to have common implementations for some of their super-traits.

This can be done by introducing a predefined type constructor Super[_] as a member of all implementation objects of typeclass traits. If impl is an implementation object for type I and typeclass trait T, and U is a typeclass trait extended by T, then impl.Super[U] would give the type of the implementation object from which impl obtains all implementations of U. For instance, assuming the factored instance declarations in the last section,

     ListMonad.Super[Functor]  =  ListApplicative.type
  ListTraverse.Super[Functor]  =  ListApplicative.type

Indeed, all Functor methods defined by List are defined in ListApplicative, and inherited (i.e. implemented implicitly as forwarders) in both ListMonad and ListTraverse. On the other hand, if we had not used factorization for ListMonad, ListMonad.Super[Functor] would be ListMonad.type, since it would be ListMonad that defines some of the methods in Functor.

Once we have the Super type defined like this, we can add type constraints to enforce equalities. For instance:

def transform[F[_], A](x: F[A], f: A => A)(
  implicit
    impl1: Monad.Impl[F],
    impl2: Traversable.Impl[F],
    impl1.Super[Functor] =:= impl2.Super[Functor])
  = { ... (x: Functor[A]).map(f) ... }

Note that this construct involves parameter dependencies as implemented in #2079 - the third implicit parameter contains types that depend on the first two.

The final piece of the puzzle is to make ambiguity checking take such constraints into account. More precisely, if searching for a injection from a type T to a typeclass U, if there are two injectors i1 and i2 from T to U, but i1.Super[U] =:= i2.Super[U], make an arbitrary choice instead of signaling an ambiguity. With this rule, the x.map(f) call in the transform method typechecks.

In effect we have replaced global coherence by local coherence - the ability to detect and require that two implementations implement a typeclass trait in the same way.