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.