1

When read the cats library's Functor source, I could not understand what the curly block after the return type of the function toFunctorOps does; My guess is that this block will be executed as part of constructor? If so, then why type TypeClassType is defined twice with the same code type TypeClassType = Functor[F]?

  trait Ops[F[_], A] extends Serializable {
    type TypeClassType <: Functor[F]
    def self: F[A]
    val typeClassInstance: TypeClassType
    def map[B](f: A => B): F[B] = typeClassInstance.map[A, B](self)(f)
    ...
  }

  trait ToFunctorOps extends Serializable {
    implicit def toFunctorOps[F[_], A](target: F[A])(implicit tc: Functor[F]): Ops[F, A] {
      type TypeClassType = Functor[F]
    } =
      new Ops[F, A] {
        type TypeClassType = Functor[F]
        val self: F[A] = target
        val typeClassInstance: TypeClassType = tc
      }
  }
DJ Chen
  • 105
  • 1
  • 7

1 Answers1

6

I could not understand what the curly block after the return type ... does

The refinement { type TypeClassType = Functor[F] } puts a further constraint on type member TypeClassType of trait Ops. In other words it gives more information to the compiler about the specific return type of method toFunctorOps

Ops[F, A] { type TypeClassType = Functor[F] }

Note the refinement block is considered part of the return type and has nothing to do with the constructor.

Let us simplify the types to illustrate the concept better, so consider

trait Foo {
  type A
  def bar: A
}

val foo: Foo = new Foo { 
  type A = Int 
  def bar: A = ???
}
val x: foo.A = 42 // type mismatch error

Note how static type of variable foo does not include specific information that type member A has been instantiated to Int. Now let's give the compiler this information by using type refinement

val foo: Foo { type A = Int } = new Foo { 
  type A = Int
  def bar: A = ??? 
}
val x: foo.A = 42 // ok

Now compiler knows that type member A is precisely an Int.

Designers of type classes make judicious decisions regarding when to use type members as opposed to type parameters, and sometimes there is even a mix of the two as in your case. For example trait Foo could have been parameterised like so

trait Foo[A] {
  def bar: A
}
val foo: Foo[Int] = new Foo[Int] { 
  def bar: Int = ??? 
}

and compiler would again have precise information that type parameter A has been instantiated to Int.

why type TypeClassType is defined twice

The refined type Foo { type A = Int } is a narrower subtype of Foo, similar to how Cat is a narrower subtype of Animal

implicitly[(Foo { type A = Int }) <:< Foo]
implicitly[Cat <:< Animal]

so even though the right-hand side expression instantiates A as Int, the left-hand side explicitly told the compiler that the static type of foo is just the wider supertype Foo

val foo: Foo = new Foo { 
  type A = Int 
  def bar: A = ???
}

similarly to how compiler knows that static type of zar below is only the wider supertype Animal despite the expression on the RHS specifying a Cat

val zar: Animal = new Cat

Hence the need for "double" type specification

val foo: Foo { type A = Int } = new Foo { 
  type A = Int 
  ...
}

similar to

val zar: Cat = new Cat

We could try to rely on inference to deduce the most specific type, however when we are explicitly annotating types then we have to provide the full information which includes the type member constraints via refinements.

Mario Galic
  • 47,285
  • 6
  • 56
  • 98