(Pre-SIP) Realiasing types in error messages


The link to the proposal is here.

Unfortunately, because I’m a new user I couldn’t copy and paste the raw markdown content in the forum (too many links). I would be grateful if someone could copy and paste it for me.




(i got you fam)

Table of Contents

  1. Proposal
    1. Motivation
      1. Examples
    2. Summary
    3. Prior work
    4. Acknowledgements



Type aliases are heavily used in functional libraries such as cats, fs2 and ZIO,
to provide friendlier type signatures over the more complex encodings which are
used within the library itself.

This allows the libraries to hide some of the complexity to newcomers who may
never need it, but still allow the library to be general enough to cater to the
more complex use-cases.

A pattern which has emerged is to have a companion object as an entry
point, which addresses the problem of discoverability, but the aliases which are
defined are not preserved in function composition, which leads to “leakage” of the
complexity which authors are trying to hide in the first place.

This proposal aims to address the issue.


Consider the following snippet of code, which uses the pattern
described. Reader is the simplified type alias with the companion object.

    type Id[A] = A

    case class ReaderT[F[_], A, B](run: A => F[B]) {
      def flatMap[BB](f: B => ReaderT[F, A, BB]): ReaderT[F, B, BB] = ???

    type Reader[A, B] = ReaderT[Id, A, B]

    object Reader {
      def apply[A, B](f: A => B): Reader[A, B] = ReaderT(f)

    val provided: ReaderT[Id, String, String] = ???
    val problem = Reader[Int, String](_.toString).flatMap(_ => provided)

If at some other point in time a newcomer has the following snippet of code,

    val problematic: Reader[String, String] = ???
    Reader[Int, String](_.toString).flatMap(_ => problematic)

we get the following error message:

    [error] -- [E007] Type Mismatch Error: /home/user/helloworld/src/main/scala/Example.scala:19:51
    [error] 19 |val line = Reader[Int, String](_.toString).flatMap(_ => problematic)
    [error]    |                                                       ^^^^^^^
    [error]    |  Found:    (foo.problematic : foo.ReaderT[foo.Id, String, String])
    [error]    |  Required: foo.ReaderT[foo.Id, Int, String]

Ideally, the ReaderT[foo.Id, Int, String] should be replaced with
Reader[Int, String].

This is even worse when we “create” aliases through composition. Consider the
snippet below for a profunctor lens encoding.

    trait Profunctor[F[_, _]]
    trait Cartesian[F[_, _]] extends Profunctor[F]
    trait Cocartesian[F[_, _]] extends Profunctor[F]

    case class Optic[-C[_[_, _]], -S, +T, +A, -B]() {
      def <<<[U, V, C1[F[_, _]] <: C[F]](other: Optic[C1, U, V, S, T]): Optic[C1, U, V, A, B] = ???

    type Lens[-S, +T, +A, -B] = Optic[Cartesian, S, T, A, B]
    type Prism[-S, +T, +A, -B] = Optic[Cocartesian, S, T, A, B]
    type Optional[-S, +T, +A, -B] = Optic[[F[_, _]] =>> Cartesian[F] & Cocartesian[F], S, T, A, B]

    type MLens[S, A] = Optic[Cartesian, S, S, A, A]
    type MPrism[S, A] = Optic[Cocartesian, S, S, A, A]
    type MOptional[S, A] = Optic[[F[_, _]] =>> Cartesian[F] & Cocartesian[F], S, S, A, A]

The following snippet of user code,

    val lens: MLens[String, String] = ???
    val prism: MPrism[String, Int] = ???
    val what: MPrism[String, Int] = prism <<< lens

would produce the error message:

    [error] -- [E007] Type Mismatch Error: /home/user/helloworld/src/main/scala/Help.scala:40:42
    [error] 40 |val what: MPrism[String, Int] = prism <<< lens
    [error]    |                                          ^^^^
    [error]    |    Found:    (foo.lens : foo.MLens[String, String])
    [error]    |    Required: foo.Optic[foo.Cocartesian, String, String, String, String]


We propose that the compiler should alias applied types to the most precise
type alias before outputting error messages to the user.

In would be impractical to search everywhere in the context, so we limit the
scope to be searched to be on the prefix of the type.

The most precise type would be one which had the least
cardinality. The cardinality referred to here, is the cardinality of types. For
the purposes of discussion, we can define a partial order on the cardinality of
types where an unrestricted type parameter is the terminal object, and for all
refined types A and B, A > B if and only if the lower bound of A is a
supertype of the lower bound of B and the upper bound of A is a subtype of
the lower bound of B.

We can summarize most precise as type alias which has the least number of type
parameters and the most narrowed bounds.

We are unsure if this should be extended to all refined types, or simply type
applications, since the pattern described has only appeared when using type


A rudimentary implementation can be found on the branch here. The examples can
be run using dotc tests/neg/reader-realias.scala and dotc tests/neg/optic-realias.scala.

An example of the error messages produced is shown below.

-- [E007] Type Mismatch Error: tests/neg/reader-realias.scala:14:59 ------------
14 |val problem = Reader[Int, String](_.toString).flatMap(_ => provided)
   |                                                           ^^^^^^^^
   |                      Found:    (provided : type Reader[String, String])
   |                      Required: type Reader[Int, BB]
   |                      where:    BB is a type variable
1 error found
-- [E007] Type Mismatch Error: tests/neg/optic-realias.scala:22:42 -------------
22 |val what: MPrism[String, Int] = prism <<< lens
   |                                          ^^^^
   |                        Found:    (foo.lens : foo.MLens[String, String])
   |                        Required: type MPrism[String, String]
1 error found


Many thanks to the creators and contributors of the libraries for the examples.
Special thanks to Julien Truffaut, Zainab Ali, Adam Fraser and John De Goes for early
feedback of this proposal.


Perhaps interesting to note that the OCaml compiler already does something like that, so it’s not such an outlandish idea.

1 Like

Amazing work @yilinwei
This feature will drastically imporve the user experience for libraries like FS2, ZIO and Monocle!

I’m not convinced that we should always re-alias. I think both (aliasing and de-aliasing) have their pros and cons. Ideally, I would like to be able to interact with the compiler (or an IDE) to see through a specific type alias, in a type error.

In the example optic-realias.scala, I don’t know off the top of my head what’s the difference between MLens and MPrism, so I’d have to look at the definition of those types to see where is the actual mismatch, which might defeat the purpose of re-aliasing.

Another situation where I’m not sure aliasing would be ideal is something like:

type Ctx[A] = Context => A

val foo: Context => Int = ...
def bar(x: Ctx[Option[Int]]) = ...


In such a case, Scala 2 gives:

type mismatch;
 found   : Context => Int
 required: Ctx[Option[Int]]
    (which expands to)  Context => Option[Int]

Which, for me, makes it easier to spot the problem: when we align the given type and the expected type, we immediately find that there is a missing Option in the result type.
On the other hand, if the expansion of Ctx was not shown, it might be harder to align the given type with the expected type.


IMO at least private type aliases should not escape the scope. Sometimes, I define one like
type R = Map[K, F[V]] in case I have to repeat Map[K, F[V]] a lot and this should really just be an implementation detail.

Edit: well… as they cannot escape the scope anyway, they only appear in errors inside the same scope so I guess there it might make sense again to show the alias.

Can we have both? Both the terse type with the most specific type variables and then a fully expanded type, where all the variables are being substituted?
(Of course, showing these two cases makes sense only if they would be different.)